Files
Funding_Rate/main_v0.py
2026-04-25 23:43:28 +00:00

771 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
### 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'
### CONSTANTS ###
ASTER_ALLOW_ORDERING: bool = False
EXTEND_ALLOW_ORDERING: bool = False
LOOP_SLEEP_SEC = 1
PRICE_WORSENER_ASTER = 0.00
PRICE_WORSENER_EXTEND = 0.0
MIN_TIME_TO_FUNDING: int = 1000 * 60 * 7 # 5 minutes.
ASTER_LH_ASSET: str = 'ETH'
ASTER_RH_ASSET: str = 'USDT'
ASTER_TICKER: str = ASTER_LH_ASSET + ASTER_RH_ASSET
EXTEND_LH_ASSET: str = 'ETH'
EXTEND_RH_ASSET: str = 'USD'
EXTEND_TICKER: str = EXTEND_LH_ASSET + '-' + EXTEND_RH_ASSET
TARGET_OPEN_CASH_POSITION: float = 10 # Each side (alpha and hedge)
### GLOBALS ###
ASTER_MULT = 150
EXTEND_MULT = 50
MAX_TARGET_NOTIONAL = min([ASTER_MULT, EXTEND_MULT]) * TARGET_OPEN_CASH_POSITION
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 = []
@dataclass(kw_only=True)
class Valkey_Stream:
channel: str
data: Any = None
none_fill: Any = None
async def update(self):
r = VAL_KEY.get(self.channel)
self.data = json.loads(r) if r is not None else self.none_fill
@dataclass(kw_only=True)
class Position:
market: str
notional: float
qty: float
@dataclass(kw_only=True)
class Open_Positions:
Valkey: Valkey_Stream
Positions: list[Position] = field(default_factory = list)
async def update(self) -> None:
self.Valkey = await self.Valkey.update()
### Collateral ###
@dataclass(kw_only=True)
class Asset:
symbol: str
balance: float
# min_order_qty: float
@dataclass(kw_only=True)
class Collateral:
Valkey: Valkey_Stream
# Last_Updated_Ts_Ms: int
# Last_Pulled_Ts_Ms: int
Assets: list[Asset] = field(default_factory = list)
async def update(self) -> None:
self.Valkey = await self.Valkey.update()
### Orders ###
@dataclass(kw_only=True)
class Order:
symbol: str
order_id: str
client_order_id: str
side: str
order_type: str
original_qty: float
original_price: float
order_status: str
last_filled_qty: float
last_filled_price: float
commission: float
trade_is_maker: bool
@dataclass(kw_only=True)
class Order_Updates:
# Last_Updated_Ts_Ms: int
# Last_Pulled_Ts_Ms: int
Valkey: Valkey_Stream
Orders: list[Order] = field(default_factory = list)
async def update(self) -> None:
self.Valkey = await self.Valkey.update()
### Funding Rate ###
@dataclass(kw_only=True)
class Funding_Rate:
# Last_Updated_Ts_Ms: int
# Last_Pulled_Ts_Ms: int
Valkey: Valkey_Stream
timestamp_arrival: int
timestamp_msg: int
symbol: str
funding_rate: float
next_funding_time_ts_ms: int
mark_price: float
index_price: float
estimated_settle_price: float
async def update(self) -> None:
self.Valkey = await self.Valkey.update()
### Markets Info ###
@dataclass(kw_only=True)
class Market:
symbol: str
min_order_qty: float
@dataclass(kw_only=True)
class Markets_Details:
Markets: list[Market] = field(default_factory=list)
### Exchanges ###
@dataclass(kw_only=True)
class Perpetual_Exchange:
Order_Updates: Order_Updates
Position_Updates: Open_Positions
Collateral_Updates: Collateral
Funding_Rate: Funding_Rate
Markets: Markets_Details
mult: int
lh_asset: str
rh_asset: str
symbol_asset_separator: str = ''
symbol: str
async def update(self):
await self.Collateral_Updates.update()
await self.Order_Updates.update()
await self.Position_Updates.update()
await self.Funding_Rate.update()
def __post_init__(self) -> None:
self.symbol = f'{self.lh_asset.upper()}{self.symbol_asset_separator}{self.rh_asset.upper()}'
@dataclass(kw_only=True)
class Aster(Perpetual_Exchange):
name: str = 'Aster'
lh_asset: str = 'ETH'
rh_asset: str = 'USDT'
def __post_init__(self):
super().__post_init__()
self.Order_Updates = Order_Updates(Valkey=Valkey_Stream(channel = 'fr_aster_user_balances', none_fills = []))
self.Collateral_Updates = Collateral(Valkey=Valkey_Stream(channel = 'fr_aster_user_orders', none_fills = []))
self.Position_Updates = Open_Positions(Valkey=Valkey_Stream(channel = 'fr_aster_user_positions', none_fills = []))
self.Funding_Rate - Funding_Rate(Valkey=Valkey_Stream(channel = 'fund_rate_aster', none_fills = None))
@dataclass(kw_only=True)
class Extend(Perpetual_Exchange):
name: str = 'Extended'
lh_asset: str = 'ETH'
rh_asset: str = 'USD'
symbol_asset_separator: str = '-'
def __post_init__(self):
super().__post_init__()
self.Order_Updates = Order_Updates(Valkey=Valkey_Stream(channel = 'fr_aster_user_balances', none_fills = []))
self.Collateral_Updates = Collateral(Valkey=Valkey_Stream(channel = 'fr_aster_user_orders', none_fills = []))
self.Position_Updates = Open_Positions(Valkey=Valkey_Stream(channel = 'fr_aster_user_positions', none_fills = []))
self.Funding_Rate - Funding_Rate(Valkey=Valkey_Stream(channel = 'fund_rate_aster', none_fills = None))
# EXCHANGES: list = [ Aster(), Extend() ]
### FLAGS ###
@dataclass(kw_only=True)
class Flags:
LIQUIDATE_POS_AND_KILL_ALGO_FLAG: bool = False
NET_FUNDING_IS_ZERO: bool = False
Flags = 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_TICKER,
}
}
resp = await aster_auth.post_authenticated_url(fut_acct_positionRisk)
d = [x for x in resp if x.get('symbol', None) == ASTER_TICKER][0]
if len(d) < 1:
logging.info(f'BAD NOTIONAL - ASTER CHANGE: Empty d: {d}; resp: {resp}')
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) > 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}')
kill_algo()
if ASTER_NOTIONAL_POSITION != previous_notional_position:
logging.info(f'ASTER NOTIONAL CHANGE: {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]
pos_dict = pos_dict[0]
unrealized_pnl = pos_dict.get('unrealised_pnl', 0)
previous_notional_position = EXTEND_NOTIONAL_POSITION
EXTEND_NOTIONAL_POSITION = float(pos_dict.get('value', 0)) - float(unrealized_pnl)
EXTEND_MULT = pos_dict.get('leverage', EXTEND_MULT)
if EXTEND_NOTIONAL_POSITION != previous_notional_position:
logging.info(f'EXTEND NOTIONAL CHANGE: {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')
### ROUTES ###
# async def aster_remainder_route():
# # Check open orders...cancel replace or new order?
# # Check collateral to confirm you have enough money to trade
# # if CR, what should be the new price? has it changed? maybe no action needed? how long has it been working?
# # if not enough collateral then need to liquidate and kill algo - flip flag
# # if good to order, then create and post order. ADD to LOCAL OPEN ORDERS LIST
# pass
# async def extend_remainder_route():
# pass
### ALGO LOOP ###
async def run_algo():
try:
while True:
loop_start = time.time()
print('__________Start___________')
### 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))
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))
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)
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.get('orderId')
order_orig_status = o['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}')
ASTER_PAYOUT_DIRECTION_STR = 'LONG PAYS SHORT' if ASTER_FUND_RATE > 0 else 'SHORT PAYS LONG'
EXTEND_PAYOUT_DIRECTION_STR = 'LONG PAYS SHORT' if EXTEND_FUND_RATE > 0 else 'SHORT PAYS LONG'
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 = MAX_TARGET_NOTIONAL
else:
ALPHA_CARRY_SIDE = 'SELL'
ALPHA_TGT_NOTIONAL = 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)
ASTER_TGT_TAIL_ORDERABLE = abs(ASTER_TGT_TAIL_BASE_QTY) >= ASTER_MIN_ORDER_QTY
EXTEND_TGT_TAIL_ORDERABLE = abs(EXTEND_TGT_TAIL_BASE_QTY) >= EXTEND_MIN_ORDER_QTY
print(f'''
{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: {ASTER_PAYOUT_DIRECTION_STR} | EXTEND: {EXTEND_PAYOUT_DIRECTION_STR}
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: $ {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} > {ASTER_MIN_ORDER_QTY:.4f} min [ Order: {ASTER_TGT_TAIL_ORDERABLE} ] | EXTEND: {EXTEND_TGT_TAIL_BASE_QTY:.4f} > {EXTEND_MIN_ORDER_QTY:.4f} min [ Order: {EXTEND_TGT_TAIL_ORDERABLE} ]
--- ASTER OPEN ORDERS ---
{ASTER_OPEN_ORDERS}
--- EXTEND OPEN ORDERS ---
{EXTEND_OPEN_ORDERS}
''')
### ROUTES ###
# ASTER
if ASTER_TGT_TAIL_ORDERABLE and ASTER_ALLOW_ORDERING:
symbol = ASTER_TICKER
side = 'BUY' if ASTER_TGT_TAIL_BASE_QTY > 0.00 else 'SELL'
qty = str(abs(ASTER_TGT_TAIL_BASE_QTY))
price = ASTER_TOB_PX - PRICE_WORSENER_ASTER if side == 'BUY' else ASTER_TOB_PX + PRICE_WORSENER_ASTER
if abs(abs(float(ASTER_TGT_TAIL_BASE_QTY))*float(price)) + abs(ASTER_NOTIONAL_POSITION) > MAX_TARGET_NOTIONAL*1.01:
pass
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 aster_remainder_route()
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_TICKER,
'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}')
else:
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 EXTEND_ALLOW_ORDERING:
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 - PRICE_WORSENER_EXTEND if side == 'BUY' else EXTEND_TOB_PX + PRICE_WORSENER_EXTEND
if abs(float(EXTEND_TGT_TAIL_BASE_QTY)*float(price)) + abs(float(EXTEND_NOTIONAL_POSITION)) > 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})')
pass
# await extend_remainder_route()
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'])
place_order = True
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:
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,
)
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)
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}')
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(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
_, 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')
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())