import asyncio import json import logging import math import os import time import traceback from dataclasses import asdict, dataclass from datetime import datetime, timezone from typing import AsyncContextManager from dotenv import load_dotenv import numpy as np import pandas as pd import requests # import talib import valkey from sqlalchemy import text from sqlalchemy.ext.asyncio import create_async_engine import modules.aster_auth as aster_auth import modules.extended_auth as extend_auth ### Database ### EXTEND_CLIENT = None CON: AsyncContextManager | None = None VAL_KEY = None ### Logging ### load_dotenv() LOG_FILEPATH: str = os.getenv("LOGS_PATH") + '/Fund_Rate_Algo.log' ### CONSTANTS ### ASTER_LH_ASSET: str = 'ETH' ASTER_RH_ASSET: str = 'USDT' ASTER_TICKER: str = ASTER_LH_ASSET + ASTER_RH_ASSET EXTEND_LH_ASSET: str = 'ETH' EXTEND_RH_ASSET: str = 'USD' EXTEND_TICKER: str = EXTEND_LH_ASSET + '-' + EXTEND_RH_ASSET TARGET_OPEN_CASH_POSITION: float = 10 # Each side (alpha and hedge) ### GLOBALS ### ASTER_MULT = 150 EXTEND_MULT = 50 MAX_TARGET_NOTIONAL = min([ASTER_MULT, EXTEND_MULT]) * TARGET_OPEN_CASH_POSITION ASTER_MIN_ORDER_QTY = 0.001 EXTEND_MIN_ORDER_QTY = 0.01 ASTER_AVAIL_COLLATERAL = 0 ASTER_NOTIONAL_POSITION = 0 EXTEND_AVAIL_COLLATERAL = 0 EXTEND_NOTIONAL_POSITION = 0 ASTER_OPEN_POSITIONS = [] EXTEND_OPEN_POSITIONS = [] ASTER_OPEN_ORDERS = [] EXTEND_OPEN_ORDERS = [] ### FLAGS ### LIQUIDATE_POS_AND_KILL_ALGO_FLAG: bool = False async def aster_remainder_route(): # Check open orders...cancel replace or new order? # Check collateral to confirm you have enough money to trade # if CR, what should be the new price? has it changed? maybe no action needed? how long has it been working? # if not enough collateral then need to liquidate and kill algo - flip flag # if good to order, then create and post order. ADD to LOCAL OPEN ORDERS LIST pass async def extend_remainder_route(): pass async def run_algo(): try: while True: loop_start = time.time() print('__________Start___________') ASTER_FUND_RATE_DICT = json.loads(VAL_KEY.get('fund_rate_aster')) ASTER_TICKER_DICT = json.loads(VAL_KEY.get('fut_ticker_aster')) # print(f'ASTER FUND RATE: {ASTER_FUND_RATE}') # print(f'ASTER TICKER: {ASTER_TICKER}') EXTENDED_FUND_RATE_DICT = json.loads(VAL_KEY.get('fund_rate_extended')) EXTENDED_TICKER_DICT = json.loads(VAL_KEY.get('fut_ticker_extended')) # print(f'EXTENDED FUND RATE: {EXTENDED_FUND_RATE}') # print(f'EXTENDED TICKER: {EXTENDED_TICKER}') ASTER_FUND_RATE = float(ASTER_FUND_RATE_DICT.get('funding_rate', 0)) EXTEND_FUND_RATE = float(EXTENDED_FUND_RATE_DICT.get('funding_rate', 0)) ASTER_FUND_RATE_TIME = float(ASTER_FUND_RATE_DICT.get('next_funding_time_ts_ms', 0)) EXTEND_FUND_RATE_TIME = float(EXTENDED_FUND_RATE_DICT.get('next_funding_time_ts_ms', 0)) ASTER_PAYOUT_DIRECTION_STR = 'LONG PAYS SHORT' if ASTER_FUND_RATE > 0 else 'SHORT PAYS LONG' EXTEND_PAYOUT_DIRECTION_STR = 'LONG PAYS SHORT' if EXTEND_FUND_RATE > 0 else 'SHORT PAYS LONG' FUNDINGS_AT_SAME_TIME_NEXT_HR = ( (ASTER_FUND_RATE_TIME < 60*60*1000) and (EXTEND_FUND_RATE < 60*60*1000) ) if ( abs(ASTER_FUND_RATE) > abs(EXTEND_FUND_RATE) ) and FUNDINGS_AT_SAME_TIME_NEXT_HR: ALPHA_EXCH = 'ASTER' ALPHA_FUND_RATE = ASTER_FUND_RATE else: ALPHA_EXCH = 'EXTEND' ALPHA_FUND_RATE = EXTEND_FUND_RATE if ALPHA_FUND_RATE < 0: ALPHA_CARRY_SIDE = 'BUY' ALPHA_TGT_NOTIONAL = MAX_TARGET_NOTIONAL else: ALPHA_CARRY_SIDE = 'SELL' ALPHA_TGT_NOTIONAL = MAX_TARGET_NOTIONAL*-1 def calc_next_net_fund_rate(FUNDINGS_AT_SAME_TIME_NEXT_HR: bool) -> float: if FUNDINGS_AT_SAME_TIME_NEXT_HR: return ASTER_FUND_RATE + EXTEND_FUND_RATE else: return EXTEND_FUND_RATE NEXT_NET_FUNDING_RATE = calc_next_net_fund_rate(FUNDINGS_AT_SAME_TIME_NEXT_HR) if ALPHA_EXCH == 'EXTEND': ASTER_TGT_NOTIONAL = ALPHA_TGT_NOTIONAL*-1 EXTEND_TGT_NOTIONAL = ALPHA_TGT_NOTIONAL else: ASTER_TGT_NOTIONAL = ALPHA_TGT_NOTIONAL EXTEND_TGT_NOTIONAL = ALPHA_TGT_NOTIONAL*-1 ASTER_TGT_TAIL = ASTER_TGT_NOTIONAL - ASTER_NOTIONAL_POSITION EXTEND_TGT_TAIL = EXTEND_TGT_NOTIONAL - EXTEND_NOTIONAL_POSITION ASTER_TGT_TAIL_ORDERABLE = abs(ASTER_TGT_TAIL) >= ASTER_MIN_ORDER_QTY EXTEND_TGT_TAIL_ORDERABLE = abs(EXTEND_TGT_TAIL) >= EXTEND_MIN_ORDER_QTY print(f''' {pd.to_datetime(ASTER_FUND_RATE_TIME, unit='ms')} ({(pd.to_datetime(ASTER_FUND_RATE_TIME, unit='ms')-datetime.now()):}) | {pd.to_datetime(EXTEND_FUND_RATE_TIME, unit='ms')} ({(pd.to_datetime(EXTEND_FUND_RATE_TIME, unit='ms')-datetime.now()):}) ASTER: {ASTER_FUND_RATE:.6%} [{ASTER_FUND_RATE*10_000:.2f}bps] [{ASTER_FUND_RATE*1_000_000:.0f}pips] | EXTEND: {EXTEND_FUND_RATE:.6%} [{EXTEND_FUND_RATE*10_000:.2f}bps] [{EXTEND_FUND_RATE*1_000_000:.0f}pips] ASTER: {ASTER_PAYOUT_DIRECTION_STR} | EXTEND: {EXTEND_PAYOUT_DIRECTION_STR} ASTER: [ Available Collateral: {ASTER_AVAIL_COLLATERAL:.4f} ] | EXTEND: [ Available Collateral: {EXTEND_AVAIL_COLLATERAL:.4f} ] ASTER: [ Notional Position $ : {ASTER_NOTIONAL_POSITION:.4f} ] | EXTEND: [ Notional Position $ : {EXTEND_NOTIONAL_POSITION:.4f} ] SAME TIME? : {FUNDINGS_AT_SAME_TIME_NEXT_HR} NET FUNDING : {NEXT_NET_FUNDING_RATE:.6%} [{NEXT_NET_FUNDING_RATE*10_000:.2f}bps] [{NEXT_NET_FUNDING_RATE*1_000_000:.0f}pips] ALPHA SIDE : {ALPHA_EXCH} [{ALPHA_CARRY_SIDE}] TGT NOTIONAL: $ {MAX_TARGET_NOTIONAL} ASTER: {ASTER_NOTIONAL_POSITION:.4f} -> {ASTER_TGT_NOTIONAL:.2f} [ Remain: {ASTER_TGT_TAIL:.4f} ] | EXTEND: {EXTEND_NOTIONAL_POSITION:.4f} -> {EXTEND_TGT_NOTIONAL:.2f} [ Remain: {EXTEND_TGT_TAIL} ] ASTER: {ASTER_TGT_TAIL:.4f} > {ASTER_MIN_ORDER_QTY:.4f} min [ Order: {ASTER_TGT_TAIL_ORDERABLE} ] | EXTEND: {EXTEND_TGT_TAIL:.4f} > {EXTEND_MIN_ORDER_QTY:.4f} min [ Order: {EXTEND_TGT_TAIL_ORDERABLE} ] ''') ### SCAN VALKEY USER FEEDS FOR BALANCE UPDATES ### # or just to begin hit the rest API before ordering and update bals then ### ROUTES ### if ASTER_TGT_TAIL_ORDERABLE: await aster_remainder_route() if EXTEND_TGT_TAIL_ORDERABLE: await extend_remainder_route() print(f'__________ End ___________ (Algo Engine ms: {(time.time() - loop_start)*1000})') time.sleep(5) except KeyboardInterrupt: print('...algo stopped') # await cancel_all_orders(CLIENT=CLIENT) except Exception as e: logging.critical(f'*** ALGO ENGINE CRASHED: {e}') logging.error(traceback.format_exc()) # await cancel_all_orders(CLIENT=CLIENT) ### WALLLET ### async def get_aster_collateral(): global ASTER_AVAIL_COLLATERAL fut_acct_balances = { "url": "/fapi/v3/balance", "method": "GET", "params": {} } r = aster_auth.post_authenticated_url(fut_acct_balances) ASTER_AVAIL_COLLATERAL = float([d for d in r if d.get('asset')==ASTER_RH_ASSET][0].get('availableBalance')) async def get_aster_notional_position(): global ASTER_NOTIONAL_POSITION global ASTER_MULT fut_acct_positionRisk = { "url": "/fapi/v3/positionRisk", "method": "GET", "params": {} } r = aster_auth.post_authenticated_url(fut_acct_positionRisk) d = [d for d in r if d.get('symbol', None) == ASTER_TICKER][0] ASTER_NOTIONAL_POSITION = float(d.get('notional' ,0)) ASTER_MULT = float(d.get('leverage', ASTER_MULT)) async def get_extend_collateral(): global EXTEND_AVAIL_COLLATERAL get_bals = dict(dict(await EXTEND_CLIENT.account.get_balance()).get('data', {})) EXTEND_AVAIL_COLLATERAL = get_bals.get('available_for_trade', 0) if get_bals.get('collateral_name', None)==EXTEND_RH_ASSET else 0 async def get_extend_notional(): global EXTEND_NOTIONAL_POSITION global EXTEND_MULT get_pos = dict(await EXTEND_CLIENT.account.get_positions()).get('data', {}) pos_dict = [d for d in get_pos if d.get('market') == EXTEND_TICKER] if pos_dict: pos_dict = pos_dict[0] EXTEND_NOTIONAL_POSITION = pos_dict.get('value', 0) EXTEND_MULT = pos_dict.get('leverage', EXTEND_MULT) else: EXTEND_NOTIONAL_POSITION = 0 ### EXCHANGE INFO ### async def get_aster_exch_info(): global ASTER_MIN_ORDER_QTY fut_acct_exchangeInfo = { "url": "/fapi/v3/exchangeInfo", "method": "GET", "params": {} } r = aster_auth.post_authenticated_url(fut_acct_exchangeInfo) s = r['symbols'] d = [d for d in s if d.get('symbol', None) == 'ETHUSDT'][0] f = [f for f in d['filters'] if f.get('filterType', None) == 'LOT_SIZE'][0] ASTER_MIN_ORDER_QTY = float(f['minQty']) async def get_extend_exch_info(): global EXTEND_MIN_ORDER_QTY r = await EXTEND_CLIENT.markets_info.get_markets_dict() EXTEND_MIN_ORDER_QTY = float(r['ETH-USD'].trading_config.min_order_size) async def main(): global EXTEND_CLIENT global VAL_KEY global CON _, EXTEND_CLIENT = await extend_auth.create_auth_account_and_trading_client() VAL_KEY = valkey.Valkey(host='localhost', port=6379, db=0, decode_responses=True) engine = create_async_engine('mysql+asyncmy://root:pwd@localhost/fund_rate') async with engine.connect() as CON: # await create_executions_orders_table(CON=CON) await get_aster_collateral() await get_aster_notional_position() await get_extend_collateral() await get_extend_notional() await run_algo() 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())