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())