added extend symbol change for ws

This commit is contained in:
2026-04-30 04:32:49 +00:00
parent dc3409ac40
commit 1ac0909c21
20 changed files with 28960 additions and 1221 deletions

136
main.py
View File

@@ -1,3 +1,5 @@
from x10.utils.http import WrappedApiResponse
from x10.perpetual.trading_client.trading_client import PerpetualTradingClient
import asyncio
import json
import logging
@@ -8,7 +10,7 @@ import traceback
from datetime import datetime, timezone
from decimal import ROUND_DOWN, Decimal
from typing import AsyncContextManager
from dataclasses import dataclass, asdict
from typing import Any
import numpy as np
import pandas as pd
@@ -19,24 +21,24 @@ 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
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
### Database ###
EXTEND_CLIENT = None
CON: AsyncContextManager | None = None
VAL_KEY = None
### Clients ###
EXTEND_CLIENT: PerpetualTradingClient
CON: AsyncContextManager
VAL_KEY: valkey.Valkey
### Logging ###
load_dotenv()
LOG_FILEPATH: str = os.getenv("LOGS_PATH") + '/Fund_Rate_Algo.log'
LOG_FILEPATH: str = f'{os.getenv(key="LOGS_PATH")}/Fund_Rate_Algo.log'
### Algo Config ###
ALGO_CONFIG: structs.Algo_Config | None = None
ALGO_CONFIG: structs.Algo_Config
MIN_TIME_TO_FUNDING: int
### EXCHANGES ###
@@ -56,7 +58,6 @@ EXTEND = structs.Perpetual_Exchange(
### GLOBALS ###
Last_Aster_Fill_Time_Ts: float = 0.00
Just_Rejected_Or_Expired: bool = False
Best_Symbol_by_Exchange: dict = {}
# ASTER_MULT = 150
# EXTEND_MULT = 50
@@ -122,7 +123,7 @@ async def get_aster_collateral():
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):
async def get_aster_notional_position(resp: list | None = None):
global ASTER_NOTIONAL_OBJ
global ASTER_NOTIONAL_POSITION
global ASTER_UNREALIZED_PNL
@@ -131,14 +132,14 @@ async def get_aster_notional_position(resp: dict | None = None):
previous_notional_obj = ASTER_NOTIONAL_OBJ
if not resp:
fut_acct_positionRisk = {
fut_acct_positionRisk: dict = {
"url": "/fapi/v3/positionRisk",
"method": "GET",
"params": {
'symbol': ASTER.symbol,
}
}
resp = await aster_auth.post_authenticated_url(fut_acct_positionRisk)
resp: list = await aster_auth.post_authenticated_url(req=fut_acct_positionRisk) # ty:ignore[invalid-assignment]
d = [x for x in resp if x.get('symbol', None) == ASTER.symbol][0]
d['timestamp_arrival'] = round(datetime.now().timestamp()*1000)
else:
@@ -168,8 +169,8 @@ async def get_aster_notional_position(resp: dict | None = None):
if abs(ASTER_NOTIONAL_POSITION) > ALGO_CONFIG.Config.Max_Target_Notional*ALGO_CONFIG.Config.Max_Order_Over_Notional_Ratio:
logging.info(f'BAD NOTIONAL - ASTER CHANGE: {previous_notional_position} -> {ASTER_NOTIONAL_POSITION}; UR PNL: {ASTER_UNREALIZED_PNL}; MULT: {ASTER.mult}; d: {d}; resp: {resp}')
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)}')
if ASTER_NOTIONAL_POSITION != previous_notional_position:
logging.info(f'ASTER NOTIONAL CHANGE: {previous_notional_position:.2f} -> {ASTER_NOTIONAL_POSITION:.2f}; UR PNL: {ASTER_UNREALIZED_PNL:.2f}; MULT: {ASTER.mult:.0f}; resp: {bool(resp)}')
async def get_extend_collateral():
global EXTEND_AVAIL_COLLATERAL
@@ -177,7 +178,7 @@ async def get_extend_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):
async def get_extend_notional(resp: list | None = None):
global EXTEND_NOTIONAL_OBJ
global EXTEND_NOTIONAL_POSITION
global EXTEND_UNREALIZED_PNL
@@ -238,15 +239,15 @@ async def get_extend_notional(resp: dict | None = None):
async def get_aster_exch_info():
global ASTER_MIN_ORDER_QTY
fut_acct_exchangeInfo = {
fut_acct_exchangeInfo: dict = {
"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]
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) == 'ETHUSDT'][0]
f: dict = [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():
@@ -290,7 +291,7 @@ async def run_algo():
global EXTEND_OPEN_ORDERS
global Last_Aster_Fill_Time_Ts
global Just_Rejected_Or_Expired
global Best_Symbol_by_Exchange
# global Best_Symbol_by_Exchange
try:
while True:
@@ -298,20 +299,22 @@ async def run_algo():
# print('__________Start___________')
### ALGO CONIFG ###
ALGO_CONFIG = json.loads(VAL_KEY.get('fr_orchestrator_output'))
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 = json.loads(VAL_KEY.get('fr_engine_best_fund_rate_output'))
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 = json.loads(VAL_KEY.get('fund_rate_aster'))
EXTENDED_FUND_RATE_DICT = json.loads(VAL_KEY.get('fund_rate_extended'))
ASTER_FUND_RATE_DICT = json.loads(VAL_KEY.get('fund_rate_aster')) # ty:ignore[invalid-argument-type]
EXTENDED_FUND_RATE_DICT = json.loads(VAL_KEY.get('fund_rate_extended')) # ty:ignore[invalid-argument-type]
ASTER_FUND_RATE = float(ASTER_FUND_RATE_DICT.get('funding_rate', 0))
EXTEND_FUND_RATE = float(EXTENDED_FUND_RATE_DICT.get('funding_rate', 0))
ASTER_FUND_RATE: float = float(ASTER_FUND_RATE_DICT.get('funding_rate', 0))
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
@@ -321,26 +324,26 @@ async def run_algo():
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'))
ASTER_TICKER_DICT = json.loads(VAL_KEY.get('fut_ticker_aster')) # ty:ignore[invalid-argument-type]
EXTENDED_TICKER_DICT = json.loads(VAL_KEY.get('fut_ticker_extended')) # ty:ignore[invalid-argument-type]
### 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 []
ASTER_WS_COLLATERAL_UPDATES = json.loads(ASTER_WS_COLLATERAL_UPDATES) if ASTER_WS_COLLATERAL_UPDATES is not None else [] # ty:ignore[invalid-argument-type]
EXTEND_WS_COLLATERAL_UPDATES = VAL_KEY.get('fr_extended_user_positions')
EXTEND_WS_COLLATERAL_UPDATES = json.loads(EXTEND_WS_COLLATERAL_UPDATES) if EXTEND_WS_COLLATERAL_UPDATES is not None else []
EXTEND_WS_COLLATERAL_UPDATES = json.loads(EXTEND_WS_COLLATERAL_UPDATES) if EXTEND_WS_COLLATERAL_UPDATES is not None else [] # ty:ignore[invalid-argument-type]
### Manage Local Notionals Using Updates from WS ###
ASTER_WS_POS_UPDATES = 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 []
ASTER_WS_POS_UPDATES: Any = VAL_KEY.get(name='fr_aster_user_positions')
ASTER_WS_POS_UPDATES: list = json.loads(s=ASTER_WS_POS_UPDATES) if ASTER_WS_POS_UPDATES is not None else []
EXTEND_WS_POS_UPDATES: Any = VAL_KEY.get('fr_extended_user_positions')
EXTEND_WS_POS_UPDATES: list = json.loads(EXTEND_WS_POS_UPDATES) if EXTEND_WS_POS_UPDATES is not None else []
### Manage Local Orders Using Updates from WS ###
ASTER_WS_ORDER_UPDATES = 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 []
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:
@@ -409,9 +412,9 @@ async def run_algo():
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()
order_update: dict = order_update[0]
order_update_status: str = order_update['status']
order_status_changed: bool = order_orig_status.upper() != order_update_status.upper()
if order_status_changed:
logging.info(f'EXTEND ORDER ({order_id}): {order_orig_status} -> {order_update_status}')
@@ -436,14 +439,18 @@ async def run_algo():
logging.critical(f'EXTEND ORDER STATUS CHG TO UNEXPECTED VALUE, KILLING... ({order_id}): {order_orig_status} -> {order_update_status}')
# if Best_Symbol_by_Exchange['symbol_aster'] != ASTER.symbol:
# if abs( ASTER_NOTIONAL_POSITION ) > 0 or abs( EXTEND_NOTIONAL_POSITION ) > 0:
# if (best_symbol_by_exchange_aster.symbol != ASTER.symbol) or (best_symbol_by_exchange_extend.symbol != EXTEND.symbol):
# if abs( ASTER_NOTIONAL_POSITION ) > 0.00 or abs( EXTEND_NOTIONAL_POSITION ) > 0.00:
# print('Symbol switch - Flattening Positions')
# ALGO_CONFIG.Overrides.Flatten_Open_Positions = True
# else:
# print(f'Balances Flattened - Updating to Trade New Symbol: ASTER.symbol -> {Best_Symbol_by_Exchange['symbol_aster']}')
# # ASTER.symbol = Best_Symbol_by_Exchange['symbol_aster']
# # EXTEND.symbol = Best_Symbol_by_Exchange['symbol_extended']
# print('Balances Flattened - Updating to Trade New Symbols:')
# print(f' ASTER.symbol -> {best_symbol_by_exchange_aster.symbol}')
# print(f' EXTEND.symbol -> {best_symbol_by_exchange_extend.symbol}')
# ALGO_CONFIG.Overrides.Flatten_Open_Positions = False
# ASTER = best_symbol_by_exchange_aster
# EXTEND = best_symbol_by_exchange_extend
# VAL_KEY.set(name='fr_algo_working_symbol', value=json.dumps(obj={'ASTER': asdict(obj=ASTER), 'EXTEND': asdict(obj=EXTEND)}))
@@ -542,7 +549,7 @@ async def run_algo():
Currently_Hedged = Hedge_Ratio < 1.00
def print_summary(use_logging: bool = False):
OUT: print | logging.info = logging.info if use_logging else print
OUT: Any = logging.info if use_logging else print
OUT(f'''
LOOP SLEEP (SEC): {ALGO_CONFIG.Config.Loop_Sleep_Sec}
@@ -566,6 +573,7 @@ async def run_algo():
FEES : TAKER: {0.0002:.2%}; Expected Alpha w Taker = {Expected_Alpha_Net_FR-0.0002:.6f} [w/o FR: {Expected_Alpha_w_Taker:.6f}]
HEDGE: {Hedge_Ratio:.2f}% <= {1:.2f}%: {Currently_Hedged} [{EXTEND_NOTIONAL_POSITION:.2f} / {ASTER_NOTIONAL_POSITION:.2f}]
MKT : Aster: {ASTER.symbol} (best: {best_symbol_by_exchange_aster.symbol}) | Extend: {ASTER.symbol} (best: {best_symbol_by_exchange_extend.symbol})
--- ASTER OPEN ORDERS ---
{ASTER_OPEN_ORDERS}
@@ -583,8 +591,8 @@ async def run_algo():
### ROUTES ###
# MIN_EXPECTED_ALPHA_TO_TRADE = 0.0001
MIN_EXPECTED_ALPHA_TO_TRADE = -0.000001
MIN_EXPECTED_ALPHA_TO_TRADE = 0.0001
# MIN_EXPECTED_ALPHA_TO_TRADE = -0.000001
# ALPHA RATIO CHECK
if not( ( Expected_Alpha_Net_FR_w_Taker > MIN_EXPECTED_ALPHA_TO_TRADE ) or ( ASTER_OPEN_ORDERS or EXTEND_OPEN_ORDERS or Just_Rejected_Or_Expired or ALGO_CONFIG.Overrides.Flatten_Open_Positions) ):
if ALGO_CONFIG.Logging.Print_Summary_Each_Loop:
@@ -614,7 +622,7 @@ async def run_algo():
logging.info('ASTER OPEN ORDER NO PX CHG; SKIPPING')
place_order = False
else:
cancel_order = {
cancel_order: dict = {
"url": "/fapi/v3/order",
"method": "DELETE",
"params": {
@@ -622,7 +630,7 @@ async def run_algo():
'orderId': open_order_id,
}
}
cr = await aster_auth.post_authenticated_url(cancel_order)
cr: dict = await aster_auth.post_authenticated_url(cancel_order) # ty:ignore[invalid-assignment]
if cr.get('status', None) == 'CANCELED':
ASTER_OPEN_ORDERS.pop(0)
place_order = True
@@ -637,7 +645,7 @@ async def run_algo():
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)
price: Decimal = Decimal(str(price)).quantize(Decimal(str(0.01)), rounding=ROUND_DOWN)
post_order = {
"url": "/fapi/v3/order",
"method": "POST",
@@ -650,7 +658,7 @@ async def run_algo():
'price': price,
}
}
order_resp = await aster_auth.post_authenticated_url(post_order)
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']
@@ -672,14 +680,14 @@ async def run_algo():
Time_Since_Last_Aster_Fill_ms = ( datetime.now().timestamp()*1000 ) - Last_Aster_Fill_Time_Ts
if Time_Since_Last_Aster_Fill_ms > ( 1000 * ALGO_CONFIG.Config.Switch_To_Taker_Seconds ): # Change to allow taker orders if its been more than x seconds
post_only = False
price = EXTEND_TOB_PX - ALGO_CONFIG.Config.Price_Worsener_Extend if side == 'BUY' else EXTEND_TOB_PX + ALGO_CONFIG.Config.Price_Worsener_Extend
price: Decimal = Decimal(value=str(EXTEND_TOB_PX - ALGO_CONFIG.Config.Price_Worsener_Extend if side == 'BUY' else EXTEND_TOB_PX + ALGO_CONFIG.Config.Price_Worsener_Extend)).quantize(Decimal(str(0.1)), rounding=ROUND_DOWN)
else:
post_only = True
price = EXTEND_TOB_PX
price: Decimal = Decimal(value=str(EXTEND_TOB_PX)).quantize(Decimal(str(0.1)), rounding=ROUND_DOWN)
symbol = EXTEND.symbol
side = OrderSide.BUY if EXTEND_TGT_TAIL_BASE_QTY > 0.00 else OrderSide.SELL
qty = Decimal(str(abs(EXTEND_TGT_TAIL_BASE_QTY)))
qty = Decimal(value=str(abs(EXTEND_TGT_TAIL_BASE_QTY)))
if abs( ( float(EXTEND_TGT_TAIL_BASE_QTY)*float(price) ) + EXTEND_NOTIONAL_POSITION ) > ALGO_CONFIG.Config.Max_Target_Notional*ALGO_CONFIG.Config.Max_Order_Over_Notional_Ratio:
logging.info(f'TRYING TO ORDER OVER MAX NOTIOANL - EXTEND: {EXTEND_NOTIONAL_POSITION:.2f} + {float(EXTEND_TGT_TAIL_BASE_QTY)*float(price):.2f} (qty: {float(EXTEND_TGT_TAIL_BASE_QTY):.2f}; px: {float(price):.2f})')
@@ -703,13 +711,13 @@ async def run_algo():
open_order_px = 0
place_order = True
if place_order:
price = Decimal(str(price)).quantize(Decimal(str(0.1)), rounding=ROUND_DOWN)
price: Decimal = Decimal(str(price)).quantize(Decimal(str(0.1)), rounding=ROUND_DOWN)
if round(open_order_px - float(price), 2) == 0.00:
logging.info('EXTEND OPEN ORDER NO PX CHG; SKIPPING')
else:
try:
taker_fee = taker_fee=Decimal("0.00000") if post_only else Decimal("0.00025")
order_resp = await EXTEND_CLIENT.place_order(
order_resp: WrappedApiResponse[PlacedOrderModel] = await EXTEND_CLIENT.place_order(
market_name=symbol,
amount_of_synthetic=qty,
price=price,
@@ -780,16 +788,14 @@ async def main():
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:
with open('algo_config.json', mode='r', encoding='utf-8') as file:
ALGO_CONFIG = json.load(file)
ALGO_CONFIG = structs.Algo_Config(**ALGO_CONFIG)
# 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.Config.Max_Target_Notional = float(min([ASTER.mult, EXTEND.mult]) * ALGO_CONFIG.Config.Target_Open_Cash_Position)
VAL_KEY.set('fr_orchestrator_output', json.dumps(ALGO_CONFIG.model_dump()))
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 ###