From 19161fa29ce4db14a0334d71409b321ac7f1d1f8 Mon Sep 17 00:00:00 2001 From: aoki-h-jp Date: Thu, 16 Mar 2023 10:52:47 +0900 Subject: [PATCH 1/6] Initial commit --- examples/get_large_divergence_single_exchange.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) create mode 100644 examples/get_large_divergence_single_exchange.py diff --git a/examples/get_large_divergence_single_exchange.py b/examples/get_large_divergence_single_exchange.py new file mode 100644 index 0000000..4f2aa89 --- /dev/null +++ b/examples/get_large_divergence_single_exchange.py @@ -0,0 +1,13 @@ +""" +An example of getting large divergence by single exchange. +""" +from frarb import FundingRateArbitrage + + +if __name__ == '__main__': + fr = FundingRateArbitrage() + # Display Top 5 large funding rate divergence on binance. + print(fr.display_large_divergence_single_exchange(exchange='binance', display_num=5)) + + # Display Top 5 large funding rate divergence on bybit (minus FR). + print(fr.display_large_divergence_single_exchange(exchange='bybit', display_num=5, minus=True)) From 5ee7433ee23453ae50772307852bf6d9050f0799 Mon Sep 17 00:00:00 2001 From: aoki-h-jp Date: Thu, 16 Mar 2023 10:54:42 +0900 Subject: [PATCH 2/6] Add commission for options trading, display_large_divergence_single_exchange --- frarb/frarb.py | 43 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 43 insertions(+) diff --git a/frarb/frarb.py b/frarb/frarb.py index 3ca4f9a..f0740ce 100644 --- a/frarb/frarb.py +++ b/frarb/frarb.py @@ -5,6 +5,9 @@ class FundingRateArbitrage: def __init__(self): self.exchanges = ['binance', 'bybit', 'okx', 'bitget', 'gate', 'coinex'] + # commission + self.is_taker = True + self.by_token = False @staticmethod def fetch_all_funding_rate(exchange: str) -> dict: @@ -22,6 +25,37 @@ def fetch_all_funding_rate(exchange: str) -> dict: perp = [p for p in info if info[p]['linear']] return {p: ex.fetch_funding_rate(p)['fundingRate'] for p in perp} + def display_large_divergence_single_exchange(self, exchange: str, minus=False, display_num=10) -> pd.DataFrame: + """ + Display large funding rate divergence on single CEX. + Args: + exchange (str): Name of exchange (binance, bybit, ...) + minus (bool): Sorted by minus FR or plus FR. + display_num (int): Number of display. + + Returns (pd.DataFrame): DataFrame sorted by large funding rate divergence. + + """ + fr = self.fetch_all_funding_rate(exchange=exchange) + columns = ['Funding Rate [%]', 'Commission [%]', 'Revenue [/100 USDT]'] + sr_fr = pd.Series(list(fr.values())) * 100 + if minus: + cm = self.get_commission(exchange=exchange, trade='futures', taker=self.is_taker, by_token=self.by_token)\ + + self.get_commission(exchange=exchange, trade='options', taker=self.is_taker, by_token=self.by_token)\ + + self.get_commission(exchange=exchange, trade='spot', taker=self.is_taker, by_token=self.by_token) + sr_cm = pd.Series([cm * 2 for i in range(len(sr_fr))]) + sr_rv = abs(sr_fr) - sr_cm + else: + cm = self.get_commission(exchange=exchange, trade='futures', taker=self.is_taker, by_token=self.by_token) \ + + self.get_commission(exchange=exchange, trade='spot', taker=self.is_taker, by_token=self.by_token) + sr_cm = pd.Series([cm * 2 for i in range(len(sr_fr))]) + sr_rv = sr_fr - sr_cm + + df = pd.concat([sr_fr, sr_cm, sr_rv], axis=1) + df.index = list(fr.keys()) + df.columns = columns + return df.sort_values(by=columns[0], ascending=minus).head(display_num) + def get_exchanges(self) -> list: """ Get a list of exchanges. @@ -74,6 +108,8 @@ def get_commission(exchange: str, trade: str, taker=True, by_token=False) -> flo return 0.018 else: return 0.02 + elif trade == 'options': + return 0.02 else: raise KeyError @@ -86,6 +122,8 @@ def get_commission(exchange: str, trade: str, taker=True, by_token=False) -> flo return 0.06 else: return 0.01 + elif trade == 'options': + return 0.03 else: raise KeyError @@ -101,6 +139,11 @@ def get_commission(exchange: str, trade: str, taker=True, by_token=False) -> flo return 0.05 else: return 0.02 + elif trade == 'options': + if taker: + return 0.03 + else: + return 0.02 else: raise KeyError From 4f80ebc350dbdcd5388b5841b21f278833ec99c0 Mon Sep 17 00:00:00 2001 From: aoki-h-jp Date: Thu, 16 Mar 2023 11:02:27 +0900 Subject: [PATCH 3/6] Update README.md --- README.md | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index c43332d..b10c8a7 100644 --- a/README.md +++ b/README.md @@ -23,13 +23,12 @@ This library can detect perpetual contract with a large divergence in funding ra ```bash - git clone https://github.com/aoki-h-jp/funding-rate-arbitrage.git pip install funding-rate-arbitrage - ``` ## Usage +### Fetch FR & commission ```python from frarb import FundingRateArbitrage @@ -42,6 +41,18 @@ fr_binance = fr.fetch_all_funding_rate(exchange='binance') cm_binance = fr.get_commission(exchange='binance', trade='futures', taker=False) ``` +### Display large FR divergence on single CEX +```python +# display large funding rate divergence on bybit +>>> fr.display_large_divergence_single_exchange(exchange='bybit', display_num=5) + Funding Rate [%] Commission [%] Revenue [/100 USDT] +CTC/USDT:USDT 0.1794 0.32 -0.1406 +CREAM/USDT:USDT 0.0338 0.32 -0.2862 +TWT/USDT:USDT 0.0295 0.32 -0.2905 +TLM/USDT:USDT 0.0252 0.32 -0.2948 +JASMY/USDT:USDT 0.0100 0.32 -0.3100 +``` + ## Disclaimer This project is for educational purposes only. You should not construe any such information or other material as legal, From eb3531bd0c23d4356455373d3e40a07a805fd720 Mon Sep 17 00:00:00 2001 From: aoki-h-jp Date: Thu, 16 Mar 2023 13:46:24 +0900 Subject: [PATCH 4/6] Initial commit --- examples/get_large_divergence_multi_exchange.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) create mode 100644 examples/get_large_divergence_multi_exchange.py diff --git a/examples/get_large_divergence_multi_exchange.py b/examples/get_large_divergence_multi_exchange.py new file mode 100644 index 0000000..2e58780 --- /dev/null +++ b/examples/get_large_divergence_multi_exchange.py @@ -0,0 +1,13 @@ +""" +An example of getting large divergence between multi exchange. +""" +from frarb import FundingRateArbitrage + + +if __name__ == '__main__': + fr = FundingRateArbitrage() + # Display Top 5 large funding rate divergence between multi exchange. + print(fr.display_large_divergence_multi_exchange(display_num=5, sorted_by='divergence')) + + # Display Top 5 large funding rate divergence between multi exchange sorted by revenue. + print(fr.display_large_divergence_multi_exchange(display_num=5, sorted_by='revenue')) From 68ef85f0e4bfbf8a3186f1bc2d20a88da3332d55 Mon Sep 17 00:00:00 2001 From: aoki-h-jp Date: Thu, 16 Mar 2023 13:47:00 +0900 Subject: [PATCH 5/6] Add display_large_divergence_multi_exchange, fix get_commission --- frarb/frarb.py | 95 +++++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 91 insertions(+), 4 deletions(-) diff --git a/frarb/frarb.py b/frarb/frarb.py index f0740ce..6a41cef 100644 --- a/frarb/frarb.py +++ b/frarb/frarb.py @@ -1,6 +1,10 @@ +from logging import getLogger import ccxt +from ccxt import ExchangeError import pandas as pd +logger = getLogger(__name__) + class FundingRateArbitrage: def __init__(self): @@ -23,7 +27,13 @@ def fetch_all_funding_rate(exchange: str) -> dict: ex = getattr(ccxt, exchange)() info = ex.load_markets() perp = [p for p in info if info[p]['linear']] - return {p: ex.fetch_funding_rate(p)['fundingRate'] for p in perp} + fr_d = {} + for p in perp: + try: + fr_d[p] = ex.fetch_funding_rate(p)['fundingRate'] + except ExchangeError: + logger.exception(f'{p} is not perp.') + return fr_d def display_large_divergence_single_exchange(self, exchange: str, minus=False, display_num=10) -> pd.DataFrame: """ @@ -39,9 +49,10 @@ def display_large_divergence_single_exchange(self, exchange: str, minus=False, d fr = self.fetch_all_funding_rate(exchange=exchange) columns = ['Funding Rate [%]', 'Commission [%]', 'Revenue [/100 USDT]'] sr_fr = pd.Series(list(fr.values())) * 100 + # TODO: Check perp or spot or options exists on CEX. if minus: - cm = self.get_commission(exchange=exchange, trade='futures', taker=self.is_taker, by_token=self.by_token)\ - + self.get_commission(exchange=exchange, trade='options', taker=self.is_taker, by_token=self.by_token)\ + cm = self.get_commission(exchange=exchange, trade='futures', taker=self.is_taker, by_token=self.by_token) \ + + self.get_commission(exchange=exchange, trade='options', taker=self.is_taker, by_token=self.by_token) \ + self.get_commission(exchange=exchange, trade='spot', taker=self.is_taker, by_token=self.by_token) sr_cm = pd.Series([cm * 2 for i in range(len(sr_fr))]) sr_rv = abs(sr_fr) - sr_cm @@ -56,6 +67,76 @@ def display_large_divergence_single_exchange(self, exchange: str, minus=False, d df.columns = columns return df.sort_values(by=columns[0], ascending=minus).head(display_num) + def display_large_divergence_multi_exchange(self, display_num=10, sorted_by='revenue') -> pd.DataFrame: + """ + Display large funding rate divergence between multi CEX. + "multi CEX" refers to self.exchanges. + Args: + display_num (int): Number of display. + sorted_by (str): Sorted by "revenue" or "divergence" + + Returns (pd.DataFrame): DataFrame sorted by large funding rate divergence. + + """ + if sorted_by == 'revenue': + sorted_by = 'Revenue [/100 USDT]' + elif sorted_by == 'divergence': + sorted_by = 'Divergence [%]' + else: + logger.error(f'{sorted_by} is not available.') + raise KeyError + + df = pd.DataFrame() + for ex in self.exchanges: + logger.info(f'fetching {ex}') + fr = self.fetch_all_funding_rate(exchange=ex) + df_ex = pd.DataFrame(fr.values(), index=list(fr.keys()), columns=[ex]).T + df = pd.concat([df, df_ex]) + df = df.T * 100 + + diff_list = [] + diff_d = {} + for i, data in df.iterrows(): + diff = data.max() - data.min() + diff_d[i] = diff + + df_diff = pd.DataFrame(diff_d.values(), index=list(diff_d.keys()), columns=['Divergence [%]']).T + df = pd.concat([df.T, df_diff]).T + + comm_list = [] + for i in df.index: + max_fr_exchange = df.loc[i][:-1].idxmax() + min_fr_exchange = df.loc[i][:-1].idxmin() + max_fr = df.loc[i][:-1].max() + min_fr = df.loc[i][:-1].min() + # TODO: Check perp or spot or options exists on CEX. + if max_fr >= 0 and min_fr >= 0: + min_commission = self.get_commission(exchange=min_fr_exchange, trade='spot') + max_commission = self.get_commission(exchange=max_fr_exchange, trade='futures') + elif max_fr >= 0 > min_fr: + max_commission = self.get_commission(exchange=max_fr_exchange, trade='futures') + min_commission = self.get_commission(exchange=min_fr_exchange, trade='futures') + else: + try: + max_commission = self.get_commission(exchange=max_fr_exchange, trade='options') + \ + self.get_commission(exchange=max_fr_exchange, trade='spot') + min_commission = self.get_commission(exchange=min_fr_exchange, trade='futures') + except KeyError: + max_commission = self.get_commission(exchange=max_fr_exchange, trade='futures') + min_commission = self.get_commission(exchange=min_fr_exchange, trade='futures') + sum_of_commission = 2 * (max_commission + min_commission) + comm_list.append(sum_of_commission) + + comm_d = {index: commission for index, commission in zip(df.index, comm_list)} + df_comm = pd.DataFrame(comm_d.values(), index=list(comm_d.keys()), columns=['Commission [%]']).T + df = pd.concat([df.T, df_comm]).T + + revenue = [diff_value - comm_value for diff_value, comm_value in zip(diff_d.values(), comm_d.values())] + df_rv = pd.DataFrame(revenue, index=list(comm_d.keys()), columns=['Revenue [/100 USDT]']).T + df = pd.concat([df.T, df_rv]).T + + return df.sort_values(by=sorted_by, ascending=False).head(display_num) + def get_exchanges(self) -> list: """ Get a list of exchanges. @@ -111,6 +192,7 @@ def get_commission(exchange: str, trade: str, taker=True, by_token=False) -> flo elif trade == 'options': return 0.02 else: + logger.error(f'{trade} is not available on {exchange}.') raise KeyError # https://www.bybit.com/ja-JP/help-center/bybitHC_Article?id=360039261154&language=ja @@ -125,6 +207,7 @@ def get_commission(exchange: str, trade: str, taker=True, by_token=False) -> flo elif trade == 'options': return 0.03 else: + logger.error(f'{trade} is not available on {exchange}.') raise KeyError # https://www.okx.com/fees @@ -145,6 +228,7 @@ def get_commission(exchange: str, trade: str, taker=True, by_token=False) -> flo else: return 0.02 else: + logger.error(f'{trade} is not available on {exchange}.') raise KeyError # https://www.bitget.com/ja/rate/ @@ -160,6 +244,7 @@ def get_commission(exchange: str, trade: str, taker=True, by_token=False) -> flo else: return 0.017 else: + logger.error(f'{trade} is not available on {exchange}.') raise KeyError # https://www.gate.io/ja/fee @@ -175,10 +260,11 @@ def get_commission(exchange: str, trade: str, taker=True, by_token=False) -> flo else: return 0.015 else: + logger.error(f'{trade} is not available on {exchange}.') raise KeyError # https://www.coinex.zone/fees?type=spot&market=normal - if exchange == 'gate': + if exchange == 'coinex': if trade == 'spot': if by_token: return 0.16 @@ -190,4 +276,5 @@ def get_commission(exchange: str, trade: str, taker=True, by_token=False) -> flo else: return 0.03 else: + logger.error(f'{trade} is not available on {exchange}.') raise KeyError From 30e8b2e016e67f5aa4ca9522b2b0c54a1da7ee7e Mon Sep 17 00:00:00 2001 From: aoki-h-jp Date: Thu, 16 Mar 2023 13:54:35 +0900 Subject: [PATCH 6/6] Update README.md --- README.md | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/README.md b/README.md index b10c8a7..11ddb21 100644 --- a/README.md +++ b/README.md @@ -53,6 +53,27 @@ TLM/USDT:USDT 0.0252 0.32 -0.2948 JASMY/USDT:USDT 0.0100 0.32 -0.3100 ``` +### Display large FR divergence between CEX +```python +# display large funding rate divergence between CEX. +>>> fr.display_large_divergence_multi_exchange(display_num=5, sorted_by='divergence') + binance bybit okx bitget gate coinex Divergence [%] Commission [%] Revenue [/100 USDT] +FIL/USDT:USDT -0.008948 -0.0229 -0.334535 -0.0084 -0.0240 -0.737473 0.729073 0.202 0.527073 +HNT/USDT:USDT -0.023885 -0.0125 NaN NaN 0.0056 0.304442 0.328327 0.180 0.148327 +WAXP/USDT:USDT NaN NaN NaN NaN 0.0100 0.205733 0.195733 0.500 -0.304267 +AXS/USDT:USDT -0.021292 -0.0385 -0.205174 -0.0212 -0.0282 -0.215217 0.194017 0.202 -0.007983 +OP/USDT:USDT -0.060397 -0.0228 -0.206011 -0.0601 -0.0147 -0.148713 0.191311 0.200 -0.008689 + +# sorted by revenue. +>>> fr.display_large_divergence_multi_exchange(display_num=5, sorted_by='revenue') + binance bybit okx bitget gate coinex Divergence [%] Commission [%] Revenue [/100 USDT] +FIL/USDT:USDT -0.004703 -0.0232 -0.334535 -0.0047 -0.0245 -0.737473 0.732773 0.202 0.530773 +HNT/USDT:USDT -0.030722 -0.0141 NaN NaN 0.0051 0.304442 0.335164 0.180 0.155164 +OP/USDT:USDT -0.057856 -0.0235 -0.206011 -0.0589 -0.0162 -0.148713 0.189811 0.200 -0.010189 +MKR/USDT:USDT 0.010000 0.0100 -0.056437 0.0104 0.0100 0.075530 0.131967 0.200 -0.068033 +TON/USDT:USDT NaN NaN -0.023741 NaN 0.0100 -0.116483 0.126483 0.200 -0.073517 +``` + ## Disclaimer This project is for educational purposes only. You should not construe any such information or other material as legal,