diff --git a/README.md b/README.md index fbbe20d..0c7ba6d 100644 --- a/README.md +++ b/README.md @@ -41,6 +41,18 @@ fr_binance = fr.fetch_all_funding_rate(exchange='binance') cm_binance = fr.get_commission(exchange='binance', trade='futures', taker=False) ``` +### Fetch FR history +```python +from frarb import FundingRateArbitrage + +fr = FundingRateArbitrage() + +# figure funding rate history +fr.fetch_funding_rate_history(exchange='binance', symbol='BTC/USDT:USDT') +``` +!['funding rate history example'](./img/readme_funding_rate_history.png) + + ### Display large FR divergence on single CEX ```python # display large funding rate divergence on bybit @@ -51,6 +63,72 @@ 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 + +# display Top 5 large funding rate divergence on bybit one by one. +>>> fr.display_one_by_one_single_exchange(exchange='bybit', display_num=5) +------------------------------------------------ +Revenue: -0.1663 / 100USDT +SELL: CTC/USDT:USDT Perp +BUY: CTC/USDT:USDT Spot +Funding Rate: 0.1537 % +Commission: 0.32 % +------------------------------------------------ +Revenue: -0.17200000000000001 / 100USDT +SELL: CREAM/USDT:USDT Perp +BUY: CREAM/USDT:USDT Spot +Funding Rate: 0.1480 % +Commission: 0.32 % +------------------------------------------------ +Revenue: -0.2107 / 100USDT +SELL: BOBA/USDT:USDT Perp +BUY: BOBA/USDT:USDT Spot +Funding Rate: 0.1093 % +Commission: 0.32 % +------------------------------------------------ +Revenue: -0.2854 / 100USDT +SELL: TLM/USDT:USDT Perp +BUY: TLM/USDT:USDT Spot +Funding Rate: 0.0346 % +Commission: 0.32 % +------------------------------------------------ +Revenue: -0.2953 / 100USDT +SELL: TOMO/USDT:USDT Perp +BUY: TOMO/USDT:USDT Spot +Funding Rate: 0.0247 % +Commission: 0.32 % + +# display Top 5 large funding rate divergence on bybit one by one (minus FR). +>>> fr.display_one_by_one_single_exchange(exchange='bybit', display_num=5, minus=True) +------------------------------------------------ +Revenue: -0.1458 / 100USDT +SELL: ARPA/USDT:USDT Options +BUY: ARPA/USDT:USDT Perp +Funding Rate: -0.2342 % +Commission: 0.38 % +------------------------------------------------ +Revenue: -0.2569 / 100USDT +SELL: MASK/USDT:USDT Options +BUY: MASK/USDT:USDT Perp +Funding Rate: -0.1231 % +Commission: 0.38 % +------------------------------------------------ +Revenue: -0.3056 / 100USDT +SELL: APE/USD:USDC Options +BUY: APE/USD:USDC Perp +Funding Rate: -0.0744 % +Commission: 0.38 % +------------------------------------------------ +Revenue: -0.3158 / 100USDT +SELL: SWEAT/USD:USDC Options +BUY: SWEAT/USD:USDC Perp +Funding Rate: -0.0642 % +Commission: 0.38 % +------------------------------------------------ +Revenue: -0.3166 / 100USDT +SELL: APE/USDT:USDT Options +BUY: APE/USDT:USDT Perp +Funding Rate: -0.0634 % +Commission: 0.38 % ``` ### Display large FR divergence between CEX @@ -72,6 +150,39 @@ HNT/USDT:USDT -0.030722 -0.0141 NaN NaN 0.0051 0.304442 0.335 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 + +# Display Top 5 large funding rate divergence between multi exchange. +>>> fr.display_one_by_one_multi_exchanges(display_num=5) +------------------------------------------------ +Revenue: 0.2184 USDT / 100USDT +SELL: coinex IOTA/USDT:USDT Perp (Funding Rate 0.3478 %) +BUY: okx IOTA/USDT:USDT Perp (Funding Rate -0.0706 %) +Divergence: 0.4184 % +Commission: 0.2000 % +------------------------------------------------ +Revenue: 0.1191 USDT / 100USDT +SELL: coinex DASH/USDT:USDT Perp (Funding Rate 0.4267 %) +BUY: okx DASH/USDT:USDT Spot +Divergence: 0.4191 % +Commission: 0.3000 % +------------------------------------------------ +Revenue: 0.1080 USDT / 100USDT +SELL: okx TON/USDT:USDT Perp (Funding Rate 0.0482 %) +BUY: coinex TON/USDT:USDT Perp (Funding Rate -0.2598 %) +Divergence: 0.3080 % +Commission: 0.2000 % +------------------------------------------------ +Revenue: 0.0842 USDT / 100USDT +SELL: binance GMX/USDT:USDT Perp (Funding Rate 0.0100 %) +BUY: coinex GMX/USDT:USDT Perp (Funding Rate -0.2542 %) +Divergence: 0.2642 % +Commission: 0.1800 % +------------------------------------------------ +Revenue: 0.0447 USDT / 100USDT +SELL: okx FIL/USDT:USDT Perp (Funding Rate 0.2416 %) +BUY: gate FIL/USDT:USDT Perp (Funding Rate -0.0031 %) +Divergence: 0.2447 % +Commission: 0.2000 % ``` ## Future works diff --git a/examples/fetch_funding_rate_history.py b/examples/fetch_funding_rate_history.py new file mode 100644 index 0000000..01dc7df --- /dev/null +++ b/examples/fetch_funding_rate_history.py @@ -0,0 +1,11 @@ +""" +An example of fetching funding rate history +""" +from frarb import FundingRateArbitrage + + +if __name__ == '__main__': + # fetch from binance + fr = FundingRateArbitrage() + # figure funding rate history + fr.fetch_funding_rate_history(exchange='binance', symbol='BTC/USDT:USDT') diff --git a/examples/get_large_divergence_multi_exchange.py b/examples/get_large_divergence_multi_exchange.py index 2e58780..86fc365 100644 --- a/examples/get_large_divergence_multi_exchange.py +++ b/examples/get_large_divergence_multi_exchange.py @@ -7,7 +7,12 @@ 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')) + # print(fr.display_large_divergence_multi_exchange(display_num=5, sorted_by='divergence')) + # TODO: Errors occur when running consecutively. + # ccxt.base.errors.BadRequest: binance {"code":-1104,"msg":"Not all sent parameters were read; read '0' parameter(s) but was sent '1'."} # 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')) + # print(fr.display_large_divergence_multi_exchange(display_num=5, sorted_by='revenue')) + + # Display Top 5 large funding rate divergence between multi exchange. + fr.display_one_by_one_multi_exchanges(display_num=5) diff --git a/examples/get_large_divergence_single_exchange.py b/examples/get_large_divergence_single_exchange.py index 4f2aa89..11b87ee 100644 --- a/examples/get_large_divergence_single_exchange.py +++ b/examples/get_large_divergence_single_exchange.py @@ -1,5 +1,5 @@ """ -An example of getting large divergence by single exchange. +An example of displaying large divergence by single exchange. """ from frarb import FundingRateArbitrage @@ -11,3 +11,9 @@ # 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)) + + # Display Top 5 large funding rate divergence on binance one by one. + fr.display_one_by_one_single_exchange(exchange='binance', display_num=5) + + # Display Top 5 large funding rate divergence on bybit one by one (minus FR). + fr.display_one_by_one_single_exchange(exchange='bybit', display_num=5, minus=True) diff --git a/frarb/frarb.py b/frarb/frarb.py index 6a41cef..4acc7af 100644 --- a/frarb/frarb.py +++ b/frarb/frarb.py @@ -1,9 +1,22 @@ -from logging import getLogger +""" +Main class of funding-rate-arbitrage +""" +import logging +from datetime import datetime import ccxt +from rich import print +from rich.logging import RichHandler from ccxt import ExchangeError import pandas as pd +import matplotlib.pyplot as plt -logger = getLogger(__name__) +logging.basicConfig( + level=logging.INFO, + format="%(message)s", + datefmt="[%X]", + handlers=[RichHandler(rich_tracebacks=True)] +) +log = logging.getLogger("rich") class FundingRateArbitrage: @@ -32,9 +45,43 @@ def fetch_all_funding_rate(exchange: str) -> dict: try: fr_d[p] = ex.fetch_funding_rate(p)['fundingRate'] except ExchangeError: - logger.exception(f'{p} is not perp.') + log.exception(f'{p} is not perp.') return fr_d + @staticmethod + def fetch_funding_rate_history(exchange: str, symbol: str) -> None: + """ + Fetch funding rates on all perpetual contracts listed on the exchange. + + Args: + exchange (str): Name of exchange (binance, bybit, ...) + symbol (str): Symbol (BTC/USDT:USDT, ETH/USDT:USDT, ...). + + Returns (dict): Dict of perpetual contract pair and funding rate. + + """ + ex = getattr(ccxt, exchange)() + funding_history_dict = ex.fetch_funding_rate_history(symbol=symbol) + funding_time = [datetime.fromtimestamp(d['timestamp'] * 0.001) for d in funding_history_dict] + funding_rate = [d['fundingRate'] * 100 for d in funding_history_dict] + plt.plot(funding_time, funding_rate, label='funding rate') + plt.hlines( + xmin=funding_time[0], + xmax=funding_time[-1], + y=sum(funding_rate) / len(funding_rate), + label='average', + colors='r', + linestyles='-.' + ) + plt.title(f'Funding rate history {symbol}') + plt.xlabel('timestamp') + plt.ylabel('Funding rate [%]') + plt.xticks(rotation=45) + plt.yticks(rotation=45) + plt.legend() + plt.tight_layout() + plt.show() + def display_large_divergence_single_exchange(self, exchange: str, minus=False, display_num=10) -> pd.DataFrame: """ Display large funding rate divergence on single CEX. @@ -45,6 +92,41 @@ def display_large_divergence_single_exchange(self, exchange: str, minus=False, d Returns (pd.DataFrame): DataFrame sorted by large funding rate divergence. + """ + return self.get_large_divergence_dataframe_single_exchange(exchange=exchange, minus=minus)\ + .sort_values(by='Funding Rate [%]', 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: + log.error(f'{sorted_by} is not available.') + raise KeyError + + return self.get_large_divergence_dataframe_multi_exchanges()\ + .sort_values(by=sorted_by, ascending=False).head(display_num) + + def get_large_divergence_dataframe_single_exchange(self, exchange: str, minus=False): + """ + Get large funding rate divergence on single CEX. + Args: + exchange (str): Name of exchange (binance, bybit, ...) + minus (bool): Sorted by minus FR or plus FR. + + Returns (pd.DataFrame): large funding rate divergence DataFrame. + """ fr = self.fetch_all_funding_rate(exchange=exchange) columns = ['Funding Rate [%]', 'Commission [%]', 'Revenue [/100 USDT]'] @@ -65,36 +147,23 @@ def display_large_divergence_single_exchange(self, exchange: str, minus=False, d 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) + return df - def display_large_divergence_multi_exchange(self, display_num=10, sorted_by='revenue') -> pd.DataFrame: + def get_large_divergence_dataframe_multi_exchanges(self): """ - Display large funding rate divergence between multi CEX. + Get 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. + Returns (pd.DataFrame): large funding rate divergence DataFrame. """ - 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}') + log.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() @@ -119,7 +188,7 @@ def display_large_divergence_multi_exchange(self, display_num=10, sorted_by='rev else: try: max_commission = self.get_commission(exchange=max_fr_exchange, trade='options') + \ - self.get_commission(exchange=max_fr_exchange, trade='spot') + 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') @@ -135,7 +204,78 @@ def display_large_divergence_multi_exchange(self, display_num=10, sorted_by='rev 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) + return df + + def display_one_by_one_single_exchange(self, exchange: str, minus=False, display_num=10): + """ + + Args: + exchange (str): Name of exchange (binance, bybit, ...) + minus (bool): Sorted by minus FR or plus FR. + display_num (int): Number of display. + + Returns: None + + """ + df = self.get_large_divergence_dataframe_single_exchange(exchange=exchange, minus=minus) + # TODO: Check perp or spot or options exists on CEX. + for i in df.sort_values(by='Funding Rate [%]', ascending=minus).head(display_num).index: + print('------------------------------------------------') + revenue = df.loc[i]["Revenue [/100 USDT]"] + if revenue > 0: + print(f'[bold deep_sky_blue1]Revenue: {revenue} / 100USDT[/]') + else: + print(f'[bold red]Revenue: {revenue} / 100USDT[/]') + if minus: + print(f'[bold red]SELL: {i} Options[/]') + print(f'[bold blue]BUY: {i} Perp[/]') + else: + print(f'[bold red]SELL: {i} Perp[/]') + print(f'[bold blue]BUY: {i} Spot[/]') + print(f'Funding Rate: {df.loc[i]["Funding Rate [%]"]:.4f} %') + print(f'Commission: {df.loc[i]["Commission [%]"]} %') + + def display_one_by_one_multi_exchanges(self, display_num=10, sorted_by='revenue'): + """ + + Args: + display_num (int): Number of display. + sorted_by (str): Sorted by "revenue" or "divergence" + + Returns: None + + """ + if sorted_by == 'revenue': + sorted_by = 'Revenue [/100 USDT]' + elif sorted_by == 'divergence': + sorted_by = 'Divergence [%]' + else: + log.error(f'{sorted_by} is not available.') + raise KeyError + df = self.get_large_divergence_dataframe_multi_exchanges() + # TODO: Check perp or spot or options exists on CEX. + for i in df.sort_values(by=sorted_by, ascending=False).head(display_num).index: + print('------------------------------------------------') + revenue = df.loc[i]["Revenue [/100 USDT]"] + if revenue > 0: + print(f'[bold deep_sky_blue1]Revenue: {revenue:.4f} USDT / 100USDT[/]') + else: + print(f'[bold red]Revenue: {revenue:.4f} USDT / 100USDT[/]') + max_fr_exchange = df.loc[i][:-3].idxmax() + min_fr_exchange = df.loc[i][:-3].idxmin() + max_fr = df.loc[i][:-3].max() + min_fr = df.loc[i][:-3].min() + if max_fr > 0 and min_fr > 0: + print(f'[bold red]SELL: {max_fr_exchange} {i} Perp (Funding Rate {max_fr:.4f} %)[/]') + print(f'[bold blue]BUY: {min_fr_exchange} {i} Spot[/]') + elif max_fr > 0 > min_fr: + print(f'[bold red]SELL: {max_fr_exchange} {i} Perp (Funding Rate {max_fr:.4f} %)[/]') + print(f'[bold blue]BUY: {min_fr_exchange} {i} Perp (Funding Rate {min_fr:.4f} %)[/]') + else: + print(f'[bold red]SELL: {max_fr_exchange} {i} Options[/]') + print(f'[bold blue]BUY: {min_fr_exchange} {i} Perp (Funding Rate {min_fr:.4f} %)[/]') + print(f'Divergence: {df.loc[i]["Divergence [%]"]:.4f} %') + print(f'Commission: {df.loc[i]["Commission [%]"]:.4f} %') def get_exchanges(self) -> list: """ @@ -192,7 +332,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}.') + log.error(f'{trade} is not available on {exchange}.') raise KeyError # https://www.bybit.com/ja-JP/help-center/bybitHC_Article?id=360039261154&language=ja @@ -207,7 +347,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}.') + log.error(f'{trade} is not available on {exchange}.') raise KeyError # https://www.okx.com/fees @@ -228,7 +368,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}.') + log.error(f'{trade} is not available on {exchange}.') raise KeyError # https://www.bitget.com/ja/rate/ @@ -244,7 +384,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}.') + log.error(f'{trade} is not available on {exchange}.') raise KeyError # https://www.gate.io/ja/fee @@ -260,7 +400,7 @@ 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}.') + log.error(f'{trade} is not available on {exchange}.') raise KeyError # https://www.coinex.zone/fees?type=spot&market=normal @@ -276,5 +416,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}.') + log.error(f'{trade} is not available on {exchange}.') raise KeyError diff --git a/img/readme_funding_rate_history.png b/img/readme_funding_rate_history.png new file mode 100644 index 0000000..969a242 Binary files /dev/null and b/img/readme_funding_rate_history.png differ diff --git a/requirements.txt b/requirements.txt index 43313cc..f37adf6 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,2 +1,4 @@ ccxt -pandas \ No newline at end of file +pandas +rich +matplotlib \ No newline at end of file