2026-04-21 20:22:33 +00:00
|
|
|
import asyncio
|
|
|
|
|
import json
|
|
|
|
|
import logging
|
|
|
|
|
import math
|
|
|
|
|
import os
|
|
|
|
|
import time
|
|
|
|
|
import traceback
|
|
|
|
|
from dataclasses import asdict, dataclass
|
|
|
|
|
from datetime import datetime, timezone
|
|
|
|
|
from typing import AsyncContextManager
|
|
|
|
|
from dotenv import load_dotenv
|
|
|
|
|
|
|
|
|
|
import numpy as np
|
|
|
|
|
import pandas as pd
|
|
|
|
|
import requests
|
|
|
|
|
# import talib
|
|
|
|
|
import valkey
|
|
|
|
|
from sqlalchemy import text
|
|
|
|
|
from sqlalchemy.ext.asyncio import create_async_engine
|
2026-04-23 06:39:51 +00:00
|
|
|
import modules.aster_auth as aster_auth
|
|
|
|
|
import modules.extended_auth as extend_auth
|
2026-04-21 20:22:33 +00:00
|
|
|
|
|
|
|
|
### Database ###
|
2026-04-23 06:39:51 +00:00
|
|
|
EXTEND_CLIENT = None
|
2026-04-21 20:22:33 +00:00
|
|
|
CON: AsyncContextManager | None = None
|
|
|
|
|
VAL_KEY = None
|
|
|
|
|
|
|
|
|
|
### Logging ###
|
|
|
|
|
load_dotenv()
|
|
|
|
|
LOG_FILEPATH: str = os.getenv("LOGS_PATH") + '/Fund_Rate_Algo.log'
|
|
|
|
|
|
2026-04-23 06:39:51 +00:00
|
|
|
### CONSTANTS ###
|
|
|
|
|
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
|
|
|
|
|
ASTER_NOTIONAL_POSITION = 0
|
|
|
|
|
EXTEND_AVAIL_COLLATERAL = 0
|
|
|
|
|
EXTEND_NOTIONAL_POSITION = 0
|
|
|
|
|
|
|
|
|
|
ASTER_OPEN_POSITIONS = []
|
|
|
|
|
EXTEND_OPEN_POSITIONS = []
|
|
|
|
|
|
|
|
|
|
ASTER_OPEN_ORDERS = []
|
|
|
|
|
EXTEND_OPEN_ORDERS = []
|
|
|
|
|
|
|
|
|
|
### FLAGS ###
|
|
|
|
|
LIQUIDATE_POS_AND_KILL_ALGO_FLAG: bool = False
|
|
|
|
|
|
|
|
|
|
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
|
2026-04-21 20:22:33 +00:00
|
|
|
|
|
|
|
|
|
2026-04-23 06:39:51 +00:00
|
|
|
pass
|
|
|
|
|
|
|
|
|
|
async def extend_remainder_route():
|
|
|
|
|
pass
|
|
|
|
|
|
|
|
|
|
async def run_algo():
|
2026-04-21 20:22:33 +00:00
|
|
|
try:
|
|
|
|
|
while True:
|
|
|
|
|
loop_start = time.time()
|
|
|
|
|
print('__________Start___________')
|
|
|
|
|
|
2026-04-23 03:11:52 +00:00
|
|
|
ASTER_FUND_RATE_DICT = json.loads(VAL_KEY.get('fund_rate_aster'))
|
|
|
|
|
ASTER_TICKER_DICT = json.loads(VAL_KEY.get('fut_ticker_aster'))
|
|
|
|
|
# print(f'ASTER FUND RATE: {ASTER_FUND_RATE}')
|
|
|
|
|
# print(f'ASTER TICKER: {ASTER_TICKER}')
|
2026-04-22 05:24:40 +00:00
|
|
|
|
2026-04-23 03:11:52 +00:00
|
|
|
EXTENDED_FUND_RATE_DICT = json.loads(VAL_KEY.get('fund_rate_extended'))
|
|
|
|
|
EXTENDED_TICKER_DICT = json.loads(VAL_KEY.get('fut_ticker_extended'))
|
|
|
|
|
# print(f'EXTENDED FUND RATE: {EXTENDED_FUND_RATE}')
|
|
|
|
|
# print(f'EXTENDED TICKER: {EXTENDED_TICKER}')
|
|
|
|
|
|
|
|
|
|
ASTER_FUND_RATE = float(ASTER_FUND_RATE_DICT.get('funding_rate', 0))
|
|
|
|
|
EXTEND_FUND_RATE = float(EXTENDED_FUND_RATE_DICT.get('funding_rate', 0))
|
2026-04-23 06:39:51 +00:00
|
|
|
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_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'
|
|
|
|
|
|
|
|
|
|
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)
|
|
|
|
|
|
|
|
|
|
if ALPHA_EXCH == 'EXTEND':
|
|
|
|
|
ASTER_TGT_NOTIONAL = ALPHA_TGT_NOTIONAL*-1
|
|
|
|
|
EXTEND_TGT_NOTIONAL = ALPHA_TGT_NOTIONAL
|
|
|
|
|
else:
|
|
|
|
|
ASTER_TGT_NOTIONAL = ALPHA_TGT_NOTIONAL
|
|
|
|
|
EXTEND_TGT_NOTIONAL = ALPHA_TGT_NOTIONAL*-1
|
|
|
|
|
|
|
|
|
|
ASTER_TGT_TAIL = ASTER_TGT_NOTIONAL - ASTER_NOTIONAL_POSITION
|
|
|
|
|
EXTEND_TGT_TAIL = EXTEND_TGT_NOTIONAL - EXTEND_NOTIONAL_POSITION
|
|
|
|
|
|
|
|
|
|
ASTER_TGT_TAIL_ORDERABLE = abs(ASTER_TGT_TAIL) >= ASTER_MIN_ORDER_QTY
|
|
|
|
|
EXTEND_TGT_TAIL_ORDERABLE = abs(EXTEND_TGT_TAIL) >= EXTEND_MIN_ORDER_QTY
|
2026-04-23 03:11:52 +00:00
|
|
|
|
|
|
|
|
print(f'''
|
2026-04-23 06:39:51 +00:00
|
|
|
{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}
|
|
|
|
|
NET FUNDING : {NEXT_NET_FUNDING_RATE:.6%} [{NEXT_NET_FUNDING_RATE*10_000:.2f}bps] [{NEXT_NET_FUNDING_RATE*1_000_000:.0f}pips]
|
|
|
|
|
ALPHA SIDE : {ALPHA_EXCH} [{ALPHA_CARRY_SIDE}]
|
|
|
|
|
|
|
|
|
|
TGT NOTIONAL: $ {MAX_TARGET_NOTIONAL}
|
|
|
|
|
|
|
|
|
|
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} ]
|
|
|
|
|
ASTER: {ASTER_TGT_TAIL:.4f} > {ASTER_MIN_ORDER_QTY:.4f} min [ Order: {ASTER_TGT_TAIL_ORDERABLE} ] | EXTEND: {EXTEND_TGT_TAIL:.4f} > {EXTEND_MIN_ORDER_QTY:.4f} min [ Order: {EXTEND_TGT_TAIL_ORDERABLE} ]
|
|
|
|
|
|
2026-04-23 03:11:52 +00:00
|
|
|
''')
|
2026-04-21 20:22:33 +00:00
|
|
|
|
2026-04-23 06:39:51 +00:00
|
|
|
### SCAN VALKEY USER FEEDS FOR BALANCE UPDATES ###
|
|
|
|
|
# or just to begin hit the rest API before ordering and update bals then
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
### ROUTES ###
|
|
|
|
|
if ASTER_TGT_TAIL_ORDERABLE:
|
|
|
|
|
await aster_remainder_route()
|
|
|
|
|
|
|
|
|
|
if EXTEND_TGT_TAIL_ORDERABLE:
|
|
|
|
|
await extend_remainder_route()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
print(f'__________ End ___________ (Algo Engine ms: {(time.time() - loop_start)*1000})')
|
2026-04-21 20:22:33 +00:00
|
|
|
time.sleep(5)
|
|
|
|
|
|
|
|
|
|
except KeyboardInterrupt:
|
|
|
|
|
print('...algo stopped')
|
|
|
|
|
# await cancel_all_orders(CLIENT=CLIENT)
|
|
|
|
|
except Exception as e:
|
|
|
|
|
logging.critical(f'*** ALGO ENGINE CRASHED: {e}')
|
|
|
|
|
logging.error(traceback.format_exc())
|
|
|
|
|
# await cancel_all_orders(CLIENT=CLIENT)
|
2026-04-23 06:39:51 +00:00
|
|
|
|
|
|
|
|
### WALLLET ###
|
|
|
|
|
async def get_aster_collateral():
|
|
|
|
|
global ASTER_AVAIL_COLLATERAL
|
|
|
|
|
|
|
|
|
|
fut_acct_balances = {
|
|
|
|
|
"url": "/fapi/v3/balance",
|
|
|
|
|
"method": "GET",
|
|
|
|
|
"params": {}
|
|
|
|
|
}
|
|
|
|
|
r = 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():
|
|
|
|
|
global ASTER_NOTIONAL_POSITION
|
|
|
|
|
global ASTER_MULT
|
|
|
|
|
|
|
|
|
|
fut_acct_positionRisk = {
|
|
|
|
|
"url": "/fapi/v3/positionRisk",
|
|
|
|
|
"method": "GET",
|
|
|
|
|
"params": {}
|
|
|
|
|
}
|
|
|
|
|
r = aster_auth.post_authenticated_url(fut_acct_positionRisk)
|
|
|
|
|
d = [d for d in r if d.get('symbol', None) == ASTER_TICKER][0]
|
|
|
|
|
|
|
|
|
|
ASTER_NOTIONAL_POSITION = float(d.get('notional' ,0))
|
|
|
|
|
ASTER_MULT = float(d.get('leverage', ASTER_MULT))
|
|
|
|
|
|
|
|
|
|
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():
|
|
|
|
|
global EXTEND_NOTIONAL_POSITION
|
|
|
|
|
global EXTEND_MULT
|
|
|
|
|
|
|
|
|
|
get_pos = dict(await EXTEND_CLIENT.account.get_positions()).get('data', {})
|
|
|
|
|
pos_dict = [d for d in get_pos if d.get('market') == EXTEND_TICKER]
|
|
|
|
|
if pos_dict:
|
|
|
|
|
pos_dict = pos_dict[0]
|
|
|
|
|
EXTEND_NOTIONAL_POSITION = pos_dict.get('value', 0)
|
|
|
|
|
EXTEND_MULT = pos_dict.get('leverage', EXTEND_MULT)
|
|
|
|
|
else:
|
|
|
|
|
EXTEND_NOTIONAL_POSITION = 0
|
|
|
|
|
|
|
|
|
|
### EXCHANGE INFO ###
|
|
|
|
|
async def get_aster_exch_info():
|
|
|
|
|
global ASTER_MIN_ORDER_QTY
|
|
|
|
|
|
|
|
|
|
fut_acct_exchangeInfo = {
|
|
|
|
|
"url": "/fapi/v3/exchangeInfo",
|
|
|
|
|
"method": "GET",
|
|
|
|
|
"params": {}
|
|
|
|
|
}
|
|
|
|
|
r = 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)
|
|
|
|
|
|
2026-04-21 20:22:33 +00:00
|
|
|
async def main():
|
2026-04-23 06:39:51 +00:00
|
|
|
global EXTEND_CLIENT
|
2026-04-21 20:22:33 +00:00
|
|
|
global VAL_KEY
|
|
|
|
|
global CON
|
|
|
|
|
|
2026-04-23 06:39:51 +00:00
|
|
|
_, EXTEND_CLIENT = await extend_auth.create_auth_account_and_trading_client()
|
2026-04-21 20:22:33 +00:00
|
|
|
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:
|
|
|
|
|
# await create_executions_orders_table(CON=CON)
|
2026-04-23 06:39:51 +00:00
|
|
|
await get_aster_collateral()
|
|
|
|
|
await get_aster_notional_position()
|
|
|
|
|
await get_extend_collateral()
|
|
|
|
|
await get_extend_notional()
|
|
|
|
|
|
2026-04-21 20:22:33 +00:00
|
|
|
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())
|
|
|
|
|
|