Files
Funding_Rate/main.py
2026-04-27 17:57:58 +00:00

690 lines
34 KiB
Python

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
MIN_TIME_TO_FUNDING: int
### 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_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_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_OBJ
global ASTER_NOTIONAL_POSITION
global ASTER_UNREALIZED_PNL
global ASTER_MULT
previous_notional_obj = ASTER_NOTIONAL_OBJ
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]
d['timestamp_arrival'] = round(datetime.now().timestamp()*1000)
else:
d = [x for x in resp if x.get('symbol', None) == ASTER.symbol][0]
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'])
previous_notional_position = ASTER_NOTIONAL_POSITION
# if not resp: # this can never evaluate
# ASTER_MULT = float(d['leverage'])
# if abs(ASTER_NOTIONAL_POSITION) > ALGO_CONFIG.Max_Target_Notional*1.01:
if abs(ASTER_NOTIONAL_POSITION) > ALGO_CONFIG.Max_Target_Notional*2.01:
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}')
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_UNREALIZED_PNL
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]
EXTEND_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(EXTEND_UNREALIZED_PNL)
EXTEND_MULT = pos_dict.get('leverage', EXTEND_MULT)
if abs(EXTEND_NOTIONAL_POSITION) > ALGO_CONFIG.Max_Target_Notional*2.01:
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} -> {EXTEND_NOTIONAL_POSITION:.2f}; UR PNL: {EXTEND_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 MIN_TIME_TO_FUNDING
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):
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)
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 - ( float(ASTER_NOTIONAL_POSITION) + float(ASTER_UNREALIZED_PNL) )
EXTEND_TGT_TAIL = EXTEND_TGT_NOTIONAL - ( float(EXTEND_NOTIONAL_POSITION) + float(EXTEND_UNREALIZED_PNL) )
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'''
LOOP SLEEP (SEC): {ALGO_CONFIG.Loop_Sleep_Sec}
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:.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} > {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.Log_Summary_Each_Loop:
print_summary(use_logging=True)
if ALGO_CONFIG.Print_Summary_Each_Loop:
print_summary(use_logging=False)
# 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
order_resp['order_status'] = order_resp['status']
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}); Sleeping for sec: {ALGO_CONFIG.Loop_Sleep_Sec}')
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())