refactor valkey into objects with health check

This commit is contained in:
2026-05-15 11:34:25 +09:00
parent f5f43be1a1
commit 1fd922d98f
25 changed files with 6461 additions and 9613 deletions

197
main.py
View File

@@ -24,7 +24,7 @@ import math
import os
import time
import traceback
from datetime import datetime, timezone
from datetime import datetime
from decimal import ROUND_DOWN, ROUND_UP, ROUND_HALF_UP, Decimal
from typing import AsyncContextManager
from dataclasses import dataclass, asdict, field
@@ -82,7 +82,7 @@ async def output_algo_status(status: str) -> None:
Algo_Status.last_update_ts_ms = int(round(datetime.now().timestamp()*1000, 2))
Algo_Status.status = status
VAL_KEY.set('algo_status', json.dumps(Algo_Status.model_dump()))
VAL_KEY.set('algo_status', json.dumps(Algo_Status.model_dump(), cls=utils.JSONEncoder_Decimal))
def create_exchange_objs_from_dict(exchanges_dict: dict) -> tuple[structs.Perpetual_Exchange, structs.Perpetual_Exchange]:
Aster = structs.Perpetual_Exchange(
@@ -129,8 +129,8 @@ async def symbol_switch(best_symbol_by_exchange_aster: structs.Perpetual_Exchang
Config.Overrides.Flatten_Open_Positions_Opportunistic = True
else:
logging.info('Balances Flattened - Updating to Trade New Symbols:')
logging.info(f' ASTER.symbol -> {best_symbol_by_exchange_aster.symbol}')
logging.info(f' EXTEND.symbol -> {best_symbol_by_exchange_extend.symbol}')
logging.info(f' {Aster.symbol} -> {best_symbol_by_exchange_aster.symbol}')
logging.info(f' {Extend.symbol} -> {best_symbol_by_exchange_extend.symbol}')
await aster_cancel_all_orders()
await extend_cancel_all_orders()
@@ -152,7 +152,16 @@ async def symbol_switch(best_symbol_by_exchange_aster: structs.Perpetual_Exchang
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)}))
if not Aster:
logging.critical(f'Main Algo, Aster is none: {Aster}')
await kill_algo()
elif not Extend:
logging.critical(f'Main Algo, Extend is none: {Extend}')
await kill_algo()
else:
logging.info(f'setting fr_algo_working_symbol: Aster: {Aster}; Extend: {Extend}')
VAL_KEY.set(name='fr_algo_working_symbol', value=json.dumps(obj={'ASTER': asdict(obj=Aster), 'EXTEND': asdict(obj=Extend)}, cls=utils.JSONEncoder_Decimal))
def calc_fr_minutes_remaining_factor(
min_start_procedure: int = 15,
@@ -436,6 +445,12 @@ async def post_aster_order(
# print_summary(use_logging=True)
else:
logging.critical(f'*** Aster Order Response Abnormal: {order_resp}; post_order: {post_order}')
'''
NEED TO HANDLE THE BELOW RESP
*** Aster Order Response Abnormal: {'code': -5018, 'msg': 'Youve reached the maximum notional value limit for this symbol. You can still reduce or close your position to manage your risk.'}; post_order: {'url': '/fapi/v3/order', 'method': 'POST', 'params': {'symbol': 'ENAUSDT', 'side': 'SELL', 'type': 'LIMIT', 'timeInForce': 'GTX', 'quantity': Decimal('1900'), 'price': Decimal('0.1339100'), 'reduceOnly': False}}
*** Aster Order Response Abnormal: {'code': -4226, 'msg': 'Nonce used'}; post_order: {'url': '/fapi/v3/order', 'method': 'POST', 'params': {'symbol': 'BTCUSDT', 'side': 'SELL', 'type': 'LIMIT', 'timeInForce': 'GTX', 'quantity': Decimal('0.006'), 'price': Decimal('80520.0'), 'reduceOnly': False}}
'''
await kill_algo()
async def cancel_extend_order(order_id: str):
@@ -487,6 +502,7 @@ async def post_extend_order(
elif '1142' in str(e): # 'Error response from https://api.starknet.extended.exchange/api/v1/user/order: code 400 - {"status":"ERROR","error":{"code":1142,"message":"Edit order not found"}};'
logging.info('EXTEND EDIT ORDER, NOT FOUND, CANCELLING and continuing')
await extend_cancel_all_orders()
# if Extend_Open_Orders:
# Extend_Open_Orders.pop(0)
# time.sleep(0.1)
@@ -578,11 +594,11 @@ async def handle_order_updates(exch: str, local_open_orders: list[dict], ws_open
logging.info(f'{exch} ORDER PARTIALLY FILLED: {order_id}')
# await get_aster_collateral()
if exch=='ASTER':
await get_aster_notional_position(resp=ws_pos_updates)
await get_aster_notional_position()
Last_Aster_Fill_Time_Ts = datetime.now().timestamp()*1000
Aster.cancel_request_pending = False
else:
await get_extend_notional(resp=ws_pos_updates)
await get_extend_notional()
Extend.cancel_request_pending = False
utils.send_tg_alert(f'FR_ALGO - {exch} PARTIALLY FILLED ({order_id})')
elif order_update_status in ['FILLED']:
@@ -591,7 +607,7 @@ async def handle_order_updates(exch: str, local_open_orders: list[dict], ws_open
# await get_aster_collateral()
if exch=='ASTER':
# await aster_cancel_all_orders()
await get_aster_notional_position(resp=ws_pos_updates)
await get_aster_notional_position()
Last_Aster_Fill_Time_Ts = datetime.now().timestamp()*1000
Aster.cancel_request_pending = False
else:
@@ -683,7 +699,7 @@ async def get_aster_notional_position(resp: list | None = None):
pos_dict['timestamp_arrival'] = round(datetime.now().timestamp()*1000)
if previous_notional_obj:
if previous_notional_obj['timestamp_arrival'] > pos_dict['timestamp_arrival']:
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
@@ -700,12 +716,12 @@ async def get_aster_notional_position(resp: list | None = None):
raise ValueError(e)
if pos_dict.get('notional') is not None:
Aster.notional_position = float(pos_dict['notional']) #- Aster.unrealized_pnl
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) > Config.Config.Max_Target_Notional*Config.Config.Max_Order_Over_Notional_Ratio:
if abs(Decimal(str(Aster.notional_position))) > Config.Config.Max_Target_Notional*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: {Config.Config.Max_Target_Notional}')
await kill_algo()
if Aster.notional_position != previous_notional_position:
@@ -778,7 +794,7 @@ async def get_extend_notional(resp: list | None = None):
Extend.notional_position = notional_pos_sided - float(Extend.unrealized_pnl)
Extend.mult = pos_dict.get('leverage', Extend.mult)
if abs(Extend.notional_position) > Config.Config.Max_Target_Notional*Config.Config.Max_Order_Over_Notional_Ratio:
if abs(Decimal(str(Extend.notional_position))) > Config.Config.Max_Target_Notional*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:
@@ -843,7 +859,11 @@ async def aster_cancel_all_orders():
logging.info(f'ASTER CANCEL ALL OPEN ORDERS RESP: {r}')
async def extend_cancel_all_orders():
global Extend_Open_Orders
r = await EXTEND_CLIENT.orders.mass_cancel(markets=[Extend.symbol])
if Extend_Open_Orders:
Extend_Open_Orders.pop(0)
logging.info(f'EXTEND CANCEL ALL OPEN ORDERS RESP: {r}')
### KILL ALGO ###
@@ -851,9 +871,10 @@ 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')
await output_algo_status('STOPPED')
await output_algo_status('DEAD')
raise ValueError('KILL FLAG ACTIVATED')
### ALGO LOOP ###
async def run_algo():
global Config
@@ -878,14 +899,36 @@ async def run_algo():
# print('__________Start___________')
### Load Algo Config ###
Config = json.loads(VAL_KEY.get('fr_orchestrator_output')) # ty:ignore[invalid-argument-type]
Config = structs.Algo_Config(**Config)
Config.Config.Max_Target_Notional = float(min([Aster.mult, Extend.mult]) * Config.Config.Target_Open_Cash_Position)
fr_orchestrator_output: str = VAL_KEY.get('fr_orchestrator_output') # ty:ignore[invalid-assignment]
if fr_orchestrator_output:
Config = json.loads(fr_orchestrator_output)
Config = structs.Algo_Config(**Config)
Config.Config.Max_Target_Notional = float(min([Aster.mult, Extend.mult]) * Config.Config.Target_Open_Cash_Position)
else:
logging.critical(f'fr_orchestrator_output is empty: {fr_orchestrator_output}; reloading from disk and continuing.')
with open('algo_config.json', mode='r', encoding='utf-8') as file:
Config = json.load(file)
Config = structs.Algo_Config(**Config)
if not Config.model_dump():
logging.critical(f'fr_orchestrator_output is empty - killing: {fr_orchestrator_output};')
Config.Config.Max_Target_Notional = float(min([Aster.mult, Extend.mult]) * Config.Config.Target_Open_Cash_Position)
VAL_KEY.set(name='fr_orchestrator_output', value=json.dumps(obj=Config.model_dump(), cls=utils.JSONEncoder_Decimal))
min_time_to_funding = 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]
vk_get: str = VAL_KEY.get(name='fr_engine_best_fund_rate_output') # ty:ignore[invalid-assignment]
if vk_get:
best_symbol_by_exchange: dict = json.loads(vk_get)
else:
logging.critical(f'best_symbol_by_exchange is none: {vk_get}')
await kill_algo()
raise ValueError(f'best_symbol_by_exchange is none: {vk_get}')
# 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'])
@@ -933,10 +976,20 @@ async def run_algo():
fr_best_exch = 'ASTER' if max([abs(fund_rate_ast), abs(fund_rate_ext)]) == abs(fund_rate_ast) else 'EXTEND'
fr_best_rate = fund_rate_ast if max([abs(fund_rate_ast), abs(fund_rate_ext)]) == abs(fund_rate_ast) else fund_rate_ext
fr_best_side = 'BUY' if fr_best_rate < 0 else 'SELL'
if fr_best_side == 'SELL':
fr_best_exch = 'ASTER' if fr_best_exch == 'EXTEND' else 'EXTEND'
fr_best_side = 'BUY'
return net_fr, fr_best_exch, fr_best_side
else:
fr_best_exch = 'EXTEND'
fr_best_side = 'BUY' if fund_rate_ext < 0 else 'SELL'
if fr_best_side == 'SELL':
fr_best_exch = 'ASTER'
fr_best_side = 'BUY'
return fund_rate_ext, fr_best_exch, fr_best_side
next_net_funding_rate, fr_best_exch, fr_best_side = calc_next_net_fund_rate(next_funding_at_same_time, fund_rate_ast=aster_fund_rate, fund_rate_ext=extend_fund_rate)
@@ -947,7 +1000,16 @@ async def run_algo():
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(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)}))
if not Aster:
logging.critical(f'Main Algo, Aster is none: {Aster}')
await kill_algo()
elif not Extend:
logging.critical(f'Main Algo, Extend is none: {Extend}')
await kill_algo()
else:
logging.info(f'setting fr_algo_working_symbol: Aster: {Aster}; Extend: {Extend}')
VAL_KEY.set(name='fr_algo_working_symbol', value=json.dumps(obj={'ASTER': asdict(obj=Aster), 'EXTEND': asdict(obj=Extend)}, cls=utils.JSONEncoder_Decimal))
time.sleep(5)
continue
# raise ValueError(f'ASTER Symbol mismatch: {ASTER_TICKER_DICT}; expected symbol: {ASTER.symbol}')
@@ -956,7 +1018,16 @@ async def run_algo():
extend_ticker_dict: dict = json.loads(s=extend_ticker_dict) if extend_ticker_dict is not None else {}
if ( extend_ticker_dict.get('symbol', None) != Extend.symbol) and not(Config.Overrides.Flatten_Open_Positions):
logging.warning(f'EXTEND Symbol mismatch: {extend_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)}))
if not Aster:
logging.critical(f'Main Algo, Aster is none: {Aster}')
await kill_algo()
elif not Extend:
logging.critical(f'Main Algo, Extend is none: {Extend}')
await kill_algo()
else:
logging.info(f'setting fr_algo_working_symbol: Aster: {Aster}; Extend: {Extend}')
VAL_KEY.set(name='fr_algo_working_symbol', value=json.dumps(obj={'ASTER': asdict(obj=Aster), 'EXTEND': asdict(obj=Extend)}, cls=utils.JSONEncoder_Decimal))
time.sleep(5)
continue
# raise ValueError(f'EXTEND Symbol mismatch: {EXTENDED_TICKER_DICT}; expected symbol: {EXTEND.symbol}')
@@ -1029,15 +1100,15 @@ async def run_algo():
if Config.Overrides.Flatten_Open_Positions:
aster_tgt = Decimal('0.00')
elif Config.Overrides.Flatten_Open_Positions_Opportunistic:
elif Config.Overrides.Flatten_Open_Positions_Opportunistic or (signal.exchange != fr_best_exch):
if signal.signal:
if signal.exchange == 'EXTEND' and Decimal(str(Aster.notional_position)) > 0:
if signal.exchange == 'EXTEND' and Decimal(str(Aster.notional_position)) >= 0:
aster_tgt = Decimal('0.00')
if signal.exchange == 'EXTEND' and Decimal(str(Aster.notional_position)) < 0:
pass
if signal.exchange == 'ASTER' and Decimal(str(Aster.notional_position)) > 0:
pass
if signal.exchange == 'ASTER' and Decimal(str(Aster.notional_position)) < 0:
if signal.exchange == 'ASTER' and Decimal(str(Aster.notional_position)) <= 0:
aster_tgt = Decimal('0.00')
aster_target = Target(
@@ -1062,7 +1133,7 @@ async def run_algo():
MKT : Aster: {Aster.symbol:<10} (best: {best_symbol_by_exchange_aster.symbol}) | Extend: {Extend.symbol:<10} (best: {best_symbol_by_exchange_extend.symbol})
FR SWITCH : {funding_rate_switch_net:.6%} [{funding_rate_switch_net*10_000:.2f}bps] [{funding_rate_switch_net*1_000_000:.0f}pips]
{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()):})
{pd.to_datetime(aster_fund_rate_time, unit='ms')} ({(pd.to_datetime(aster_fund_rate_time, unit='ms') - pd.to_datetime(datetime.now().timestamp()*1000, unit='ms')) }) | {pd.to_datetime(extend_fund_rate_time, unit='ms')} ({(pd.to_datetime(extend_fund_rate_time, unit='ms')-pd.to_datetime(datetime.now().timestamp()*1000, unit='ms'))})
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} ]
@@ -1136,7 +1207,7 @@ async def run_algo():
open_order_dict = dict(Aster_Open_Orders[0])
open_order_id = str(open_order_dict['order_id'])
try:
open_order_px = float(open_order_dict['price'])
open_order_px = float(open_order_dict['price']) if open_order_dict.get('price') is not None else float(open_order_dict['last_filled_price'])
except Exception as e:
logging.critical(f'Aster cant find price on order obj: {open_order_dict}; e: {e}')
await kill_algo()
@@ -1226,7 +1297,7 @@ async def run_algo():
)
### Output Algo Status ###
await output_algo_status('WORKING')
await output_algo_status('HEALTHY')
### CHECK TIME TO FUNDING AND WHETHER TO BE ACTIVE ###
if ( time_to_funding_ms > min_time_to_funding ) and (not Aster_Open_Orders) and (not Extend_Open_Orders):
@@ -1246,13 +1317,13 @@ async def run_algo():
time.sleep(Config.Config.Loop_Sleep_Sec)
except KeyboardInterrupt:
logging.info('CANCELLING OPEN ORDERS')
await output_algo_status('STOPPING')
await output_algo_status('UNHEALTHY')
await kill_algo()
except Exception as e:
logging.error(traceback.format_exc())
logging.critical(f'*** ALGO ENGINE CRASHED: {e}')
logging.info('CANCELLING OPEN ORDERS')
await output_algo_status('STOPPING')
await output_algo_status('UNHEALTHY')
utils.send_tg_alert(f'FR_ALGO_CRASHED: {str(e)}')
await kill_algo()
@@ -1275,14 +1346,23 @@ async def main():
await set_comb_open_symbols()
# Open_Symbols = ['HYPE-USD']
best_symbol_by_exchange: dict = json.loads(s=VAL_KEY.get(name='fr_engine_best_fund_rate_output')) # ty:ignore[invalid-argument-type]
vk_get: str = VAL_KEY.get(name='fr_engine_best_fund_rate_output') # ty:ignore[invalid-assignment]
if vk_get:
best_symbol_by_exchange: dict = json.loads(vk_get)
else:
best_symbol_by_exchange = None
raise ValueError('best_symbol_by_exchange is none')
if Open_Symbols:
if Open_Symbols or not(best_symbol_by_exchange):
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]
if not current_pos_master_ast:
logging.critical(f'Open symbol not found in master data, killing algo: symbol {open_symbol_to_work}; md: {current_pos_master_ast}')
await kill_algo()
Aster, Extend = create_exchange_objs_from_dict(exchanges_dict=current_pos_master_ast)
Open_Symbols.pop(0)
@@ -1297,36 +1377,45 @@ async def main():
Config = json.load(file)
Config = structs.Algo_Config(**Config)
Config.Config.Max_Target_Notional = float(min([Aster.mult, Extend.mult]) * 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=Config.model_dump()))
VAL_KEY.set(name='fr_algo_working_symbol', value=json.dumps(obj={'ASTER': asdict(obj=Aster), 'EXTEND': asdict(obj=Extend)}))
if not Config.model_dump():
raise ValueError(f'Main Algo, initial config is none: {Config}')
elif not Aster:
raise ValueError(f'Main Algo, Aster is none: {Aster}')
elif not Extend:
raise ValueError(f'Main Algo, Extend is none: {Extend}')
else:
Config.Config.Max_Target_Notional = float(min([Aster.mult, Extend.mult]) * Config.Config.Target_Open_Cash_Position)
VAL_KEY.set(name='fr_orchestrator_output', value=json.dumps(obj=Config.model_dump(), cls=utils.JSONEncoder_Decimal))
logging.info(f'setting fr_algo_working_symbol: Aster: {Aster}; Extend: {Extend}')
VAL_KEY.set(name='fr_algo_working_symbol', value=json.dumps(obj={'ASTER': asdict(obj=Aster), 'EXTEND': asdict(obj=Extend)}, cls=utils.JSONEncoder_Decimal))
Funding_Rates_Min_Remaining_Factor_Pcts = calc_fr_minutes_remaining_factor()
Algo_Status = structs.Algo_Status(
last_update_ts_ms = int(round(datetime.now().timestamp()*1000, 2)),
status = 'WORKING',
expected_alpha = 0.00,
model_ratio = 0.00,
current_ratio = 0.00,
)
await output_algo_status('WORKING')
Funding_Rates_Min_Remaining_Factor_Pcts = calc_fr_minutes_remaining_factor()
Algo_Status = structs.Algo_Status(
last_update_ts_ms = int(round(datetime.now().timestamp()*1000, 2)),
status = 'HEALTHY',
expected_alpha = 0.00,
model_ratio = 0.00,
current_ratio = 0.00,
)
await output_algo_status('HEALTHY')
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()
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()
await run_algo()
if __name__ == '__main__':
START_TIME = round(datetime.now().timestamp()*1000)