from x10.utils.http import WrappedApiResponse from x10.perpetual.trading_client.trading_client import PerpetualTradingClient import asyncio import json import logging import math import os import time import traceback from datetime import datetime, timezone from decimal import ROUND_DOWN, ROUND_UP, ROUND_HALF_UP, Decimal from typing import AsyncContextManager from dataclasses import dataclass, asdict from typing import Any import numpy as np import pandas as pd import requests # import talib import valkey from dotenv import load_dotenv from sqlalchemy import text from sqlalchemy.ext.asyncio import create_async_engine from x10.models.order import OrderSide, PlacedOrderModel import modules.utils as utils import modules.aster_auth as aster_auth import modules.extended_auth as extend_auth import modules.structs as structs ### Clients ### EXTEND_CLIENT: PerpetualTradingClient CON: AsyncContextManager VAL_KEY: valkey.Valkey ### Logging ### load_dotenv() LOG_FILEPATH: str = f'{os.getenv(key="LOGS_PATH")}/Fund_Rate_Algo.log' ### Algo Config ### ALGO_CONFIG: structs.Algo_Config MIN_TIME_TO_FUNDING: int ### EXCHANGES ### ASTER: structs.Perpetual_Exchange EXTEND: structs.Perpetual_Exchange ### GLOBALS ### Open_Symbols: list[str] = [] Last_Aster_Fill_Time_Ts: float = 0.00 Just_Rejected_Or_Expired: bool = False ASTER_AVAIL_COLLATERAL = 0 EXTEND_AVAIL_COLLATERAL = 0 ASTER_NOTIONAL_POSITION = 0 EXTEND_NOTIONAL_POSITION = 0 ASTER_NOTIONAL_OBJ: dict | None = None EXTEND_NOTIONAL_OBJ: dict | None = None ASTER_UNREALIZED_PNL = 0 EXTEND_UNREALIZED_PNL = 0 ASTER_OPEN_ORDERS = [] EXTEND_OPEN_ORDERS = [] # ASTER_OPEN_POSITIONS = [] # EXTEND_OPEN_POSITIONS = [] # EXCHANGES: list = [ Aster(), Extend() ] ### FLAGS ### Flags = structs.Flags() ### UTILS ### # def round_decimal_down(value, decimal_places): # # Construct precision string like '0.01' for 2 places # fmt = f'0.{"0" * decimal_places}' if decimal_places > 0 else '0' # precision = Decimal(fmt) # return Decimal(str(value)).quantize(precision, rounding=ROUND_HALF_UP) ### OPEN ORDERS ### async def get_aster_open_orders(): global ASTER_OPEN_ORDERS fut_acct_openOrders = { "url": "/fapi/v3/openOrders", "method": "GET", "params": {} } ASTER_OPEN_ORDERS = await aster_auth.post_authenticated_url(fut_acct_openOrders) async def get_extend_open_orders(): global EXTEND_OPEN_ORDERS EXTEND_OPEN_ORDERS = list(dict(await EXTEND_CLIENT.account.get_open_orders()).get('data', 0)) ### WALLLET ### # async def get_aster_collateral(): # global ASTER_AVAIL_COLLATERAL # fut_acct_balances = { # "url": "/fapi/v3/balance", # "method": "GET", # "params": {} # } # r = await 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_account_open_symbols() -> list[str]: fut_acct_positionRisk: dict = { "url": "/fapi/v3/positionRisk", "method": "GET", "params": { 'symbol':'' } } try: resp: list = await aster_auth.post_authenticated_url(req=fut_acct_positionRisk) # ty:ignore[invalid-assignment] except Exception as e: logging.critical(f'JSONDecodeError trying to get Aster open orders: {e}; resp: {resp}') await kill_algo() resp: list = [] ld = [ utils.symbol_to_extend_fmt(x['symbol']) for x in resp if abs(float(x.get('positionAmt', 0))) > 0] return ld async def get_aster_notional_position(resp: list | None = None): global ASTER_NOTIONAL_OBJ global ASTER_NOTIONAL_POSITION global ASTER_UNREALIZED_PNL global ASTER previous_notional_obj = ASTER_NOTIONAL_OBJ previous_notional_position = ASTER_NOTIONAL_POSITION if resp: d = [x for x in resp if x.get('symbol', None) == ASTER.symbol] d = d[0] if d else {} if ( not resp ) or ( not d ): fut_acct_positionRisk: dict = { "url": "/fapi/v3/positionRisk", "method": "GET", "params": { 'symbol': ASTER.symbol, } } try: resp: list = await aster_auth.post_authenticated_url(req=fut_acct_positionRisk) # ty:ignore[invalid-assignment] except Exception as e: logging.critical(f'JSONDecodeError trying to get Aster notional: {e}; resp: {resp}') await kill_algo() resp: list = [] d = [x for x in resp if x.get('symbol', None) == ASTER.symbol][0] d['timestamp_arrival'] = round(datetime.now().timestamp()*1000) if previous_notional_obj is not None: if previous_notional_obj['timestamp_arrival'] > d['timestamp_arrival']: # logging.info(f'ASTER NOTIONAL: prev timestamp ({pd.to_datetime(previous_notional_obj['timestamp_arrival'], unit='ms')}) > new timestamp ({pd.to_datetime(d['timestamp_arrival'], unit='ms')}); skipping') return ASTER_NOTIONAL_OBJ = d if len(d) < 1: logging.info(f'BAD NOTIONAL - ASTER CHANGE: Empty d: {d}; resp: {resp}') await kill_algo() ASTER_UNREALIZED_PNL = float(d['unrealized_pnl']) if d.get('unrealized_pnl') is not None else float(d['unRealizedProfit']) if d.get('notional') is not None: ASTER_NOTIONAL_POSITION = float(d['notional']) - ASTER_UNREALIZED_PNL else: ASTER_NOTIONAL_POSITION = float(d['position_amount'])*float(d['entry_price']) if d.get('leverage') is not None: ASTER.mult = int(d['leverage']) if abs(ASTER_NOTIONAL_POSITION) > ALGO_CONFIG.Config.Max_Target_Notional*ALGO_CONFIG.Config.Max_Order_Over_Notional_Ratio: logging.info(f'BAD NOTIONAL - ASTER CHANGE: {previous_notional_position} -> {ASTER_NOTIONAL_POSITION}; UR PNL: {ASTER_UNREALIZED_PNL}; MULT: {ASTER.mult}; d: {d}; resp: {resp}; max_tgt_notional: {ALGO_CONFIG.Config.Max_Target_Notional}') await kill_algo() if ASTER_NOTIONAL_POSITION != previous_notional_position: logging.info(f'ASTER NOTIONAL CHANGE: {previous_notional_position:.2f} -> {ASTER_NOTIONAL_POSITION:.2f}; UR PNL: {ASTER_UNREALIZED_PNL:.2f}; MULT: {ASTER.mult:.0f}; resp: {bool(resp)}') # 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_account_open_symbols() -> list[str]: resp = dict(await EXTEND_CLIENT.account.get_positions()).get('data', []) ld = [x.market for x in list(resp) if abs(float(x.size)) > 0] return ld async def set_comb_open_symbols() -> None: global Open_Symbols open_aster_symbols = await get_aster_account_open_symbols() open_extend_symbols = await get_extend_account_open_symbols() Open_Symbols = list(set(open_aster_symbols + open_extend_symbols)) async def get_extend_notional(resp: list | None = None): global EXTEND_NOTIONAL_OBJ global EXTEND_NOTIONAL_POSITION global EXTEND_UNREALIZED_PNL global EXTEND previous_notional_obj = EXTEND_NOTIONAL_OBJ previous_notional_position = EXTEND_NOTIONAL_POSITION if not resp: resp = dict(await EXTEND_CLIENT.account.get_positions()).get('data', []) pos_dict = [dict(d) for d in resp if dict(d).get('market') == EXTEND.symbol] if pos_dict: pos_dict = pos_dict[0] pos_dict['timestamp_arrival'] = round(datetime.now().timestamp()*1000) else: pos_dict = {} pos_dict['side'] = 'LONG' pos_dict['value'] = 0.00 pos_dict['unrealised_pnl'] = 0.00 pos_dict['timestamp_arrival'] = round(datetime.now().timestamp()*1000) logging.info('get_extend_notional - No Positions') else: pos_dict = [dict(d) for d in resp if dict(d).get('market') == EXTEND.symbol] if pos_dict: pos_dict = pos_dict[0] else: pos_dict = {} pos_dict['side'] = 'LONG' pos_dict['value'] = 0.00 pos_dict['unrealised_pnl'] = 0.00 pos_dict['timestamp_arrival'] = round(datetime.now().timestamp()*1000) # logging.info('get_extend_notional - No Positions') # pos_dict['timestamp_arrival'] = round(datetime.now().timestamp()*1000) if previous_notional_obj is not None: if previous_notional_obj['timestamp_arrival'] > pos_dict['timestamp_arrival']: # logging.info(f'EXTEND NOTIONAL: prev timestamp ({pd.to_datetime(previous_notional_obj['timestamp_arrival'], unit='ms')}) > new timestamp ({pd.to_datetime(pos_dict['timestamp_arrival'], unit='ms')}); skipping') return else: previous_notional_obj = {} EXTEND_NOTIONAL_OBJ = pos_dict EXTEND_UNREALIZED_PNL = pos_dict.get('unrealised_pnl', 0) position_side = pos_dict['side'] # LONG or SHORT notional_pos_abs = abs(float(pos_dict['value'])) if position_side == 'LONG': notional_pos_sided = notional_pos_abs elif position_side == 'SHORT': notional_pos_sided = notional_pos_abs * -1 else: logging.info(f'EXTEND BAD SIDE ON POSITION UPDATE: {pos_dict}') EXTEND_NOTIONAL_POSITION = notional_pos_sided - float(EXTEND_UNREALIZED_PNL) EXTEND.mult = pos_dict.get('leverage', EXTEND.mult) if abs(EXTEND_NOTIONAL_POSITION) > ALGO_CONFIG.Config.Max_Target_Notional*ALGO_CONFIG.Config.Max_Order_Over_Notional_Ratio: logging.info(f'BAD NOTIONAL - EXTEND CHANGE: {previous_notional_position} -> {EXTEND_NOTIONAL_POSITION}; UR PNL: {EXTEND_UNREALIZED_PNL}; MULT: {EXTEND.mult}; d: {pos_dict}; resp: {resp}') await kill_algo() if EXTEND_NOTIONAL_POSITION != previous_notional_position: logging.info(f'EXTEND NOTIONAL CHANGE: {previous_notional_position} [{previous_notional_obj.get('timestamp_arrival')}] -> {EXTEND_NOTIONAL_POSITION:.2f} [{EXTEND_NOTIONAL_OBJ['timestamp_arrival']}]; UR PNL: {EXTEND_UNREALIZED_PNL:.2f}; MULT: {EXTEND.mult}; resp: {bool(resp)}') ### EXCHANGE INFO ### async def get_aster_exch_info(symbol_override: str | None = None): global ASTER if symbol_override: ASTER.symbol = utils.symbol_to_aster_fmt(symbol_override) 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] s: list = r['symbols'] d: dict = [d for d in s if d.get('symbol', None) == ASTER.symbol][0] f: dict = [f for f in d['filters'] if f.get('filterType', None) == 'LOT_SIZE'][0] q: dict = [f for f in d['filters'] if f.get('filterType', None) == 'PRICE_FILTER'][0] n: dict = [f for f in d['filters'] if f.get('filterType', None) == 'MIN_NOTIONAL'][0] min_qty = float(f['minQty']) min_qty = int(min_qty) if min_qty == int(min_qty) else min_qty min_price = float(q['minPrice']) min_price = int(min_price) if min_price == int(min_price) else min_price ASTER.min_order_size = min_qty ASTER.min_price = min_price ASTER.min_notional = float(n['notional']) async def get_extend_exch_info(symbol_override: str | None = None): global EXTEND if symbol_override: EXTEND.symbol = utils.symbol_to_extend_fmt(symbol_override) r = await EXTEND_CLIENT.markets_info.get_markets_dict() EXTEND.min_order_size = float(r[EXTEND.symbol].trading_config.min_order_size) EXTEND.min_price = float(r[EXTEND.symbol].trading_config.min_price_change) ### CANCEL ORDERS ### async def aster_cancel_all_orders(): cancel_all_open_orders = { "url": "/fapi/v3/allOpenOrders", "method": "DELETE", "params": { 'symbol': ASTER.symbol, } } r = await aster_auth.post_authenticated_url(cancel_all_open_orders) logging.info(f'ASTER CANCEL ALL OPEN ORDERS RESP: {r}') async def extend_cancel_all_orders(): r = await EXTEND_CLIENT.orders.mass_cancel(markets=[EXTEND.symbol]) logging.info(f'EXTEND CANCEL ALL OPEN ORDERS RESP: {r}') ### KILL ALGO ### async def kill_algo(): await aster_cancel_all_orders() await extend_cancel_all_orders() logging.info('ALGO KILL FLAG ACTIVATED; CANCELLING OPEN ORDERS AND SHUTTING DOWN') raise ValueError('KILL FLAG ACTIVATED') ### ALGO LOOP ### async def run_algo(): global ASTER global EXTEND global ALGO_CONFIG global MIN_TIME_TO_FUNDING global ASTER_OPEN_ORDERS global EXTEND_OPEN_ORDERS global Last_Aster_Fill_Time_Ts global Just_Rejected_Or_Expired # global Best_Symbol_by_Exchange try: while True: loop_start = time.time() # print('__________Start___________') ### ALGO CONIFG ### ALGO_CONFIG = json.loads(VAL_KEY.get('fr_orchestrator_output')) # ty:ignore[invalid-argument-type] ALGO_CONFIG = structs.Algo_Config(**ALGO_CONFIG) ALGO_CONFIG.Config.Max_Target_Notional = float(min([ASTER.mult, EXTEND.mult]) * ALGO_CONFIG.Config.Target_Open_Cash_Position) MIN_TIME_TO_FUNDING = ALGO_CONFIG.Config.Min_Time_To_Funding_Minutes * 60 * 1000 ### Load Data from Feedhandlers ### best_symbol_by_exchange: dict = json.loads(s=VAL_KEY.get(name='fr_engine_best_fund_rate_output')) # ty:ignore[invalid-argument-type] best_symbol_by_exchange_aster = structs.Perpetual_Exchange(**best_symbol_by_exchange['ASTER']) best_symbol_by_exchange_extend = structs.Perpetual_Exchange(**best_symbol_by_exchange['EXTEND']) ASTER_FUND_RATE_DICT: Any = VAL_KEY.get('fund_rate_aster') ASTER_FUND_RATE_DICT: dict = json.loads(s=ASTER_FUND_RATE_DICT) if ASTER_FUND_RATE_DICT is not None else {} if ASTER_FUND_RATE_DICT.get('symbol', None) != ASTER.symbol: ASTER_FUND_RATE: float = ASTER.initial_funding_rate # logging.info(f'ASTER Symbol mismatch: {ASTER_FUND_RATE_DICT}; expected symbol: {ASTER.symbol}') # raise ValueError(f'ASTER Symbol mismatch: {ASTER_FUND_RATE_DICT}; expected symbol: {ASTER.symbol}') else: ASTER_FUND_RATE: float = float(ASTER_FUND_RATE_DICT.get('funding_rate', 0)) EXTENDED_FUND_RATE_DICT: Any = VAL_KEY.get('fund_rate_extended') EXTENDED_FUND_RATE_DICT: dict = json.loads(s=EXTENDED_FUND_RATE_DICT) if EXTENDED_FUND_RATE_DICT is not None else {} if EXTENDED_FUND_RATE_DICT.get('symbol', None) != EXTEND.symbol: EXTEND_FUND_RATE: float = EXTEND.initial_funding_rate # logging.info(f'ASTER Symbol mismatch: {EXTENDED_FUND_RATE_DICT}; expected symbol: {EXTEND.symbol}') # raise ValueError(f'ASTER Symbol mismatch: {EXTENDED_FUND_RATE_DICT}; expected symbol: {EXTEND.symbol}') else: EXTEND_FUND_RATE: float = float(EXTENDED_FUND_RATE_DICT.get('funding_rate', 0)) if ALGO_CONFIG.Overrides.Flip_Side_For_Testing: ASTER_FUND_RATE = ASTER_FUND_RATE * -1 EXTEND_FUND_RATE = EXTEND_FUND_RATE * -1 ASTER_FUND_RATE_TIME = float(ASTER_FUND_RATE_DICT.get('next_funding_time_ts_ms', 0)) ASTER_FUND_RATE_TIME = ASTER_FUND_RATE_TIME+(60*60*1000) if ASTER_FUND_RATE_TIME < (datetime.now().timestamp()*1000) else ASTER_FUND_RATE_TIME EXTEND_FUND_RATE_TIME = max([float(EXTENDED_FUND_RATE_DICT.get('next_funding_time_ts_ms', 0)), 0]) EXTEND_FUND_RATE_TIME = EXTEND_FUND_RATE_TIME+(60*60*1000) if EXTEND_FUND_RATE_TIME < (datetime.now().timestamp()*1000) else EXTEND_FUND_RATE_TIME ASTER_TICKER_DICT: Any = VAL_KEY.get('fut_ticker_aster') ASTER_TICKER_DICT: dict = json.loads(s=ASTER_TICKER_DICT) if ASTER_TICKER_DICT is not None else {} if ( ASTER_TICKER_DICT.get('symbol', None) != ASTER.symbol ) and not(ALGO_CONFIG.Overrides.Flatten_Open_Positions): logging.warning(f'ASTER Symbol mismatch: {ASTER_TICKER_DICT}; expected symbol: {ASTER.symbol}') VAL_KEY.set(name='fr_algo_working_symbol', value=json.dumps(obj={'ASTER': asdict(obj=ASTER), 'EXTEND': asdict(obj=EXTEND)})) time.sleep(5) continue # raise ValueError(f'ASTER Symbol mismatch: {ASTER_TICKER_DICT}; expected symbol: {ASTER.symbol}') EXTENDED_TICKER_DICT: Any = VAL_KEY.get('fut_ticker_extended') EXTENDED_TICKER_DICT: dict = json.loads(s=EXTENDED_TICKER_DICT) if EXTENDED_TICKER_DICT is not None else {} if ( EXTENDED_TICKER_DICT.get('symbol', None) != EXTEND.symbol) and not(ALGO_CONFIG.Overrides.Flatten_Open_Positions): logging.warning(f'EXTEND Symbol mismatch: {EXTENDED_TICKER_DICT}; expected symbol: {EXTEND.symbol}') VAL_KEY.set(name='fr_algo_working_symbol', value=json.dumps(obj={'ASTER': asdict(obj=ASTER), 'EXTEND': asdict(obj=EXTEND)})) time.sleep(5) continue # raise ValueError(f'EXTEND Symbol mismatch: {EXTENDED_TICKER_DICT}; expected symbol: {EXTEND.symbol}') ### Manage Local Collateral Using Updates from WS ### # ASTER_WS_COLLATERAL_UPDATES = VAL_KEY.get('fr_aster_user_positions') # ASTER_WS_COLLATERAL_UPDATES = json.loads(ASTER_WS_COLLATERAL_UPDATES) if ASTER_WS_COLLATERAL_UPDATES is not None else [] # ty:ignore[invalid-argument-type] # EXTEND_WS_COLLATERAL_UPDATES = VAL_KEY.get('fr_extended_user_positions') # EXTEND_WS_COLLATERAL_UPDATES = json.loads(EXTEND_WS_COLLATERAL_UPDATES) if EXTEND_WS_COLLATERAL_UPDATES is not None else [] # ty:ignore[invalid-argument-type] ### Manage Local Notionals Using Updates from WS ### ASTER_WS_POS_UPDATES: Any = VAL_KEY.get(name='fr_aster_user_positions') ASTER_WS_POS_UPDATES: list = json.loads(s=ASTER_WS_POS_UPDATES) if ASTER_WS_POS_UPDATES is not None else [] EXTEND_WS_POS_UPDATES: Any = VAL_KEY.get('fr_extended_user_positions') EXTEND_WS_POS_UPDATES: list = json.loads(EXTEND_WS_POS_UPDATES) if EXTEND_WS_POS_UPDATES is not None else [] ### Manage Local Orders Using Updates from WS ### ASTER_WS_ORDER_UPDATES: Any = VAL_KEY.get('fr_aster_user_orders') ASTER_WS_ORDER_UPDATES: list = json.loads(ASTER_WS_ORDER_UPDATES) if ASTER_WS_ORDER_UPDATES is not None else [] EXTEND_WS_ORDER_UPDATES: Any = VAL_KEY.get('fr_extended_user_orders') EXTEND_WS_ORDER_UPDATES: list = json.loads(EXTEND_WS_ORDER_UPDATES) if EXTEND_WS_ORDER_UPDATES is not None else [] # CHECK NO MORE THAN 1 OPEN ORDER ON EITHER EXCHANGE # if len(ASTER_OPEN_ORDERS) > 1 or len(EXTEND_OPEN_ORDERS) > 1: logging.info(f'MORE THAN 1 ORDER OPEN - KILLING ALGO: ASTER_OPEN_ORDERS ({len(ASTER_OPEN_ORDERS)}): {ASTER_OPEN_ORDERS}; EXTEND_OPEN_ORDERS ({len(EXTEND_OPEN_ORDERS)}): {EXTEND_OPEN_ORDERS}') await kill_algo() raise ValueError('NOT HERE: MORE THAN 1 ORDER OPEN - KILLING ALGO: ASTER_OPEN_ORDERS') ### CHECK TIME TO FUNDING AND WHETHER TO BE ACTIVE ### now_ms = round(datetime.now().timestamp()*1000) time_to_funding_ms = min([ASTER_FUND_RATE_TIME, EXTEND_FUND_RATE_TIME]) - now_ms if ( time_to_funding_ms > MIN_TIME_TO_FUNDING ) and (not ASTER_OPEN_ORDERS) and (not EXTEND_OPEN_ORDERS): logging.info(f'Outside action window (minutes) and no active order (sleeping for 5 sec): {pd.to_datetime(time_to_funding_ms, unit='ms').minute} > {pd.to_datetime(MIN_TIME_TO_FUNDING, unit='ms').minute}') time.sleep(5) continue if len(ASTER_WS_POS_UPDATES) > 0: # await get_aster_notional_position() await get_aster_notional_position(resp=ASTER_WS_POS_UPDATES) ###### *** returned 0 notional even though had a position, need to handle and safety check to not order above max notional. ##### NEED TO UPDATE SO IT TAKES THE LATEST MSG, ie drop the WS msg if its older than the exisiting one from the API. if len(EXTEND_WS_POS_UPDATES) > 0: await get_extend_notional(resp=EXTEND_WS_POS_UPDATES) # await get_extend_notional() # ************** NOT USING WEBSOCKET FEED DUE TO ISSUES WITH IT OVERWRITING API DATA ie the WS just statically shows last update and doesnt pull new when you start the algo. ### Also WS was just stale and caused issues where it sees a fill then gets new API Collateral (correct) and then the next loop would be the old incorrect collateral in the WS, causing bad orders. Do not have issue on ASTER. if ASTER_WS_ORDER_UPDATES is not None: for idx, o in enumerate(ASTER_OPEN_ORDERS): order_id = o.get('order_id') if o.get('order_id') is not None else o['orderId'] order_orig_status = o.get('status') if o.get('status') is not None else o['order_status'] order_update = [ou for ou in ASTER_WS_ORDER_UPDATES if ou.get('order_id', None) == order_id] if len(order_update) > 0: order_update = order_update[0] order_update_status = order_update.get('status') if order_update.get('status') is not None else order_update.get('order_status') order_status_changed = order_orig_status.upper() != order_update_status.upper() if order_status_changed: logging.info(f'ASTER ORDER ({order_id}): {order_orig_status} -> {order_update_status}') ASTER_OPEN_ORDERS[idx] = order_update if order_update_status in ['CANCELED','EXPIRED']: logging.info(f'ASTER ORDER CANCELLED or EXPIRED: {order_id}') ASTER_OPEN_ORDERS.pop(idx) Just_Rejected_Or_Expired = True utils.send_tg_alert(f'FR_ALGO - ASTER REJECTED ({order_id})') elif order_update_status in ['PARTIALLY_FILLED']: logging.info(f'ASTER ORDER PARTIALLY FILLED: {order_id}') # await get_aster_collateral() await get_aster_notional_position(resp=ASTER_WS_POS_UPDATES) Last_Aster_Fill_Time_Ts = datetime.now().timestamp()*1000 utils.send_tg_alert(f'FR_ALGO - ASTER PARTIALLY FILLED ({order_id})') elif order_update_status in ['FILLED']: logging.info(f'ASTER ORDER FILLED: {order_id}') ASTER_OPEN_ORDERS.pop(idx) # await get_aster_collateral() await get_aster_notional_position(resp=ASTER_WS_POS_UPDATES) Last_Aster_Fill_Time_Ts = datetime.now().timestamp()*1000 utils.send_tg_alert(f'FR_ALGO - ASTER FILLED ({order_id})') else: logging.critical(f'EXTEND ORDER STATUS CHG TO UNEXPECTED VALUE, KILLING... ({order_id}): {order_orig_status} -> {order_update_status}') if EXTEND_WS_ORDER_UPDATES is not None: for idx, o in enumerate(EXTEND_OPEN_ORDERS): o = dict(o) order_id = o.get('order_id') if o.get('order_id') is not None else o.get('id') order_orig_status = o['status'] order_update = [dict(ou) for ou in EXTEND_WS_ORDER_UPDATES if dict(ou).get('order_id', None) == order_id] if len(order_update) > 0: order_update: dict = order_update[0] order_update_status: str = order_update['status'] order_status_changed: bool = order_orig_status.upper() != order_update_status.upper() if order_status_changed: logging.info(f'EXTEND ORDER ({order_id}): {order_orig_status} -> {order_update_status}') EXTEND_OPEN_ORDERS[idx] = order_update if order_update_status in ['CANCELLED','EXPIRED','REJECTED']: logging.info(f'EXTEND ORDER CANCELLED, REJECTED or EXPIRED: {order_id}') EXTEND_OPEN_ORDERS.pop(idx) Just_Rejected_Or_Expired = True utils.send_tg_alert(f'FR_ALGO - EXTEND REJECTED ({order_id})') elif order_update_status in ['PARTIALLY_FILLED']: logging.info(f'EXTEND ORDER PARTIALLY FILLED: {order_id}') # await get_extend_collateral() await get_extend_notional() utils.send_tg_alert(f'FR_ALGO - EXTEND PARTIALLY FILLED ({order_id})') elif order_update_status in ['FILLED']: logging.info(f'EXTEND ORDER FILLED: {order_id}') EXTEND_OPEN_ORDERS.pop(idx) # await get_extend_collateral() await get_extend_notional() utils.send_tg_alert(f'FR_ALGO - EXTEND FILLED ({order_id})') else: logging.critical(f'EXTEND ORDER STATUS CHG TO UNEXPECTED VALUE, KILLING... ({order_id}): {order_orig_status} -> {order_update_status}') if ALGO_CONFIG.Overrides.Allow_Symbol_Change: if (best_symbol_by_exchange_aster.symbol != ASTER.symbol) or (best_symbol_by_exchange_extend.symbol != EXTEND.symbol): if abs( ASTER_NOTIONAL_POSITION ) > 0.00 or abs( EXTEND_NOTIONAL_POSITION ) > 0.00: if ALGO_CONFIG.Logging.Print_Summary_Each_Loop: print(f'Symbol switch [{ASTER.symbol} > {best_symbol_by_exchange_aster.symbol}] - Flattening Positions') ALGO_CONFIG.Overrides.Flatten_Open_Positions_Opportunistic = True else: logging.info('Balances Flattened - Updating to Trade New Symbols:') logging.info(f' ASTER.symbol -> {best_symbol_by_exchange_aster.symbol}') logging.info(f' EXTEND.symbol -> {best_symbol_by_exchange_extend.symbol}') ALGO_CONFIG.Overrides.Flatten_Open_Positions_Opportunistic = False if Open_Symbols: logging.info(f'OPEN SYMBOLS TO CLOSE: {Open_Symbols}') master_data = json.loads(s=VAL_KEY.get(name='fr_engine_best_fund_rate_master')) # ty:ignore[invalid-argument-type] open_symbol_to_work = Open_Symbols[0] current_pos_master_ast = [d for d in master_data if d.get('symbol_ext') == open_symbol_to_work][0] ASTER = structs.Perpetual_Exchange( mult = int(current_pos_master_ast['max_leverage_ast']), lh_asset = current_pos_master_ast['lh_asset_ast'], rh_asset = current_pos_master_ast['rh_asset_ast'], symbol_asset_separator = '', initial_funding_rate=float(current_pos_master_ast['funding_rate_ast']), min_price=float(current_pos_master_ast['min_price_ast']), min_order_size=float(current_pos_master_ast['min_order_size_ast']), min_lot_size=float(current_pos_master_ast['min_lot_size_ast']), min_notional=float(current_pos_master_ast['min_notional_ast']), ) EXTEND = structs.Perpetual_Exchange( mult = int(current_pos_master_ast['max_leverage_ext']), lh_asset = current_pos_master_ast['lh_asset_ext'], rh_asset = current_pos_master_ast['rh_asset_ext'], symbol_asset_separator = '-', initial_funding_rate=float(current_pos_master_ast['funding_rate_ext']), min_price=float(current_pos_master_ast['min_price_ext']), min_order_size=float(current_pos_master_ast['min_order_size_ext']), min_lot_size=float(current_pos_master_ast['min_lot_size_ext']), min_notional=float(current_pos_master_ast['min_notional_ext']), ) Open_Symbols.pop(0) await get_aster_notional_position() await get_extend_notional() else: ASTER = best_symbol_by_exchange_aster EXTEND = best_symbol_by_exchange_extend VAL_KEY.set(name='fr_algo_working_symbol', value=json.dumps(obj={'ASTER': asdict(obj=ASTER), 'EXTEND': asdict(obj=EXTEND)})) min_between_fundings = round((abs(ASTER_FUND_RATE_TIME - EXTEND_FUND_RATE_TIME) / 1000 / 60)) FUNDINGS_AT_SAME_TIME_NEXT_HR = min_between_fundings < 5 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 = ALGO_CONFIG.Config.Max_Target_Notional else: ALPHA_CARRY_SIDE = 'SELL' ALPHA_TGT_NOTIONAL = ALGO_CONFIG.Config.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 max([ASTER_FUND_RATE, EXTEND_FUND_RATE]) - min([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) Flags.NET_FUNDING_IS_ZERO = ( NEXT_NET_FUNDING_RATE >= ( (ALGO_CONFIG.Config.Min_Fund_Rate_Pct_To_Trade*-1) / 100) ) and ( NEXT_NET_FUNDING_RATE <= ( ALGO_CONFIG.Config.Min_Fund_Rate_Pct_To_Trade / 100 ) ) if Flags.NET_FUNDING_IS_ZERO or ALGO_CONFIG.Overrides.Flatten_Open_Positions or ALGO_CONFIG.Overrides.Flatten_Open_Positions_Opportunistic: ALPHA_TGT_NOTIONAL = 0.00 # if ASTER_OPEN_ORDERS or EXTEND_OPEN_ORDERS: # logging.info('NET FUNDING = 0.00; Cancelling Open Orders! then Waiting...') # aster_cancel_all_orders() # extend_cancel_all_orders() # time.sleep(5) # else: # logging.info('NET FUNDING = 0.00; NO OPEN ORDERS; Waiting...') # time.sleep(5) if ALGO_CONFIG.Overrides.Flatten_Open_Positions or ALGO_CONFIG.Overrides.Flatten_Open_Positions_Opportunistic or ALPHA_TGT_NOTIONAL==0.00: # ROUNDING = ROUND_UP ROUNDING = ROUND_HALF_UP else: ROUNDING = ROUND_DOWN if ALPHA_EXCH == 'EXTEND': ASTER_TGT_NOTIONAL = ALPHA_TGT_NOTIONAL*-1 EXTEND_TGT_NOTIONAL = ALPHA_TGT_NOTIONAL if ALPHA_CARRY_SIDE == 'BUY': ASTER_TOB_PX = float(ASTER_TICKER_DICT['best_ask_px']) EXTEND_TOB_PX = float(EXTENDED_TICKER_DICT['best_bid_px']) Alpha_Nominator = ASTER_TOB_PX Alpha_Denominator = EXTEND_TOB_PX ALPHA_RATIO = ASTER_TOB_PX / EXTEND_TOB_PX Expected_Alpha = ( ( ASTER_TOB_PX - EXTEND_TOB_PX ) / (( ASTER_TOB_PX + EXTEND_TOB_PX ) / 2) ) else: ASTER_TOB_PX = float(ASTER_TICKER_DICT['best_bid_px']) EXTEND_TOB_PX = float(EXTENDED_TICKER_DICT['best_ask_px']) Alpha_Nominator = EXTEND_TOB_PX Alpha_Denominator = ASTER_TOB_PX ALPHA_RATIO = EXTEND_TOB_PX / ASTER_TOB_PX Expected_Alpha = ( ( EXTEND_TOB_PX - ASTER_TOB_PX ) / (( ASTER_TOB_PX + EXTEND_TOB_PX ) / 2) ) else: ASTER_TGT_NOTIONAL = ALPHA_TGT_NOTIONAL EXTEND_TGT_NOTIONAL = ALPHA_TGT_NOTIONAL*-1 if ALPHA_CARRY_SIDE == 'BUY': ASTER_TOB_PX = float(ASTER_TICKER_DICT['best_bid_px']) EXTEND_TOB_PX = float(EXTENDED_TICKER_DICT['best_ask_px']) Alpha_Nominator = EXTEND_TOB_PX Alpha_Denominator = ASTER_TOB_PX ALPHA_RATIO = EXTEND_TOB_PX / ASTER_TOB_PX Expected_Alpha = ( ( EXTEND_TOB_PX - ASTER_TOB_PX ) / (( EXTEND_TOB_PX + ASTER_TOB_PX ) / 2) ) else: ASTER_TOB_PX = float(ASTER_TICKER_DICT['best_ask_px']) EXTEND_TOB_PX = float(EXTENDED_TICKER_DICT['best_bid_px']) Alpha_Nominator = ASTER_TOB_PX Alpha_Denominator = EXTEND_TOB_PX ALPHA_RATIO = ASTER_TOB_PX / EXTEND_TOB_PX Expected_Alpha = ( ( ASTER_TOB_PX - EXTEND_TOB_PX ) / (( ASTER_TOB_PX + EXTEND_TOB_PX ) / 2) ) Expected_Alpha_Net_FR = abs(NEXT_NET_FUNDING_RATE) + Expected_Alpha Expected_Alpha_Net_FR_w_Taker = Expected_Alpha_Net_FR-0.00025 Expected_Alpha_w_Taker = Expected_Alpha-0.00025 EXTEND_TGT_NOTIONAL = ASTER_NOTIONAL_POSITION * -1 ASTER_TGT_TAIL = ASTER_TGT_NOTIONAL - ( float(ASTER_NOTIONAL_POSITION) + float(ASTER_UNREALIZED_PNL) ) # EXTEND_TGT_TAIL = EXTEND_TGT_NOTIONAL - ( float(EXTEND_NOTIONAL_POSITION) + float(EXTEND_UNREALIZED_PNL) ) EXTEND_TGT_TAIL = EXTEND_TGT_NOTIONAL - ( float(EXTEND_NOTIONAL_POSITION) ) # EXTEND_TGT_TAIL = float(ASTER_NOTIONAL_POSITION)*-1 min_order_size = ASTER.min_order_size min_order_size = int(min_order_size) if min_order_size == int(min_order_size) else min_order_size ASTER_TGT_TAIL_BASE_QTY = Decimal(str(float(ASTER_TGT_TAIL) / float(ASTER_TOB_PX))).quantize(Decimal(str(min_order_size)), rounding=ROUNDING) if ASTER.min_lot_size: ASTER_TGT_TAIL_BASE_QTY = float(ASTER_TGT_TAIL_BASE_QTY) - ( float(ASTER_TGT_TAIL_BASE_QTY) % ASTER.min_lot_size ) ASTER_TGT_TAIL_BASE_QTY = Decimal(str(ASTER_TGT_TAIL_BASE_QTY)).quantize(Decimal(str(min_order_size)), rounding=ROUNDING) min_order_size = EXTEND.min_order_size min_order_size = int(min_order_size) if min_order_size == int(min_order_size) else min_order_size EXTEND_TGT_TAIL_BASE_QTY = Decimal(str(float(EXTEND_TGT_TAIL) / float(EXTEND_TOB_PX))).quantize(Decimal(str(min_order_size)), rounding=ROUNDING) if EXTEND.min_lot_size: EXTEND_TGT_TAIL_BASE_QTY = float(EXTEND_TGT_TAIL_BASE_QTY) - ( float(EXTEND_TGT_TAIL_BASE_QTY) % EXTEND.min_lot_size ) EXTEND_TGT_TAIL_BASE_QTY = Decimal(str(EXTEND_TGT_TAIL_BASE_QTY)).quantize(Decimal(str(min_order_size)), rounding=ROUNDING) # MAX_MIN_ORDER_QTY = max([ASTER.min_order_size, EXTEND.min_order_size]) ASTER_TGT_TAIL_ORDERABLE = ( Decimal(str(abs(ASTER_TGT_TAIL_BASE_QTY)) ) >= Decimal(str(abs(ASTER.min_order_size))) ) and ( Decimal(str(abs(ASTER_TGT_TAIL))) > Decimal(str(abs(ASTER.min_notional))) ) EXTEND_TGT_TAIL_ORDERABLE = ( Decimal(str(abs(EXTEND_TGT_TAIL_BASE_QTY))) >= Decimal(str(abs(EXTEND.min_order_size))) ) and ( Decimal(str(abs(EXTEND_TGT_TAIL))) > Decimal(str(abs(EXTEND.min_notional))) ) if not ASTER_TGT_TAIL_ORDERABLE: if abs(ASTER_TGT_TAIL_BASE_QTY) > 0: if ALGO_CONFIG.Overrides.Flatten_Open_Positions or ALGO_CONFIG.Overrides.Flatten_Open_Positions_Opportunistic or ALPHA_TGT_NOTIONAL == 0.00: logging.info('* Trying to flatten small Aster balance, was originally not orderable.') ASTER_TGT_TAIL_ORDERABLE = True if not EXTEND_TGT_TAIL_ORDERABLE: if abs(EXTEND_TGT_TAIL_BASE_QTY) > 0: if ALGO_CONFIG.Overrides.Flatten_Open_Positions or ALGO_CONFIG.Overrides.Flatten_Open_Positions_Opportunistic or ALPHA_TGT_NOTIONAL == 0.00: logging.info('* Trying to flatten small Extend balance, was originally not orderable.') EXTEND_TGT_TAIL_ORDERABLE = True # Hedge_Ratio = abs(( abs( max([abs(float(EXTEND_NOTIONAL_POSITION)), 0.01]) / max([abs(float(ASTER_NOTIONAL_POSITION)), 0.01]) ) - 1 ) * 100) Hedge_Ratio = abs( ( EXTEND_NOTIONAL_POSITION + ASTER_NOTIONAL_POSITION ) / max([ASTER_NOTIONAL_POSITION, 0.01]) ) * 100 Currently_Hedged = Hedge_Ratio < 1.00 def print_summary(use_logging: bool = False): OUT: Any = logging.info if use_logging else print OUT(f''' LOOP SLEEP (SEC): {ALGO_CONFIG.Config.Loop_Sleep_Sec} FLIP SIDES FOR TESTING?: {ALGO_CONFIG.Overrides.Flip_Side_For_Testing}; ASTER ORDER ENABLED? {ALGO_CONFIG.Overrides.Allow_Ordering_Aster}; EXTEND ORDER ENABLED? {ALGO_CONFIG.Overrides.Allow_Ordering_Extend} {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: {'LONG PAYS SHORT' if ASTER_FUND_RATE > 0 else 'SHORT PAYS LONG'} | EXTEND: {'LONG PAYS SHORT' if EXTEND_FUND_RATE > 0 else 'SHORT PAYS LONG'} ASTER: [ Notional Position $ : {ASTER_NOTIONAL_POSITION:.4f} ] | EXTEND: [ Notional Position $ : {EXTEND_NOTIONAL_POSITION:.4f} ] SAME TIME? : {FUNDINGS_AT_SAME_TIME_NEXT_HR} [ Minutes Between Fundings: {min_between_fundings} ] NET FUNDING : {NEXT_NET_FUNDING_RATE:.6%} [{NEXT_NET_FUNDING_RATE*10_000:.2f}bps] [{NEXT_NET_FUNDING_RATE*1_000_000:.0f}pips]; Is Zero?: {Flags.NET_FUNDING_IS_ZERO} [Min: {ALGO_CONFIG.Config.Min_Fund_Rate_Pct_To_Trade}] ALPHA SIDE : {ALPHA_EXCH} [{ALPHA_CARRY_SIDE}] TGT NOTIONAL: $ {abs(ALPHA_TGT_NOTIONAL):.2f}; Flatten Open Positions Flag? {ALGO_CONFIG.Overrides.Flatten_Open_Positions}; Opportunistic? {ALGO_CONFIG.Overrides.Flatten_Open_Positions_Opportunistic} 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:.4f} ] ASTER: {ASTER_TGT_NOTIONAL:.2f} - {ASTER_NOTIONAL_POSITION:.2f} + {ASTER_UNREALIZED_PNL:.2f} = {ASTER_TGT_TAIL:2f} | EXTEND: {EXTEND_TGT_NOTIONAL:.2f} - {EXTEND_NOTIONAL_POSITION:.2f} + {EXTEND_UNREALIZED_PNL:.2f} = {EXTEND_TGT_TAIL:2f} ASTER: {ASTER_TGT_TAIL_BASE_QTY:.4f} > {ASTER.min_order_size:.4f} min [ Order: {ASTER_TGT_TAIL_ORDERABLE} ] | EXTEND: {EXTEND_TGT_TAIL_BASE_QTY:.4f} > {EXTEND.min_order_size:.4f} min [ Order: {EXTEND_TGT_TAIL_ORDERABLE} ] ALPHA: {ALPHA_RATIO:.8f} ALPHA_RATIO: {Alpha_Nominator:_.6f} / {Alpha_Denominator:_.6f} (Px Diff: {abs(Alpha_Nominator-Alpha_Denominator):.2f}); Expected_Alpha = {Expected_Alpha:.6f} + FR[{NEXT_NET_FUNDING_RATE:.6f}] = * {Expected_Alpha_Net_FR:.6f} * FEES : TAKER: {0.00025:.6%}; Expected Alpha w Taker = {Expected_Alpha_Net_FR_w_Taker:.6f} [w/o FR: {Expected_Alpha_w_Taker:.6f}] HEDGE: {Hedge_Ratio:.2f}% <= {1:.2f}%: {Currently_Hedged} [{EXTEND_NOTIONAL_POSITION:.2f} / {ASTER_NOTIONAL_POSITION:.2f}] MKT : Aster: {ASTER.symbol} (best: {best_symbol_by_exchange_aster.symbol}) | Extend: {ASTER.symbol} (best: {best_symbol_by_exchange_extend.symbol}) --- ASTER OPEN ORDERS --- {ASTER_OPEN_ORDERS} --- EXTEND OPEN ORDERS --- {EXTEND_OPEN_ORDERS} ''') # ASTER: [ Available Collateral: {ASTER_AVAIL_COLLATERAL:.4f} ] | EXTEND: [ Available Collateral: {EXTEND_AVAIL_COLLATERAL:.4f} ] # Try Making Hedge Order Contingent on Alpha Order Fills (Basically Hedge has to wait for sig Diff in Balance to order.) would improve when extended is thin (Overnight). if ALGO_CONFIG.Logging.Log_Summary_Each_Loop: print_summary(use_logging=True) if ALGO_CONFIG.Logging.Print_Summary_Each_Loop: print_summary(use_logging=False) # print_summary() ### ROUTES ### # Just_Rejected_Or_Expired MIN_EXPECTED_ALPHA_TO_TRADE = 0.0000 if ALGO_CONFIG.Overrides.Flatten_Open_Positions_Opportunistic: exp_alpha = Expected_Alpha_w_Taker else: exp_alpha = Expected_Alpha_Net_FR_w_Taker # MIN_EXPECTED_ALPHA_TO_TRADE = abs(NEXT_NET_FUNDING_RATE)*-1 # MIN_EXPECTED_ALPHA_TO_TRADE = -0.000001 # ALPHA RATIO CHECK if not( ( exp_alpha > MIN_EXPECTED_ALPHA_TO_TRADE ) or ( ASTER_OPEN_ORDERS or EXTEND_OPEN_ORDERS or ALGO_CONFIG.Overrides.Flatten_Open_Positions) ) and Currently_Hedged: # if not( ( Expected_Alpha_Net_FR_w_Taker > MIN_EXPECTED_ALPHA_TO_TRADE ) or ( ASTER_OPEN_ORDERS or EXTEND_OPEN_ORDERS or ALGO_CONFIG.Overrides.Flatten_Open_Positions) ) and Currently_Hedged: if ALGO_CONFIG.Logging.Print_Summary_Each_Loop: print(f'Alpha Ratio too low ({ALPHA_RATIO:.8f}) and no Open Orders...') elif ( Expected_Alpha_Net_FR_w_Taker <= MIN_EXPECTED_ALPHA_TO_TRADE ) and ( ASTER_OPEN_ORDERS or EXTEND_OPEN_ORDERS ) and Currently_Hedged and not(ALGO_CONFIG.Overrides.Flatten_Open_Positions): await aster_cancel_all_orders() await extend_cancel_all_orders() logging.info('Expected_Alpha went away with open orders...cancelling since we are currently hedged...') # time.sleep( (1/1000)*100 ) # 100ms wait for ws cancel response else: # logging.info(f'*** Alpha Ratio HIT - LETS ORDER: {ALPHA_RATIO:.8f}') # ASTER if ASTER_TGT_TAIL_ORDERABLE and ALGO_CONFIG.Overrides.Allow_Ordering_Aster: # if ALGO_CONFIG.Overrides.Allow_Ordering_Aster: symbol = ASTER.symbol side = 'BUY' if ASTER_TGT_TAIL_BASE_QTY > 0.00 else 'SELL' # qty = str(abs(ASTER_TGT_TAIL_BASE_QTY)) qty = Decimal(value=str(abs(ASTER_TGT_TAIL_BASE_QTY))) price = ASTER_TOB_PX - ( float(ASTER.min_price)*int(ALGO_CONFIG.Config.Price_Worsener_Aster) ) if side == 'BUY' else ASTER_TOB_PX + ( float(ASTER.min_price)*int(ALGO_CONFIG.Config.Price_Worsener_Aster) ) if abs( ( float(ASTER_TGT_TAIL_BASE_QTY)*float(price) ) + ASTER_NOTIONAL_POSITION ) > ALGO_CONFIG.Config.Max_Target_Notional*ALGO_CONFIG.Config.Max_Order_Over_Notional_Ratio: logging.info(f'TRYING TO ORDER OVER MAX NOTIOANL - ASTER: {ASTER_NOTIONAL_POSITION} + {float(ASTER_TGT_TAIL_BASE_QTY)*float(price)} (qty: {float(ASTER_TGT_TAIL_BASE_QTY):.2f}; px: {float(price):.2f})') await kill_algo() if ASTER_OPEN_ORDERS: open_order_id = ASTER_OPEN_ORDERS[0].get('order_id') if ASTER_OPEN_ORDERS[0].get('order_id') is not None else ASTER_OPEN_ORDERS[0]['orderId'] open_order_px = float(ASTER_OPEN_ORDERS[0].get('price')) if ASTER_OPEN_ORDERS[0].get('price') is not None else float(ASTER_OPEN_ORDERS[0]['original_price']) min_price = ASTER.min_price min_price = int(min_price) if min_price == int(min_price) else min_price if Decimal(str( float(open_order_px) - float(price) )).quantize(Decimal(str(min_price)), rounding=ROUND_HALF_UP) == 0.00: # if round(open_order_px - float(price), len(str(ASTER.min_price)) - 2 ) == 0.00: if ALGO_CONFIG.Logging.Print_Summary_Each_Loop: print('ASTER OPEN ORDER NO PX CHG; SKIPPING') place_order = False else: cancel_order: dict = { "url": "/fapi/v3/order", "method": "DELETE", "params": { 'symbol': ASTER.symbol, 'orderId': open_order_id, } } cr: dict = await aster_auth.post_authenticated_url(cancel_order) # ty:ignore[invalid-assignment] if cr.get('status', None) == 'CANCELED': ASTER_OPEN_ORDERS.pop(0) place_order = True else: logging.warning(f'ASTER ORDER FAILED TO CANCEL DURING CR ({open_order_id}): RESP {cr}') place_order = False else: place_order = True if ASTER_TGT_TAIL_BASE_QTY == 0.00: place_order = False logging.info('ASTER TRYNG TO ORDER 0.00 BASE QTY, SKIPPING') if place_order: min_price = ASTER.min_price min_price = int(min_price) if min_price == int(min_price) else min_price price: Decimal = Decimal(str(price)).quantize(Decimal(str(min_price)), rounding=ROUND_HALF_UP) if price == Decimal(str(0.00)).quantize(Decimal(str(min_price)), rounding=ROUND_HALF_UP): logging.info('ASTER TRYNG TO ORDER with A PRICE OF 0.00, SKIPPING') continue if qty >= ASTER.min_order_size and (qty*price) > ASTER.min_notional: reduceOnly = False else: reduceOnly = True post_order = { "url": "/fapi/v3/order", "method": "POST", "params": { 'symbol': symbol, 'side': side, 'type': 'LIMIT', 'timeInForce': 'GTX', 'quantity': qty, 'price': price, 'reduceOnly': reduceOnly } } order_resp: dict = await aster_auth.post_authenticated_url(post_order) # ty:ignore[invalid-assignment] if order_resp.get('orderId', None) is not None: order_resp['original_price'] = price order_resp['order_status'] = order_resp['status'] ASTER_OPEN_ORDERS.append(order_resp) Just_Rejected_Or_Expired = False utils.send_tg_alert(f'FR_ALGO - ASTER Order ({order_resp['orderId']}). Start_$: {ASTER_NOTIONAL_POSITION:.4f}; Value: {float(ASTER_TGT_TAIL_BASE_QTY)*float(price):.4f}; Price: {float(price):.4f}') logging.info(f'ASTER ORDER PLACED SUCCESS: {order_resp}') print_summary(use_logging=True) else: logging.critical(f'*** Aster Order Response Abnormal: {order_resp}; post_order: {post_order}') await kill_algo() else: pass # logging.warning('ASTER PLACE ORDER CHECKS FAILED, SKIPPING') elif not(ASTER_TGT_TAIL_ORDERABLE) and ASTER_OPEN_ORDERS: ### Add code to flatten small balances logging.info('ASTER HAS NO TAIL BUT OPEN ORDERS - CANCELLING OPEN ORDERS') await aster_cancel_all_orders() time.sleep(0.1) # if (float(ALPHA_TGT_NOTIONAL) < float(EXTEND_NOTIONAL_POSITION)) and ((float(EXTEND_NOTIONAL_POSITION) + float(EXTEND_TGT_TAIL)) > float(EXTEND_NOTIONAL_POSITION)): # EXTEND_TGT_TAIL_ORDERABLE= False # print('ASTER ordering in the wrong directiion - Should be selling, but its buying - skipping') # elif (float(ALPHA_TGT_NOTIONAL) > float(EXTEND_NOTIONAL_POSITION)) and ((float(EXTEND_NOTIONAL_POSITION) + float(EXTEND_TGT_TAIL)) < float(EXTEND_NOTIONAL_POSITION)): # EXTEND_TGT_TAIL_ORDERABLE= False # print('ASTER ordering in the wrong directiion - Should be buying, but its selling - skipping') # EXTEND if (EXTEND_TGT_TAIL_ORDERABLE and ALGO_CONFIG.Overrides.Allow_Ordering_Extend): # if ALGO_CONFIG.Overrides.Allow_Ordering_Extend: side = OrderSide.BUY if EXTEND_TGT_TAIL_BASE_QTY > 0.00 else OrderSide.SELL symbol = EXTEND.symbol qty = Decimal(value=str(abs(EXTEND_TGT_TAIL_BASE_QTY))) Time_Since_Last_Aster_Fill_ms = ( datetime.now().timestamp()*1000 ) - Last_Aster_Fill_Time_Ts min_price = EXTEND.min_price min_price = int(min_price) if min_price == int(min_price) else min_price if Time_Since_Last_Aster_Fill_ms > ( 1000 * ALGO_CONFIG.Config.Switch_To_Taker_Seconds ): # Change to allow taker orders if its been more than x seconds post_only = False price: Decimal = Decimal(value=str(EXTEND_TOB_PX - ( float(min_price)*int(ALGO_CONFIG.Config.Price_Worsener_Extend) ) if side == 'BUY' else EXTEND_TOB_PX + ( float(min_price)*int(ALGO_CONFIG.Config.Price_Worsener_Extend) ) )).quantize(Decimal(str(min_price)), rounding=ROUND_HALF_UP) else: # post_only = True post_only = False price: Decimal = Decimal(value=str(EXTEND_TOB_PX)).quantize(Decimal(str(min_price)), rounding=ROUND_HALF_UP) if abs( ( float(EXTEND_TGT_TAIL_BASE_QTY)*float(price) ) + EXTEND_NOTIONAL_POSITION ) > ALGO_CONFIG.Config.Max_Target_Notional*ALGO_CONFIG.Config.Max_Order_Over_Notional_Ratio: logging.info(f'TRYING TO ORDER OVER MAX NOTIOANL - EXTEND: {EXTEND_NOTIONAL_POSITION:.2f} + {float(EXTEND_TGT_TAIL_BASE_QTY)*float(price):.2f} (qty: {float(EXTEND_TGT_TAIL_BASE_QTY):.2f}; px: {float(price):.2f})') await kill_algo() if EXTEND_OPEN_ORDERS: open_order_dict = dict(EXTEND_OPEN_ORDERS[0]) open_order_id = str(open_order_dict['external_id']) open_order_px = float(open_order_dict['price']) # if int(qty) == 0: # place_order = False # place_residual_order = False # logging.info(f'EXTEND NOT ORDERING DUE TO NOTIONAL QTY == 0; Filled: {float(open_order_filled_qty):.4f}; Residual: {qty:.4f}') # else: # place_order = True # place_residual_order = False # logging.info(f'Ordering RESIDUAL market order for remaining small amount: {qty}') else: open_order_id = None open_order_px = 0 place_order = True if place_order: price: Decimal = Decimal(str(price)).quantize(Decimal(str(min_price)), rounding=ROUND_HALF_UP) if round(open_order_px - float(price), len(str(min_price)) - 2 ) == 0.00: if ALGO_CONFIG.Logging.Print_Summary_Each_Loop: print('EXTEND OPEN ORDER NO PX CHG; SKIPPING') else: try: if abs(float(EXTEND_NOTIONAL_POSITION) + (float(qty)*float(price))) < abs(float(EXTEND_NOTIONAL_POSITION)): reduce_only = True else: reduce_only = False # taker_fee = taker_fee=Decimal("0.00000") if post_only else Decimal("0.00025") taker_fee = Decimal("0.00025") order_resp: WrappedApiResponse[PlacedOrderModel] = await EXTEND_CLIENT.place_order( market_name=symbol, amount_of_synthetic=Decimal(str(qty)), price=Decimal(str(price)), side=side, taker_fee=taker_fee, previous_order_id=open_order_id, post_only=post_only, reduce_only=reduce_only ) except Exception as e: logging.error(f'EXTEND ORDER PLACEMENT FAILED: {e}') logging.error(f'EXTEND ORDER PLACEMENT FAILED - POSTED: market_name:{symbol}, side: {side} amount_of_synthetic:{qty}, price:{price}, side:{side},taker_fee:{taker_fee}, previous_order_id:{open_order_id}, post_only:{post_only}; reduce_only:{reduce_only}') logging.error(traceback.format_exc()) logging.error(f'EXTEND ORDER PLACEMENT FAILED - RESP: {order_resp}') order_resp_dict = dict(order_resp) if order_resp_dict.get('status', None) == 'OK': if EXTEND_OPEN_ORDERS: EXTEND_OPEN_ORDERS.pop(0) order_dict = dict(order_resp_dict['data']) order_dict['status'] = 'NEW' order_dict['price'] = str(price) order_dict['qty'] = str(qty) order_dict['filled_qty'] = str(0) order_dict['side'] = str(side) EXTEND_OPEN_ORDERS.append(order_dict) Just_Rejected_Or_Expired = False utils.send_tg_alert(f'FR_ALGO - EXTEND Order ({order_dict.get('id', None)}). Start_$: {EXTEND_NOTIONAL_POSITION:.2f}; Value: {float(EXTEND_TGT_TAIL_BASE_QTY)*float(price):.2f}; Price: {float(price):.2f}') logging.info(f'EXTEND ORDER PLACED SUCCESS: {order_dict}') print_summary(use_logging=True) else: logging.critical(f'*** Extend Order Response Abnormal: {order_resp};') await kill_algo() else: logging.warning('EXTEND PLACE ORDER CHECKS FAILED, SKIPPING') elif not(EXTEND_TGT_TAIL_ORDERABLE) and EXTEND_OPEN_ORDERS: logging.info('EXTEND HAS NO TAIL BUT OPEN ORDERS - CANCELLING OPEN ORDERS') await extend_cancel_all_orders() if ASTER_OPEN_ORDERS or EXTEND_OPEN_ORDERS: continue else: time.sleep(ALGO_CONFIG.Config.Loop_Sleep_Sec) if ALGO_CONFIG.Logging.Print_Summary_Each_Loop: print(f'_____ End No Open Orders _____ (Algo Engine ms: {(time.time() - loop_start)*1000:.2f}); Sleeping for sec: {ALGO_CONFIG.Config.Loop_Sleep_Sec:.0f}') except KeyboardInterrupt: logging.info('CANCELLING OPEN ORDERS') await kill_algo() except Exception as e: logging.error(traceback.format_exc()) logging.critical(f'*** ALGO ENGINE CRASHED: {e}') logging.info('CANCELLING OPEN ORDERS') utils.send_tg_alert(f'FR_ALGO_CRASHED: {str(e)}') await kill_algo() ### MAIN STARTUP ### async def main(): global EXTEND_CLIENT global VAL_KEY global CON global ALGO_CONFIG global ASTER global EXTEND global Open_Symbols _, 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') await set_comb_open_symbols() best_symbol_by_exchange: dict = json.loads(s=VAL_KEY.get(name='fr_engine_best_fund_rate_output')) # ty:ignore[invalid-argument-type] if Open_Symbols: logging.info(f'OPEN SYMBOLS: {Open_Symbols}') master_data = json.loads(s=VAL_KEY.get(name='fr_engine_best_fund_rate_master')) # ty:ignore[invalid-argument-type] open_symbol_to_work = Open_Symbols[0] current_pos_master_ast = [d for d in master_data if d.get('symbol_ext') == open_symbol_to_work][0] ASTER = structs.Perpetual_Exchange( mult = int(current_pos_master_ast['max_leverage_ast']), lh_asset = current_pos_master_ast['lh_asset_ast'], rh_asset = current_pos_master_ast['rh_asset_ast'], symbol_asset_separator = '', initial_funding_rate=float(current_pos_master_ast['funding_rate_ast']), min_price=float(current_pos_master_ast['min_price_ast']), min_order_size=float(current_pos_master_ast['min_order_size_ast']), min_lot_size=float(current_pos_master_ast['min_lot_size_ast']), min_notional=float(current_pos_master_ast['min_notional_ast']), buy_ratio=float(current_pos_master_ast['min_notional_ast']), ) EXTEND = structs.Perpetual_Exchange( mult = int(current_pos_master_ast['max_leverage_ext']), lh_asset = current_pos_master_ast['lh_asset_ext'], rh_asset = current_pos_master_ast['rh_asset_ext'], symbol_asset_separator = '-', initial_funding_rate=float(current_pos_master_ast['funding_rate_ext']), min_price=float(current_pos_master_ast['min_price_ext']), min_order_size=float(current_pos_master_ast['min_order_size_ext']), min_lot_size=float(current_pos_master_ast['min_lot_size_ext']), min_notional=float(current_pos_master_ast['min_notional_ext']), buy_ratio=float(current_pos_master_ast['min_notional_ext']), ) Open_Symbols.pop(0) else: ASTER = structs.Perpetual_Exchange(**best_symbol_by_exchange['ASTER']) EXTEND = structs.Perpetual_Exchange(**best_symbol_by_exchange['EXTEND']) # await get_aster_exch_info(symbol_override=Open_Symbols[0]) # await get_extend_exch_info(symbol_override=Open_Symbols[0]) with open('algo_config.json', mode='r', encoding='utf-8') as file: ALGO_CONFIG = json.load(file) ALGO_CONFIG = structs.Algo_Config(**ALGO_CONFIG) ALGO_CONFIG.Config.Max_Target_Notional = float(min([ASTER.mult, EXTEND.mult]) * ALGO_CONFIG.Config.Target_Open_Cash_Position) # logging.info(f'Initial Algo Config: {ALGO_CONFIG}') VAL_KEY.set(name='fr_orchestrator_output', value=json.dumps(obj=ALGO_CONFIG.model_dump())) VAL_KEY.set(name='fr_algo_working_symbol', value=json.dumps(obj={'ASTER': asdict(obj=ASTER), 'EXTEND': asdict(obj=EXTEND)})) async with engine.connect() as CON: ### ASTER SETUP ### # await get_aster_collateral() await get_aster_notional_position() await get_aster_exch_info() await get_aster_open_orders() ### EXTEND SETUP ### # await get_extend_collateral() await get_extend_notional() await get_extend_exch_info() await get_extend_open_orders() 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())