Files
Funding_Rate/main_v1.1.py

1018 lines
54 KiB
Python
Raw Permalink Normal View History

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
at_notional_target: structs.Locked_Value = structs.Locked_Value(None)
ALPHA_TGT_NOTIONAL: structs.Current_Previous_Value = structs.Current_Previous_Value(None)
ASTER_OPEN_ORDERS: list[dict] = []
EXTEND_OPEN_ORDERS: list[dict] = []
### 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 handle_order_updates(exch: str, local_open_orders: list[dict], ws_open_orders: list[dict]) -> list[dict]: # exch = 'ASTER' | 'EXTEND'
global Just_Rejected_Or_Expired
global Last_Aster_Fill_Time_Ts
if ws_open_orders:
for idx, o in enumerate(local_open_orders):
o = dict(o)
if o.get('order_id') is not None:
ws_order_id_field = 'order_id'
elif o.get('orderId') is not None:
ws_order_id_field = 'orderId'
else:
ws_order_id_field = 'id'
order_id = o[ws_order_id_field]
order_orig_status: str = o.get('status') if o.get('status') is not None else o['order_status'] # ty:ignore[invalid-assignment]
order_update: list[dict] = [dict(ou) for ou in ws_open_orders 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.get('status') if order_update.get('status') is not None else order_update['order_status'] # ty:ignore[invalid-assignment]
order_status_changed: bool = order_orig_status.upper() != order_update_status.upper()
local_open_orders[idx]['order_id'] = order_id
local_open_orders[idx]['status'] = order_update_status
local_open_orders[idx]['price'] = order_update.get('price', 0) if order_update.get('price') is not None else order_update['original_price']
if order_status_changed:
logging.info(f'{exch} ORDER ({order_id}): {order_orig_status} -> {order_update_status}')
local_open_orders[idx] = order_update
if order_update_status in ['CANCELLED','CANCELED','EXPIRED','REJECTED']:
logging.info(f'{exch} ORDER CANCELLED or EXPIRED: {order_id}')
local_open_orders.pop(idx)
Just_Rejected_Or_Expired = True
utils.send_tg_alert(f'FR_ALGO - {exch} REJECTED ({order_id})')
elif order_update_status in ['PARTIALLY_FILLED']:
logging.info(f'{exch} ORDER PARTIALLY FILLED: {order_id}')
# await get_aster_collateral()
if exch=='ASTER':
await get_aster_notional_position(resp=ws_open_orders)
Last_Aster_Fill_Time_Ts = datetime.now().timestamp()*1000
else:
await get_extend_notional()
utils.send_tg_alert(f'FR_ALGO - {exch} PARTIALLY FILLED ({order_id})')
elif order_update_status in ['FILLED']:
logging.info(f'{exch} ORDER FILLED: {order_id}')
local_open_orders.pop(idx)
# await get_aster_collateral()
if exch=='ASTER':
await get_aster_notional_position(resp=ws_open_orders)
Last_Aster_Fill_Time_Ts = datetime.now().timestamp()*1000
else:
await get_extend_notional()
utils.send_tg_alert(f'FR_ALGO - {exch} FILLED ({order_id})')
else:
logging.critical(f'{exch} ORDER STATUS CHG TO UNEXPECTED VALUE, KILLING... ({order_id}): {order_orig_status} -> {order_update_status}')
await kill_algo()
return local_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) # ty:ignore[invalid-assignment]
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_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
previous_notional_obj = ASTER.notional_obj
previous_notional_position = ASTER.notional_position
if resp:
pos_dict = [x for x in resp if x.get('symbol', None) == ASTER.symbol]
if pos_dict:
pos_dict = pos_dict[0]
else:
pos_dict = {}
pos_dict['side'] = 'LONG'
pos_dict['entry_price'] = 0.00
pos_dict['position_amount'] = 0.00
pos_dict['unrealized_pnl'] = 0.00
pos_dict['timestamp_arrival'] = round(datetime.now().timestamp()*1000)
# logging.info('get_aster_notional - No Positions')
else:
logging.info('Getting Aster Notionals from API')
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 = []
pos_dict = [x for x in resp if x.get('symbol', None) == ASTER.symbol]
if pos_dict:
pos_dict = pos_dict[0]
else:
pos_dict = {}
pos_dict['side'] = 'LONG'
pos_dict['entry_price'] = 0.00
pos_dict['position_amount'] = 0.00
pos_dict['unrealized_pnl'] = 0.00
logging.info('get_aster_notional - No Positions')
pos_dict['timestamp_arrival'] = round(datetime.now().timestamp()*1000)
if previous_notional_obj:
if previous_notional_obj['timestamp_arrival'] > pos_dict['timestamp_arrival']:
# logging.info(f'ASTER 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
ASTER.notional_obj = pos_dict
if len(pos_dict) < 1:
logging.info(f'BAD NOTIONAL - ASTER CHANGE: Empty pos_dict: {pos_dict}; resp: {resp}')
await kill_algo()
ASTER.unrealized_pnl = float(pos_dict['unrealized_pnl']) if pos_dict.get('unrealized_pnl') is not None else float(pos_dict['unRealizedProfit'])
if pos_dict.get('notional') is not None:
ASTER.notional_position = float(pos_dict['notional']) - ASTER.unrealized_pnl
else:
ASTER.notional_position = float(pos_dict['position_amount'])*float(pos_dict['entry_price'])
if pos_dict.get('leverage') is not None:
ASTER.mult = int(pos_dict['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}; pos_dict: {pos_dict}; 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_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
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:
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}; pos_dict: {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 ALPHA_TGT_NOTIONAL
global at_notional_target
try:
while True:
loop_start = time.time()
# print('__________Start___________')
### Load 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}')
### Load Local Notional 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 []
if len(ASTER_WS_POS_UPDATES) > 0:
await get_aster_notional_position(resp=ASTER_WS_POS_UPDATES)
if len(EXTEND_WS_POS_UPDATES) > 0:
await get_extend_notional(resp=EXTEND_WS_POS_UPDATES)
### Load Local Order 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')
### Update Local Open Orders w Changes from WS ###
ASTER_OPEN_ORDERS = await handle_order_updates(exch='ASTER', local_open_orders=ASTER_OPEN_ORDERS, ws_open_orders=ASTER_WS_ORDER_UPDATES)
EXTEND_OPEN_ORDERS = await handle_order_updates(exch='EXTEND', local_open_orders=EXTEND_OPEN_ORDERS, ws_open_orders=EXTEND_WS_ORDER_UPDATES)
### 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
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.value = ALGO_CONFIG.Config.Max_Target_Notional
else:
ALPHA_CARRY_SIDE = 'SELL'
ALPHA_TGT_NOTIONAL.value = 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
### Calculate Alpha ###
def unlock_notional_tgt() -> bool:
return ( ALPHA_TGT_NOTIONAL.value != ALPHA_TGT_NOTIONAL.previous_value )
at_notional_target.unlock()
sec_until_funding = round(( EXTEND_FUND_RATE_TIME - (datetime.now().timestamp()*1000) ) / 1000)
if at_notional_target.is_locked and ( sec_until_funding > ( 60*5 ) ) and at_notional_target.value:
ALPHA_TGT_NOTIONAL_FINAL = 0.00
ALPHA_CARRY_SIDE = 'BUY' if ALPHA_CARRY_SIDE == 'SELL' else 'SELL'
else:
ALPHA_TGT_NOTIONAL_FINAL = ALPHA_TGT_NOTIONAL.value
ALPHA_CARRY_SIDE = ALPHA_CARRY_SIDE
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.value = 0.00
if ALGO_CONFIG.Overrides.Flatten_Open_Positions or ALGO_CONFIG.Overrides.Flatten_Open_Positions_Opportunistic or ALPHA_TGT_NOTIONAL_FINAL==0.00:
# ROUNDING = ROUND_UP
ROUNDING = ROUND_HALF_UP
else:
ROUNDING = ROUND_DOWN
if ALPHA_EXCH == 'EXTEND':
ASTER_TGT_NOTIONAL = ALPHA_TGT_NOTIONAL_FINAL*-1
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'])
current_ratio = ( ASTER_TOB_PX / EXTEND_TOB_PX ) - 1
alpha_model_ratio = EXTEND.buy_ratio
alpha_signal: bool = current_ratio > EXTEND.buy_ratio
else:
ASTER_TOB_PX = float(ASTER_TICKER_DICT['best_bid_px'])
EXTEND_TOB_PX = float(EXTENDED_TICKER_DICT['best_ask_px'])
current_ratio = ( ASTER_TOB_PX / EXTEND_TOB_PX ) - 1
alpha_model_ratio = EXTEND.buy_ratio
alpha_signal: bool = current_ratio < EXTEND.buy_ratio
else:
ASTER_TGT_NOTIONAL = ALPHA_TGT_NOTIONAL_FINAL
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'])
current_ratio = ( ( ASTER_TOB_PX / EXTEND_TOB_PX ) - 1 ) * -1
alpha_model_ratio = ASTER.buy_ratio
alpha_signal: bool = current_ratio > ASTER.buy_ratio
else:
ASTER_TOB_PX = float(ASTER_TICKER_DICT['best_ask_px'])
EXTEND_TOB_PX = float(EXTENDED_TICKER_DICT['best_bid_px'])
current_ratio = ( ( ASTER_TOB_PX / EXTEND_TOB_PX ) - 1 ) * -1
alpha_model_ratio = ASTER.buy_ratio
alpha_signal: bool = current_ratio < ASTER.buy_ratio
EXTEND_TGT_NOTIONAL = ASTER.notional_position * -1
ASTER_TGT_TAIL = structs.Current_Previous_Value( (ASTER_TGT_NOTIONAL - ( float(ASTER.notional_position) + float(ASTER.unrealized_pnl) )) )
# ASTER_TGT_TAIL.value = ASTER_TGT_NOTIONAL - ( float(ASTER.notional_position) + float(ASTER.unrealized_pnl) )
EXTEND_TGT_TAIL = structs.Current_Previous_Value( (EXTEND_TGT_NOTIONAL - ( float(EXTEND.notional_position) )) )
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.value) / 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.value) / 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)
### Define if Orderable ###
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.value))) > 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.value))) > 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_FINAL == 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_FINAL == 0.00:
# logging.info('* Trying to flatten small Extend balance, was originally not orderable.')
# EXTEND_TGT_TAIL_ORDERABLE = True
if at_notional_target.is_unlocked:
at_notional_target.value = not(ASTER_TGT_TAIL_ORDERABLE) and not(EXTEND_TGT_TAIL_ORDERABLE)
if at_notional_target.value:
at_notional_target._unlock_func = unlock_notional_tgt
at_notional_target.lock()
continue
### Check if Currently Hedged ###
# Hedge_Ratio = abs( ( EXTEND.notional_position + ASTER.notional_position ) / max([ASTER.notional_position, 0.01]) ) * 100
# Currently_Hedged = Hedge_Ratio < 1.00
### Logging ###
def print_summary(use_logging: bool = False):
OUT: Any = logging.info if use_logging else print
# ASTER: [ Available Collateral: {ASTER_AVAIL_COLLATERAL:.4f} ] | EXTEND: [ Available Collateral: {EXTEND_AVAIL_COLLATERAL:.4f} ]
OUT(f'''
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}
MKT : Aster: {ASTER.symbol:<10} (best: {best_symbol_by_exchange_aster.symbol}) | Extend: {EXTEND.symbol:<10} (best: {best_symbol_by_exchange_extend.symbol})
{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}]
ALPHA SIGNAL: {alpha_signal}; Current {current_ratio:.4f} [{current_ratio*10_000:.2f}scl] {">" if ALPHA_CARRY_SIDE=='BUY' else "<"} Model {alpha_model_ratio:.4f} [{alpha_model_ratio*10_000:.2f}scl]
TGT NOTIONAL: $ {abs(ALPHA_TGT_NOTIONAL_FINAL):.2f}; Flatten Open Positions Flag? {ALGO_CONFIG.Overrides.Flatten_Open_Positions}; Opportunistic? {ALGO_CONFIG.Overrides.Flatten_Open_Positions_Opportunistic}
AT TARGET? : {at_notional_target.value}; is_locked?: {at_notional_target.is_locked}
>5min Fund? : {( sec_until_funding > ( 60*5 ) )}
ASTER : {ASTER.notional_position:.4f} -> {ASTER_TGT_NOTIONAL:.2f} [ Remain: {ASTER_TGT_TAIL.value:.4f} ] | EXTEND: {EXTEND.notional_position:.4f} -> {EXTEND_TGT_NOTIONAL:.2f} [ Remain: {EXTEND_TGT_TAIL.value:.4f} ]
ASTER : {ASTER_TGT_NOTIONAL:.2f} - {ASTER.notional_position:.2f} + {ASTER.unrealized_pnl:.2f} = {ASTER_TGT_TAIL.value:2f} | EXTEND: {EXTEND_TGT_NOTIONAL:.2f} - {EXTEND.notional_position:.2f} + {EXTEND.unrealized_pnl:.2f} = {EXTEND_TGT_TAIL.value: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} ]
--- ASTER OPEN ORDERS ---
{ASTER_OPEN_ORDERS}
--- EXTEND OPEN ORDERS ---
{EXTEND_OPEN_ORDERS}
''')
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)
### Define Ordering Logic ###
'''
Notes
- handle increasing vs flattening
- if increasing, set not reduce only
- if flattening, set as reduce only and make sure allowed to trade below min notional, and qty calc should be exact
- handle opportunistic vs immediate
- handle cancel-replace manually for aster and sometimes manually for extend (e.g. cant change certain things on an existing order)
- gracefully handle err responses (well known err codes e.g.) and response errors (e.g. json fails to parse)
'''
async def cancel_aster_order(open_order_id: str):
global ASTER_OPEN_ORDERS
start = time.time()
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)
else:
logging.warning(f'ASTER ORDER FAILED TO CANCEL DURING CR ({open_order_id}): RESP {cr}')
logging.info(f'TIMING - cancel_aster_order: {(time.time() - start)*1000:.2f}')
async def post_aster_order(
symbol: str,
side: str,
qty: Decimal,
price: Decimal,
reduceOnly: bool,
postOnly: bool
):
global ASTER_OPEN_ORDERS
global Just_Rejected_Or_Expired
if postOnly:
timeInForce = 'GTX'
else:
timeInForce = 'GTC'
post_order = {
"url": "/fapi/v3/order",
"method": "POST",
"params": {
'symbol': symbol,
'side': side,
'type': 'LIMIT',
'timeInForce': timeInForce,
'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()
async def cancel_extend_order(order_id: str):
r = EXTEND_CLIENT.orders.cancel_order(order_id=order_id)
r = dict(r)
if r.get('status', None) == 'OK':
logging.info(f'EXTEND ORDER CANCELLED: {order_id}')
else:
logging.warning(f'EXTEND ORDER FAILED TO CANCEL DURING CR ({order_id}): RESP {r}')
async def post_extend_order(
symbol: str,
side: str,
qty: Decimal,
price: Decimal,
reduceOnly: bool,
postOnly: bool,
cxl_prev_order_id: str | None = None,
):
global EXTEND_OPEN_ORDERS
global Just_Rejected_Or_Expired
side = OrderSide.BUY if side == 'BUY' else OrderSide.SELL
taker_fee = Decimal("0.00025")
try:
order_resp: WrappedApiResponse[PlacedOrderModel] = await EXTEND_CLIENT.place_order(
market_name=symbol,
amount_of_synthetic=qty,
price=price,
side=side,
taker_fee=taker_fee,
previous_order_id=cxl_prev_order_id,
post_only=postOnly,
reduce_only=reduceOnly
)
except Exception as e:
logging.critical(e)
order_resp_dict = dict(order_resp)
if order_resp_dict.get('status', None) == 'ERROR':
if order_resp_dict['error']['code']==1142:
logging.info('Cant find edit order for Extend, skipping cancel.')
else:
logging.critical(f'*** Extend Order Response Abnormal: {order_resp};')
await kill_algo()
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()
### ASTER
if ALGO_CONFIG.Overrides.Allow_Ordering_Aster and ASTER_TGT_TAIL_ORDERABLE: # Tier 1 Overrides
if alpha_signal or ALGO_CONFIG.Overrides.Flatten_Open_Positions: # Tier 2 Overrides / Alpha
skip = False
side = 'BUY' if ASTER_TGT_TAIL_BASE_QTY > 0.00 else 'SELL'
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: # Cancel Open Order?
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',0)) if ASTER_OPEN_ORDERS[0].get('price') is not None else float(ASTER_OPEN_ORDERS[0]['original_price'])
open_order_dict = dict(ASTER_OPEN_ORDERS[0])
open_order_id = str(open_order_dict['order_id'])
open_order_px = float(open_order_dict['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 ALGO_CONFIG.Logging.Print_Summary_Each_Loop:
print('ASTER OPEN ORDER NO PX CHG; SKIPPING')
skip = True
else:
await cancel_aster_order(open_order_id) # ty:ignore[invalid-argument-type]
if ASTER_TGT_TAIL_BASE_QTY == 0.00:
logging.info('ASTER TRYNG TO ORDER 0.00 BASE QTY, SKIPPING')
skip = True
if not skip:
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
await post_aster_order(
symbol=ASTER.symbol,
side=side,
qty=qty,
price=price,
reduceOnly=reduceOnly,
postOnly=True,
)
else:
pass
elif not(ASTER_TGT_TAIL_ORDERABLE) and ASTER_OPEN_ORDERS:
logging.info('ASTER HAS NO TAIL BUT OPEN ORDERS - CANCELLING OPEN ORDERS')
await extend_cancel_all_orders()
### EXTEND ###
if ALGO_CONFIG.Overrides.Allow_Ordering_Extend and EXTEND_TGT_TAIL_ORDERABLE: # Tier 1 Overrides
if alpha_signal or ALGO_CONFIG.Overrides.Flatten_Open_Positions: # Tier 2 Overrides / Alpha
skip = False
side = 'BUY' if EXTEND_TGT_TAIL_BASE_QTY > 0.00 else 'SELL'
qty = Decimal(value=str(abs(EXTEND_TGT_TAIL_BASE_QTY)))
price = EXTEND_TOB_PX - ( float(EXTEND.min_price)*int(ALGO_CONFIG.Config.Price_Worsener_Extend) ) if side == 'BUY' else EXTEND_TOB_PX + ( float(EXTEND.min_price)*int(ALGO_CONFIG.Config.Price_Worsener_Extend) ) # ty:ignore[invalid-assignment]
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} + {float(EXTEND_TGT_TAIL_BASE_QTY)*float(price)} (qty: {float(EXTEND_TGT_TAIL_BASE_QTY):.2f}; px: {float(price):.2f})')
await kill_algo()
if EXTEND_OPEN_ORDERS: # Cancel Open Order?
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'])
min_price = EXTEND.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 ALGO_CONFIG.Logging.Print_Summary_Each_Loop:
print('EXTEND OPEN ORDER NO PX CHG; SKIPPING')
skip = True
else:
open_order_id = None
if EXTEND_TGT_TAIL_BASE_QTY == 0.00:
logging.info('EXTEND TRYNG TO ORDER 0.00 BASE QTY, SKIPPING')
skip = True
if not skip:
min_price = EXTEND.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('EXTEND TRYNG TO ORDER with A PRICE OF 0.00, SKIPPING')
continue
if qty >= EXTEND.min_order_size and ((qty*price) > EXTEND.min_notional):
reduceOnly = False
else:
reduceOnly = True
await post_extend_order(
symbol=EXTEND.symbol,
side=side,
qty=qty,
price=price,
reduceOnly=reduceOnly,
postOnly=True,
cxl_prev_order_id=open_order_id
)
else:
pass
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()
### Continue immediately or sleep ###
if ASTER_OPEN_ORDERS or EXTEND_OPEN_ORDERS:
if ALGO_CONFIG.Logging.Print_Summary_Each_Loop:
print(f'_____ Open Orders _____ (Algo Engine ms: {(time.time() - loop_start)*1000:.2f}); Continuing...')
continue
else:
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}')
time.sleep(ALGO_CONFIG.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
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['buy_ratio_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['buy_ratio_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())