import asyncio import json import logging import math import os import time import traceback from dataclasses import asdict, dataclass, field from datetime import datetime, timezone from decimal import ROUND_DOWN, Decimal from typing import AsyncContextManager 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 import modules.utils as utils import modules.aster_auth as aster_auth import modules.extended_auth as extend_auth import modules.structs as structs ### 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' ### Algo Config ### ALGO_CONFIG: structs.Algo_Config = None ### CONSTANTS ### ASTER = structs.Perpetual_Exchange( mult = 150, lh_asset = 'ETH', rh_asset = 'USDT', symbol_asset_separator = '', ) EXTEND_LH_ASSET: str = 'ETH' EXTEND_RH_ASSET: str = 'USD' EXTEND_TICKER: str = EXTEND_LH_ASSET + '-' + EXTEND_RH_ASSET ### GLOBALS ### ASTER_MULT = 150 EXTEND_MULT = 50 ASTER_MIN_ORDER_QTY = 0.001 EXTEND_MIN_ORDER_QTY = 0.01 ASTER_AVAIL_COLLATERAL = 0 EXTEND_AVAIL_COLLATERAL = 0 ASTER_NOTIONAL_POSITION = 0 EXTEND_NOTIONAL_POSITION = 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_DOWN) ### 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_notional_position(resp: dict | None = None): global ASTER_NOTIONAL_POSITION global ASTER_MULT if not resp: fut_acct_positionRisk = { "url": "/fapi/v3/positionRisk", "method": "GET", "params": { 'symbol': ASTER.symbol, } } resp = await aster_auth.post_authenticated_url(fut_acct_positionRisk) d = [x for x in resp if x.get('symbol', None) == ASTER.symbol][0] 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: notional = float(d['notional']) else: notional = float(d['position_amount'])*float(d['entry_price']) previous_notional_position = ASTER_NOTIONAL_POSITION ASTER_NOTIONAL_POSITION = notional - aster_unrealized_pnl if not resp: ASTER_MULT = float(d['leverage']) if abs(ASTER_NOTIONAL_POSITION) > ALGO_CONFIG.Max_Target_Notional*1.01: logging.info(f'BAD NOTIONAL - ASTER CHANGE: {ASTER_NOTIONAL_POSITION}; UR PNL: {aster_unrealized_pnl}; MULT: {ASTER_MULT}; d: {d}; resp: {resp}') await kill_algo() if ASTER_NOTIONAL_POSITION != previous_notional_position: logging.info(f'ASTER NOTIONAL CHANGE: {previous_notional_position} -> {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_notional(resp: dict | None = None): global EXTEND_NOTIONAL_POSITION global EXTEND_MULT 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_TICKER] if not pos_dict: logging.info('get_extend_notional - No Positions') else: pos_dict = pos_dict[0] unrealized_pnl = pos_dict.get('unrealised_pnl', 0) previous_notional_position = EXTEND_NOTIONAL_POSITION 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(unrealized_pnl) EXTEND_MULT = pos_dict.get('leverage', EXTEND_MULT) if EXTEND_NOTIONAL_POSITION != previous_notional_position: logging.info(f'EXTEND NOTIONAL CHANGE: {previous_notional_position} -> {EXTEND_NOTIONAL_POSITION:.2f}; UR PNL: {unrealized_pnl:.2f}; MULT: {EXTEND_MULT:.0f}; resp: {bool(resp)}') ### EXCHANGE INFO ### async def get_aster_exch_info(): global ASTER_MIN_ORDER_QTY fut_acct_exchangeInfo = { "url": "/fapi/v3/exchangeInfo", "method": "GET", "params": {} } r = await 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) ### CANCEL ORDERS ### async def aster_cancel_all_orders(): cancel_all_open_orders = { "url": "/fapi/v3/allOpenOrders", "method": "DELETE", "params": { 'symbol': 'ETHUSDT', } } 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_TICKER]) 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 ALGO_CONFIG global ASTER_OPEN_ORDERS global EXTEND_OPEN_ORDERS try: while True: loop_start = time.time() # print('__________Start___________') ### ALGO CONIFG ### ALGO_CONFIG = json.loads(VAL_KEY.get('fr_orchestrator_output'), object_hook=lambda d: structs.Algo_Config(**d)) ALGO_CONFIG.Max_Target_Notional = float(min([ASTER_MULT, EXTEND_MULT]) * ALGO_CONFIG.Target_Open_Cash_Position) MIN_TIME_TO_FUNDING = ALGO_CONFIG.Min_Time_To_Funding_Minutes * 60 * 1000 ### Load Data from Feedhandlers ### ASTER_FUND_RATE_DICT = json.loads(VAL_KEY.get('fund_rate_aster')) EXTENDED_FUND_RATE_DICT = json.loads(VAL_KEY.get('fund_rate_extended')) ASTER_FUND_RATE = float(ASTER_FUND_RATE_DICT.get('funding_rate', 0)) EXTEND_FUND_RATE = float(EXTENDED_FUND_RATE_DICT.get('funding_rate', 0)) if ALGO_CONFIG.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)) EXTEND_FUND_RATE_TIME = float(EXTENDED_FUND_RATE_DICT.get('next_funding_time_ts_ms', 0)) EXTEND_FUND_RATE_TIME = max([EXTEND_FUND_RATE_TIME, 0]) ASTER_TICKER_DICT = json.loads(VAL_KEY.get('fut_ticker_aster')) EXTENDED_TICKER_DICT = json.loads(VAL_KEY.get('fut_ticker_extended')) ### 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 [] 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 [] ### Manage Local Notionals Using Updates from WS ### ASTER_WS_POS_UPDATES = VAL_KEY.get('fr_aster_user_positions') ASTER_WS_POS_UPDATES = json.loads(ASTER_WS_POS_UPDATES) if ASTER_WS_POS_UPDATES is not None else [] EXTEND_WS_POS_UPDATES = VAL_KEY.get('fr_extended_user_positions') EXTEND_WS_POS_UPDATES = 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 = VAL_KEY.get('fr_aster_user_orders') ASTER_WS_ORDER_UPDATES = json.loads(ASTER_WS_ORDER_UPDATES) if ASTER_WS_ORDER_UPDATES is not None else [] EXTEND_WS_ORDER_UPDATES = VAL_KEY.get('fr_extended_user_orders') EXTEND_WS_ORDER_UPDATES = 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): print(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(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. 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['status'] ### Got a keyerror on this 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) 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() 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() 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 = order_update[0] order_update_status = order_update.get('status') order_status_changed = 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 or EXPIRED: {order_id}') EXTEND_OPEN_ORDERS.pop(idx) elif order_update_status in ['PARTIALLY_FILLED']: logging.info(f'EXTEND ORDER PARTIALLY FILLED: {order_id}') await get_extend_collateral() await get_extend_notional() 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() else: logging.critical(f'EXTEND ORDER STATUS CHG TO UNEXPECTED VALUE, KILLING... ({order_id}): {order_orig_status} -> {order_update_status}') 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 # 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 = ALGO_CONFIG.Max_Target_Notional else: ALPHA_CARRY_SIDE = 'SELL' ALPHA_TGT_NOTIONAL = ALGO_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 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 == 0.00 if Flags.NET_FUNDING_IS_ZERO: logging.info('NET FUNDING = 0.00; Cancelling Open Orders; Wait Until Non-Zero.') ALPHA_TGT_NOTIONAL = 0.00 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']) else: ASTER_TOB_PX = float(ASTER_TICKER_DICT['best_bid_px']) EXTEND_TOB_PX = float(EXTENDED_TICKER_DICT['best_ask_px']) 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']) else: ASTER_TOB_PX = float(ASTER_TICKER_DICT['best_ask_px']) EXTEND_TOB_PX = float(EXTENDED_TICKER_DICT['best_bid_px']) ASTER_TGT_TAIL = ASTER_TGT_NOTIONAL - ASTER_NOTIONAL_POSITION EXTEND_TGT_TAIL = EXTEND_TGT_NOTIONAL - EXTEND_NOTIONAL_POSITION ASTER_TGT_TAIL_BASE_QTY = Decimal(str(float(ASTER_TGT_TAIL) / float(ASTER_TOB_PX))).quantize(Decimal(str(0.001)), rounding=ROUND_DOWN) EXTEND_TGT_TAIL_BASE_QTY = Decimal(str(float(EXTEND_TGT_TAIL) / float(EXTEND_TOB_PX))).quantize(Decimal(str(0.001)), rounding=ROUND_DOWN) MAX_MIN_ORDER_QTY = max([ASTER_MIN_ORDER_QTY, EXTEND_MIN_ORDER_QTY]) ASTER_TGT_TAIL_ORDERABLE = abs(ASTER_TGT_TAIL_BASE_QTY) >= MAX_MIN_ORDER_QTY EXTEND_TGT_TAIL_ORDERABLE = abs(EXTEND_TGT_TAIL_BASE_QTY) >= MAX_MIN_ORDER_QTY def print_summary(use_logging: bool = False): OUT: print | logging.info = logging.info if use_logging else print OUT(f''' FLIP SIDES FOR TESTING?: {ALGO_CONFIG.Flip_Side_For_Testing} {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: [ 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} [ 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} ALPHA SIDE : {ALPHA_EXCH} [{ALPHA_CARRY_SIDE}] TGT NOTIONAL: $ {ALGO_CONFIG.Max_Target_Notional if not Flags.NET_FUNDING_IS_ZERO else 0.00} 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:.4f} - {ASTER_NOTIONAL_POSITION:.4f} = Tail: {ASTER_TGT_TAIL:4f} | EXTEND: {EXTEND_TGT_NOTIONAL:.4f} - {EXTEND_NOTIONAL_POSITION:.4f} = Tail: {EXTEND_TGT_TAIL:4f} ASTER: {ASTER_TGT_TAIL_BASE_QTY:.4f} > {MAX_MIN_ORDER_QTY:.4f} min [ Order: {ASTER_TGT_TAIL_ORDERABLE} ] | EXTEND: {EXTEND_TGT_TAIL_BASE_QTY:.4f} > {MAX_MIN_ORDER_QTY:.4f} min [ Order: {EXTEND_TGT_TAIL_ORDERABLE} ] --- ASTER OPEN ORDERS --- {ASTER_OPEN_ORDERS} --- EXTEND OPEN ORDERS --- {EXTEND_OPEN_ORDERS} ''') if ALGO_CONFIG.Print_Summary_Each_Loop: print_summary() # print_summary() ### ROUTES ### # ASTER if ASTER_TGT_TAIL_ORDERABLE and ALGO_CONFIG.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)) price = ASTER_TOB_PX - ALGO_CONFIG.Price_Worsener_Aster if side == 'BUY' else ASTER_TOB_PX + ALGO_CONFIG.Price_Worsener_Aster if abs( ( float(ASTER_TGT_TAIL_BASE_QTY)*float(price) ) + ASTER_NOTIONAL_POSITION ) > ALGO_CONFIG.Max_Target_Notional*1.01: 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']) if round(open_order_px - float(price), 2) == 0.00: logging.info('ASTER OPEN ORDER NO PX CHG; SKIPPING') place_order = False else: cancel_order = { "url": "/fapi/v3/order", "method": "DELETE", "params": { 'symbol': ASTER.symbol, 'orderId': open_order_id, } } cr = await aster_auth.post_authenticated_url(cancel_order) 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: price = Decimal(str(price)).quantize(Decimal(str(0.01)), rounding=ROUND_DOWN) post_order = { "url": "/fapi/v3/order", "method": "POST", "params": { 'symbol': symbol, 'side': side, 'type': 'LIMIT', 'timeInForce': 'GTC', 'quantity': qty, 'price': price, } } order_resp = await aster_auth.post_authenticated_url(post_order) if order_resp.get('orderId', None) is not None: order_resp['original_price'] = price ASTER_OPEN_ORDERS.append(order_resp) utils.send_tg_alert(f'FR_ALGO - ASTER Order. Start_$: {ASTER_NOTIONAL_POSITION:.2f}; Value: {float(ASTER_TGT_TAIL_BASE_QTY)*float(price):.2f}; Price: {float(price):.2f}') logging.info(f'ASTER ORDER PLACED SUCCESS: {order_resp}') print_summary(use_logging=True) else: pass # logging.warning('ASTER PLACE ORDER CHECKS FAILED, SKIPPING') elif not(ASTER_TGT_TAIL_ORDERABLE) and ASTER_OPEN_ORDERS: logging.info('ASTER HAS NO TAIL BUT OPEN ORDERS - CANCELLING OPEN ORDERS') await aster_cancel_all_orders() # EXTEND if EXTEND_TGT_TAIL_ORDERABLE and ALGO_CONFIG.Allow_Ordering_Extend: symbol = EXTEND_TICKER side = OrderSide.BUY if EXTEND_TGT_TAIL_BASE_QTY > 0.00 else OrderSide.SELL qty = Decimal(str(abs(EXTEND_TGT_TAIL_BASE_QTY))) price = EXTEND_TOB_PX - ALGO_CONFIG.Price_Worsener_Extend if side == 'BUY' else EXTEND_TOB_PX + ALGO_CONFIG.Price_Worsener_Extend if abs( ( float(EXTEND_TGT_TAIL_BASE_QTY)*float(price) ) + EXTEND_NOTIONAL_POSITION ) > ALGO_CONFIG.Max_Target_Notional*1.01: 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 = open_order_dict['external_id'] open_order_px = float(open_order_dict['price']) open_order_filled_qty = float(open_order_dict['filled_qty']) # qty = abs(float(qty)) - abs(float(open_order_filled_qty)) # Was trying to account for partial fills but thats not necessary, handled by position change so qty is correct w/o further adj. # qty = Decimal(str(qty)) if qty >= MAX_MIN_ORDER_QTY: place_order = True else: place_order = False logging.info(f'EXTEND NOT ORDERING DUE TO FILLED QTY RESIDUAL < MIN ORDER; Filled: {float(open_order_filled_qty):.4f}; Residual: {qty:.4f}') else: open_order_id = None open_order_px = 0 place_order = True if place_order: price = Decimal(str(price)).quantize(Decimal(str(0.01)), rounding=ROUND_DOWN) if round(open_order_px - float(price), 2) == 0.00: logging.info('EXTEND OPEN ORDER NO PX CHG; SKIPPING') else: try: order_resp = await EXTEND_CLIENT.place_order( market_name=symbol, amount_of_synthetic=qty, price=price, side=side, taker_fee=Decimal("0.00025"), previous_order_id=open_order_id, ) except Exception as e: logging.error(f'EXTEND ORDER PLACEMENT FAILED - RESP: {order_resp}') logging.error(f'EXTEND ORDER PLACEMENT FAILED: {e}') logging.error(traceback.format_exc()) 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) EXTEND_OPEN_ORDERS.append(order_dict) utils.send_tg_alert(f'FR_ALGO - EXTEND Order. 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: order_resp_dict.get 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() print(f'__________ End ___________ (Algo Engine ms: {(time.time() - loop_start)*1000})') time.sleep(ALGO_CONFIG.Loop_Sleep_Sec) 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 _, 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') with open('algo_config.json', 'r', encoding='utf-8') as file: ALGO_CONFIG = json.load(file, object_hook=lambda d: structs.Algo_Config(**d)) ALGO_CONFIG.Max_Target_Notional = float(min([ASTER_MULT, EXTEND_MULT]) * ALGO_CONFIG.Target_Open_Cash_Position) VAL_KEY.set('fr_orchestrator_output', json.dumps(asdict(ALGO_CONFIG))) 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())