172 lines
8.1 KiB
Python
172 lines
8.1 KiB
Python
|
|
import asyncio
|
||
|
|
import json
|
||
|
|
import logging
|
||
|
|
import os
|
||
|
|
import time
|
||
|
|
import traceback
|
||
|
|
from dataclasses import dataclass, field
|
||
|
|
from datetime import datetime
|
||
|
|
from typing import AsyncContextManager
|
||
|
|
|
||
|
|
import pandas as pd
|
||
|
|
import requests
|
||
|
|
import valkey
|
||
|
|
from dotenv import load_dotenv
|
||
|
|
|
||
|
|
# from sqlalchemy.ext.asyncio import create_async_engine
|
||
|
|
|
||
|
|
### Structs ###
|
||
|
|
@dataclass(kw_only=False)
|
||
|
|
class Asset_Leverage:
|
||
|
|
exchange: str
|
||
|
|
lh_asset: str
|
||
|
|
rh_asset: str
|
||
|
|
max_leverage: int
|
||
|
|
max_notional: float
|
||
|
|
# max_leverage_notional: list = field(default_factory=list)
|
||
|
|
|
||
|
|
### MANUAL LEVERAGE DATA ###
|
||
|
|
LEVERAGE_BY_EXCH: list[Asset_Leverage] = [
|
||
|
|
Asset_Leverage('ASTER', 'BTC' , 'USDT', 150, 300_000), Asset_Leverage('EXTEND', 'BTC' , 'USD', 50, 4_000_000),
|
||
|
|
Asset_Leverage('ASTER', 'ETH' , 'USDT', 150, 300_000), Asset_Leverage('EXTEND', 'ETH' , 'USD', 50, 4_000_000),
|
||
|
|
Asset_Leverage('ASTER', 'LIT' , 'USDT', 50 , 2_500 ), Asset_Leverage('EXTEND', 'LIT' , 'USD', 25, 400_000 ),
|
||
|
|
Asset_Leverage('ASTER', 'CHIP' , 'USDT', 50 , 5_000 ), Asset_Leverage('EXTEND', 'CHIP' , 'USD', 5 , 100_000 ),
|
||
|
|
Asset_Leverage('ASTER', 'XAG' , 'USDT', 100, 50_000 ), Asset_Leverage('EXTEND', 'XAG' , 'USD', 10, 1_000_000),
|
||
|
|
Asset_Leverage('ASTER', '4' , 'USDT', 50 , 5_000 ), Asset_Leverage('EXTEND', '4' , 'USD', 5 , 100_000 ),
|
||
|
|
Asset_Leverage('ASTER', 'XPT' , 'USDT', 3 , 30_000 ), Asset_Leverage('EXTEND', 'XPT' , 'USD', 5 , 1_000_000),
|
||
|
|
Asset_Leverage('ASTER', 'XMR' , 'USDT', 50 , 10_000 ), Asset_Leverage('EXTEND', 'XMR' , 'USD', 25, 400_000 ),
|
||
|
|
Asset_Leverage('ASTER', 'WLFI' , 'USDT', 25 , 104_869), Asset_Leverage('EXTEND', 'WLFI' , 'USD', 10, 250_000 ),
|
||
|
|
Asset_Leverage('ASTER', 'TRUMP', 'USDT', 50 , 5_567 ), Asset_Leverage('EXTEND', 'TRUMP', 'USD', 25, 400_000 ),
|
||
|
|
Asset_Leverage('ASTER', 'INIT' , 'USDT', 50 , 5_000 ), Asset_Leverage('EXTEND', 'INIT' , 'USD', 5 , 100_000 ),
|
||
|
|
Asset_Leverage('ASTER', 'ZORA' , 'USDT', 5 , 100_000), Asset_Leverage('EXTEND', 'ZORA' , 'USD', 5 , 100_000 ),
|
||
|
|
Asset_Leverage('ASTER', 'ZEC' , 'USDT', 75 , 6_250 ), Asset_Leverage('EXTEND', 'ZEC' , 'USD', 10, 250_000 ),
|
||
|
|
]
|
||
|
|
df_leverage_by_exch = pd.DataFrame(LEVERAGE_BY_EXCH)
|
||
|
|
|
||
|
|
### Database ###
|
||
|
|
# CON: AsyncContextManager | None = None
|
||
|
|
VAL_KEY = None
|
||
|
|
VK_OUT = 'fr_engine_best_fund_rate_output'
|
||
|
|
|
||
|
|
### Logging ###
|
||
|
|
load_dotenv()
|
||
|
|
LOG_FILEPATH: str = os.getenv("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
|
||
|
|
Mkt_Volume_Last_Refresh_TS_ms: int
|
||
|
|
|
||
|
|
### Funcs - Load Data ###
|
||
|
|
def get_extended_markets_info() -> pd.DataFrame:
|
||
|
|
global Mkt_Info_Last_Refresh_TS_ms
|
||
|
|
|
||
|
|
r = json.loads(requests.get('https://api.starknet.extended.exchange/api/v1/info/markets').text)
|
||
|
|
|
||
|
|
df = pd.DataFrame(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['min_order_size'] = df['tradingConfig'].apply(lambda x: x.get('minOrderSize',{}))
|
||
|
|
df['min_price_change'] = df['tradingConfig'].apply(lambda x: x.get('minPriceChange',{}))
|
||
|
|
df['max_leverage'] = df['tradingConfig'].apply(lambda x: x.get('maxLeverage',{}))
|
||
|
|
|
||
|
|
Mkt_Info_Last_Refresh_TS_ms = round(datetime.now().timestamp() * 1000)
|
||
|
|
|
||
|
|
print('Extend markets info refreshed successfully')
|
||
|
|
|
||
|
|
return df
|
||
|
|
|
||
|
|
def load_aster_current_fr() -> pd.DataFrame:
|
||
|
|
df = pd.DataFrame(json.loads(VAL_KEY.get('fund_rate_aster_all')))
|
||
|
|
|
||
|
|
df = 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()
|
||
|
|
|
||
|
|
return df
|
||
|
|
|
||
|
|
def load_extend_current_fr(df_mkt_stats: pd.DataFrame) -> pd.DataFrame:
|
||
|
|
df = pd.DataFrame(json.loads(VAL_KEY.get('fund_rate_extended_all')))
|
||
|
|
|
||
|
|
df = 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 = df.merge(df_mkt_stats[['name','assetName','status', 'funding_rate_ts','max_leverage']].rename({'name':'symbol','funding_rate_ts':'next_funding_ts'}, axis=1), on='symbol', how='left')
|
||
|
|
df = df.loc[df['status']=='ACTIVE',:]
|
||
|
|
df['USDT_Symbol'] = df['assetName'] + 'USDT'
|
||
|
|
|
||
|
|
df['time_delta_to_next_funding'] = pd.to_datetime(df['next_funding_ts'], unit='ms') - pd.Timestamp.now()
|
||
|
|
|
||
|
|
return df
|
||
|
|
|
||
|
|
async def loop() -> None:
|
||
|
|
global Mkt_Info_Last_Refresh_TS_ms
|
||
|
|
|
||
|
|
df_extend_mkt_stats = get_extended_markets_info()
|
||
|
|
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 = get_extended_markets_info()
|
||
|
|
|
||
|
|
df_aster_fr = load_aster_current_fr()
|
||
|
|
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(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 = df_comb_fr[['symbol_ext','symbol_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)
|
||
|
|
best_next_funding_pair = {'symbol_aster':df_best_fr_rate['symbol_ast'][0],'symbol_extended':df_best_fr_rate['symbol_ext'][0]}
|
||
|
|
|
||
|
|
VAL_KEY.set(VK_OUT, json.dumps(best_next_funding_pair))
|
||
|
|
print(best_next_funding_pair)
|
||
|
|
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('ORCHESTRATOR SHUTTING DOWN...')
|
||
|
|
except Exception as e:
|
||
|
|
logging.error(traceback.format_exc())
|
||
|
|
logging.critical(f'*** ORCHESTRATOR 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(datetime.now().timestamp()*1000)
|
||
|
|
|
||
|
|
logging.info(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(f"STARTED: {START_TIME}")
|
||
|
|
|
||
|
|
asyncio.run(main())
|