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

Release/1.1.0 #7

Merged
merged 8 commits into from
Mar 16, 2023
Merged
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
36 changes: 34 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -42,6 +41,39 @@ 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
```

### 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,
Expand Down
13 changes: 13 additions & 0 deletions examples/get_large_divergence_multi_exchange.py
Original file line number Diff line number Diff line change
@@ -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'))
13 changes: 13 additions & 0 deletions examples/get_large_divergence_single_exchange.py
Original file line number Diff line number Diff line change
@@ -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))
134 changes: 132 additions & 2 deletions frarb/frarb.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,17 @@
from logging import getLogger
import ccxt
from ccxt import ExchangeError
import pandas as pd

logger = getLogger(__name__)


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:
Expand All @@ -20,7 +27,115 @@ 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:
"""
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
# 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) \
+ 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 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:
"""
Expand Down Expand Up @@ -74,7 +189,10 @@ 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:
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
Expand All @@ -86,7 +204,10 @@ 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:
logger.error(f'{trade} is not available on {exchange}.')
raise KeyError

# https://www.okx.com/fees
Expand All @@ -101,7 +222,13 @@ 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:
logger.error(f'{trade} is not available on {exchange}.')
raise KeyError

# https://www.bitget.com/ja/rate/
Expand All @@ -117,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
Expand All @@ -132,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
Expand All @@ -147,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