197 lines
9.7 KiB
Python
197 lines
9.7 KiB
Python
import asyncio
|
|
import json
|
|
import logging
|
|
import os
|
|
import time
|
|
import traceback
|
|
from dataclasses import asdict
|
|
from datetime import datetime
|
|
from typing import AsyncContextManager
|
|
import modules.structs as structs
|
|
import pandas as pd
|
|
import requests
|
|
import valkey
|
|
from dotenv import load_dotenv
|
|
import modules.manual_leverage as leverage
|
|
import modules.aster_auth as aster_auth
|
|
|
|
### MANUAL LEVERAGE DATA ###
|
|
df_leverage_by_exch = pd.DataFrame(data=leverage.LEVERAGE_BY_EXCH)
|
|
|
|
### Database ###
|
|
# CON: AsyncContextManager | None = None
|
|
VAL_KEY: valkey.Valkey
|
|
VK_OUT: str = 'fr_engine_best_fund_rate_output'
|
|
|
|
### Logging ###
|
|
load_dotenv()
|
|
LOG_FILEPATH: str = f'{os.getenv(key="LOGS_PATH")}/Fund_Rate_Engine_BFR.log'
|
|
|
|
### CONSTANTS ###
|
|
LOOP_SLEEP_SEC: int = 5
|
|
REFRESH_MKT_INFO_EVERY_SEC: int = 90
|
|
REFRESH_MKT_VOLUME_EVERY_SEC: int = 30
|
|
|
|
### GLOBALS ###
|
|
Mkt_Info_Last_Refresh_TS_ms: int = 0
|
|
Mkt_Volume_Last_Refresh_TS_ms: int = 0
|
|
|
|
### Funcs - Load Data ###
|
|
async def get_extended_markets_info() -> pd.DataFrame:
|
|
r: dict = json.loads(s=requests.get(url='https://api.starknet.extended.exchange/api/v1/info/markets').text)
|
|
|
|
df: pd.DataFrame = pd.DataFrame(data=r['data'])
|
|
df['funding_rate'] = df['marketStats'].apply(lambda x: x.get('fundingRate',{}))
|
|
df['funding_rate_ts'] = df['marketStats'].apply(lambda x: x.get('nextFundingRate',{}))
|
|
df['daily_volume'] = df['marketStats'].apply(lambda x: x.get('dailyVolume',{})).astype(float)
|
|
df['min_order_size'] = df['tradingConfig'].apply(lambda x: x.get('minOrderSize',{}))
|
|
df['min_price'] = df['tradingConfig'].apply(lambda x: x.get('minPriceChange',{}))
|
|
df['max_leverage'] = df['tradingConfig'].apply(lambda x: x.get('maxLeverage',{}))
|
|
|
|
print('Extend markets info refreshed successfully')
|
|
|
|
return df
|
|
|
|
async def get_aster_exch_info() -> pd.DataFrame:
|
|
### ASTER EXCHANGE INFO ###
|
|
fut_acct_exchangeInfo: dict = {
|
|
"url": "/fapi/v3/exchangeInfo",
|
|
"method": "GET",
|
|
"params": {}
|
|
}
|
|
r: dict = await aster_auth.post_authenticated_url(fut_acct_exchangeInfo) # ty:ignore[invalid-assignment]
|
|
df = pd.DataFrame(r['symbols'])
|
|
df['min_order_size'] = df['filters'].apply(lambda x: [f for f in x if f.get('filterType', None) == 'LOT_SIZE'][0]['minQty'] )
|
|
df['min_price'] = df['filters'].apply(lambda x: [f for f in x if f.get('filterType', None) == 'PRICE_FILTER'][0]['minPrice'] )
|
|
|
|
fut_acct_ticker_stats: dict = {
|
|
"url": "/fapi/v3/ticker/24hr",
|
|
"method": "GET",
|
|
"params": {}
|
|
}
|
|
r: dict = await aster_auth.post_authenticated_url(fut_acct_ticker_stats) # ty:ignore[invalid-assignment]
|
|
df_stats = pd.DataFrame(r)
|
|
|
|
df = df.merge(df_stats[['symbol','quoteVolume']].rename({'quoteVolume':'daily_volume'}, axis=1), on='symbol', how='left')
|
|
df['daily_volume'] = df['daily_volume'].astype(float)
|
|
|
|
|
|
print('Aster markets info refreshed successfully')
|
|
|
|
return df
|
|
|
|
def load_aster_current_fr(df_aster_exch_info: pd.DataFrame) -> pd.DataFrame:
|
|
df = pd.DataFrame(data=json.loads(s=VAL_KEY.get(name='fund_rate_aster_all'))) # ty:ignore[invalid-argument-type]
|
|
|
|
df: pd.DataFrame = df[['s','E','r','T']].rename({'s':'symbol','E':'funding_rate_updated_ts_ms','r':'funding_rate','T':'next_funding_ts'}, axis=1)
|
|
df['funding_rate_updated_dt'] = pd.to_datetime(df['funding_rate_updated_ts_ms'], unit='ms')
|
|
df['funding_rate'] = df['funding_rate'].astype(float)
|
|
df['time_delta_to_next_funding'] = pd.to_datetime(df['next_funding_ts'], unit='ms') - pd.Timestamp.now()
|
|
df = df.merge(df_aster_exch_info[['symbol','daily_volume','min_order_size','min_price']], on='symbol', how='left')
|
|
|
|
return df
|
|
|
|
def load_extend_current_fr(df_mkt_stats: pd.DataFrame) -> pd.DataFrame:
|
|
df = pd.DataFrame(data=json.loads(s=VAL_KEY.get(name='fund_rate_extended_all'))) # ty:ignore[invalid-argument-type]
|
|
|
|
df: pd.DataFrame = df[['symbol','funding_rate_updated_ts_ms','funding_rate']]
|
|
df['funding_rate_updated_dt'] = pd.to_datetime(df['funding_rate_updated_ts_ms'], unit='ms')
|
|
df['funding_rate'] = df['funding_rate'].astype(float)
|
|
|
|
df: pd.DataFrame = df.merge(df_mkt_stats[['name','assetName','status','funding_rate_ts','daily_volume','min_order_size','min_price']].rename({'name':'symbol','funding_rate_ts':'next_funding_ts'}, axis=1), on='symbol', how='left')
|
|
df: pd.DataFrame = df.loc[df['status']=='ACTIVE',:]
|
|
df['USDT_Symbol'] = df['assetName'] + 'USDT'
|
|
|
|
df['time_delta_to_next_funding'] = pd.to_datetime(arg=df['next_funding_ts'], unit='ms') - pd.Timestamp.now()
|
|
|
|
return df
|
|
|
|
async def loop() -> None:
|
|
global Mkt_Info_Last_Refresh_TS_ms
|
|
try:
|
|
while True:
|
|
ts_arrival = round(datetime.now().timestamp() * 1000)
|
|
if ( ts_arrival - Mkt_Info_Last_Refresh_TS_ms ) > ( REFRESH_MKT_INFO_EVERY_SEC * 1000 ):
|
|
df_extend_mkt_stats = await get_extended_markets_info()
|
|
df_aster_exch_info = await get_aster_exch_info()
|
|
Mkt_Info_Last_Refresh_TS_ms = round(datetime.now().timestamp() * 1000)
|
|
|
|
df_aster_fr = load_aster_current_fr(df_aster_exch_info=df_aster_exch_info)
|
|
df_extend_fr = load_extend_current_fr(df_mkt_stats=df_extend_mkt_stats)
|
|
|
|
df_comb_fr = df_extend_fr.merge(df_aster_fr, left_on='USDT_Symbol', right_on='symbol', how='inner', suffixes=('_ext', '_ast'))
|
|
df_comb_fr['next_funding_at_same_time'] = (abs(df_comb_fr['time_delta_to_next_funding_ext'].dt.total_seconds() - df_comb_fr['time_delta_to_next_funding_ast'].dt.total_seconds()) / 60) < 1
|
|
df_comb_fr['net_funding_rate'] = (df_comb_fr[['funding_rate_ext', 'funding_rate_ast']].max(axis=1) - df_comb_fr[['funding_rate_ext', 'funding_rate_ast']].min(axis=1)).where(df_comb_fr['next_funding_at_same_time'], df_comb_fr['funding_rate_ext'])
|
|
df_comb_fr['net_funding_rate_abs'] = df_comb_fr['net_funding_rate'].abs()
|
|
|
|
### NET MULT ###
|
|
df_comb_fr = df_comb_fr.merge(right=df_leverage_by_exch.loc[df_leverage_by_exch['exchange']=='EXTEND'], left_on='assetName', right_on='lh_asset').merge(df_leverage_by_exch.loc[df_leverage_by_exch['exchange']=='ASTER'], left_on='assetName', right_on='lh_asset', suffixes=('_ext', '_ast'))
|
|
df_comb_fr['net_mult'] = 1 / ( ( 0.5 / df_comb_fr['max_leverage_ext'] ) + ( 0.5 / df_comb_fr['max_leverage_ast'] ) )
|
|
df_comb_fr['net_mult'] = df_comb_fr['net_mult'].round(2)
|
|
df_comb_fr['net_mult_x_net_fr_abs'] = df_comb_fr['net_funding_rate_abs'] * df_comb_fr['net_mult']
|
|
|
|
|
|
df_best_fr_rate: pd.DataFrame = df_comb_fr[['symbol_ext','symbol_ast','daily_volume_ext','daily_volume_ast','funding_rate_ext','funding_rate_ast','min_price_ext','min_price_ast','min_order_size_ext','min_order_size_ast','max_leverage_ext','max_leverage_ast','lh_asset_ext','lh_asset_ast','rh_asset_ext','rh_asset_ast','net_mult_x_net_fr_abs','net_funding_rate_abs','net_funding_rate','next_funding_at_same_time']].sort_values(by='net_mult_x_net_fr_abs', ascending=False).reset_index(drop=True)
|
|
|
|
min_daily_volume = 100_000
|
|
df_best_fr_rate = df_best_fr_rate.loc[ (df_best_fr_rate['daily_volume_ast']>=min_daily_volume) & (df_best_fr_rate['daily_volume_ext']>min_daily_volume) ,:].reset_index(drop=True)
|
|
|
|
ASTER = structs.Perpetual_Exchange(
|
|
mult = int(df_best_fr_rate['max_leverage_ast'][0]),
|
|
lh_asset = df_best_fr_rate['lh_asset_ast'][0],
|
|
rh_asset = df_best_fr_rate['rh_asset_ast'][0],
|
|
symbol_asset_separator = '',
|
|
initial_funding_rate=float(df_best_fr_rate['funding_rate_ast'][0]),
|
|
min_price=float(df_best_fr_rate['min_price_ast'][0]),
|
|
min_order_size=float(df_best_fr_rate['min_order_size_ast'][0]),
|
|
)
|
|
EXTEND = structs.Perpetual_Exchange(
|
|
mult = int(df_best_fr_rate['max_leverage_ext'][0]),
|
|
lh_asset = df_best_fr_rate['lh_asset_ext'][0],
|
|
rh_asset = df_best_fr_rate['rh_asset_ext'][0],
|
|
symbol_asset_separator = '-',
|
|
initial_funding_rate=float(df_best_fr_rate['funding_rate_ext'][0]),
|
|
min_price=float(df_best_fr_rate['min_price_ext'][0]),
|
|
min_order_size=float(df_best_fr_rate['min_order_size_ext'][0]),
|
|
)
|
|
|
|
best_next_funding_pair: dict[str, dict] = {'ASTER': asdict(obj=ASTER), 'EXTEND': asdict(obj=EXTEND)}
|
|
|
|
VAL_KEY.set(name=VK_OUT, value=json.dumps(obj=best_next_funding_pair))
|
|
print(df_best_fr_rate[['symbol_ext','max_leverage_ext','funding_rate_ast','funding_rate_ext','net_funding_rate','daily_volume_ast']].head(10))
|
|
time.sleep(LOOP_SLEEP_SEC)
|
|
continue
|
|
except valkey.exceptions.ConnectionError as e:
|
|
logging.info(f"Could not connect to Valkey. Please check the publish server is up; {e}")
|
|
except KeyboardInterrupt:
|
|
logging.info('SHUTTING DOWN...')
|
|
except Exception as e:
|
|
logging.error(traceback.format_exc())
|
|
logging.critical(f'*** CRASHED: {e}')
|
|
|
|
|
|
### STARTUP ###
|
|
async def main() -> None:
|
|
global VAL_KEY
|
|
# global CON
|
|
|
|
VAL_KEY = valkey.Valkey(host='localhost', port=6379, db=0, decode_responses=True)
|
|
# engine = create_async_engine('mysql+asyncmy://root:pwd@localhost/fund_rate')
|
|
|
|
await loop()
|
|
|
|
if __name__ == '__main__':
|
|
START_TIME = round(number=datetime.now().timestamp()*1000)
|
|
|
|
logging.info(msg=f'Log FilePath: {LOG_FILEPATH}')
|
|
|
|
logging.basicConfig(
|
|
force=True,
|
|
filename=LOG_FILEPATH,
|
|
level=logging.INFO,
|
|
format='%(asctime)s - %(levelname)s - %(message)s',
|
|
filemode='w'
|
|
)
|
|
logging.info(msg=f"STARTED: {START_TIME}")
|
|
|
|
asyncio.run(main()) |