refactor algo orchestrator and hedge ratio bug fix

This commit is contained in:
2026-04-29 16:18:42 +00:00
parent 8f3f7c6667
commit dc3409ac40
14 changed files with 1475 additions and 11128 deletions

View File

@@ -1,13 +1,25 @@
{
"Config_Updated_Timestamp": 1777098091913,
"Allow_Ordering_Aster": false,
"Allow_Ordering_Extend": false,
"Loop_Sleep_Sec": 5.00,
"Max_Target_Notional": 0.00,
"Min_Time_To_Funding_Minutes": 10,
"Price_Worsener_Aster": 0.0,
"Price_Worsener_Extend": 0.0,
"Target_Open_Cash_Position": 10,
"Print_Summary_Each_Loop" : true,
"Flip_Side_For_Testing": false
"Updated_Timestamp": 1777433705326,
"Config": {
"Loop_Sleep_Sec": 0.0,
"Max_Order_Over_Notional_Ratio": 1.05,
"Max_Target_Notional": 0.0,
"Min_Time_To_Funding_Minutes": 60,
"Min_Fund_Rate_Pct_To_Trade": 0.0,
"Price_Worsener_Aster": 0.0,
"Price_Worsener_Extend": -0.1,
"Switch_To_Taker_Seconds": 1,
"Target_Open_Cash_Position": 10
},
"Logging": {
"Log_Summary_Each_Loop": false,
"Print_Summary_Each_Loop": false
},
"Overrides": {
"Allow_Ordering_Aster": true,
"Allow_Ordering_Extend": true,
"Flatten_Open_Positions": false,
"Flip_Side_For_Testing": false
}
}

View File

@@ -2,7 +2,7 @@
"cells": [
{
"cell_type": "code",
"execution_count": 4,
"execution_count": 22,
"id": "d1eed397",
"metadata": {},
"outputs": [],
@@ -13,12 +13,13 @@
"import valkey\n",
"\n",
"with open('algo_config.json', 'r', encoding='utf-8') as file:\n",
" ALGO_CONFIG = json.load(file, object_hook=lambda d: structs.Algo_Config(**d))"
" ALGO_CONFIG = json.load(file)\n",
" ALGO_CONFIG = structs.Algo_Config(**ALGO_CONFIG)"
]
},
{
"cell_type": "code",
"execution_count": 5,
"execution_count": 23,
"id": "c6151613",
"metadata": {},
"outputs": [],
@@ -38,23 +39,31 @@
"1"
]
},
"execution_count": 12,
"execution_count": 4,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"config_update = {\n",
" # 'Min_Time_To_Funding_Minutes': 60,\n",
" # 'Allow_Ordering_Aster': True,\n",
" # 'Allow_Ordering_Extend': True,\n",
" # 'Loop_Sleep_Sec': 0.00,\n",
" # 'Min_Fund_Rate_Pct_To_Trade': 0.001,\n",
" # 'Flip_Side_For_Testing': False,\n",
" # 'Price_Worsener_Extend': 0.0,\n",
" # 'Log_Summary_Each_Loop': False,\n",
" 'Print_Summary_Each_Loop': True,\n",
" 'Flatten_Open_Positions': False,\n",
" # 'Config': {\n",
" # 'Loop_Sleep_Sec': 0.00,\n",
" # 'Min_Time_To_Funding_Minutes': 60,\n",
" # 'Min_Fund_Rate_Pct_To_Trade': 0.001,\n",
" # 'Price_Worsener_Extend': 0.0,\n",
" # 'Price_Worsener_Aster': 0.0,\n",
" # 'Switch_To_Taker_Seconds': 1,\n",
" # },\n",
" 'Logging': {\n",
" 'Log_Summary_Each_Loop': False,\n",
" 'Print_Summary_Each_Loop': True,\n",
" },\n",
" # 'Overrides': {\n",
" # 'Allow_Ordering_Aster': True,\n",
" # 'Allow_Ordering_Extend': True,\n",
" # 'Flip_Side_For_Testing': False,\n",
" # 'Flatten_Open_Positions': False,\n",
" # },\n",
"}\n",
"VAL_KEY.publish('fr_orchestrator_input', json.dumps(config_update))"
]

View File

@@ -1,16 +1,24 @@
{
"Config_Updated_Timestamp": 1777411434591,
"Allow_Ordering_Aster": true,
"Allow_Ordering_Extend": true,
"Loop_Sleep_Sec": 0.0,
"Max_Target_Notional": 0.0,
"Min_Time_To_Funding_Minutes": 60,
"Min_Fund_Rate_Pct_To_Trade": 0.0,
"Price_Worsener_Aster": 0.0,
"Price_Worsener_Extend": -0.1,
"Target_Open_Cash_Position": 10,
"Log_Summary_Each_Loop": false,
"Print_Summary_Each_Loop": true,
"Flatten_Open_Positions": false,
"Flip_Side_For_Testing": false
"Updated_Timestamp": 1777478176165,
"Config": {
"Loop_Sleep_Sec": 0.0,
"Max_Order_Over_Notional_Ratio": 1.05,
"Max_Target_Notional": 0.0,
"Min_Time_To_Funding_Minutes": 60,
"Min_Fund_Rate_Pct_To_Trade": 0.0,
"Price_Worsener_Aster": 0.0,
"Price_Worsener_Extend": -0.1,
"Switch_To_Taker_Seconds": 1,
"Target_Open_Cash_Position": 10
},
"Logging": {
"Log_Summary_Each_Loop": false,
"Print_Summary_Each_Loop": true
},
"Overrides": {
"Allow_Ordering_Aster": true,
"Allow_Ordering_Extend": true,
"Flatten_Open_Positions": false,
"Flip_Side_For_Testing": false
}
}

View File

@@ -26,7 +26,6 @@ load_dotenv()
LOG_FILEPATH: str = os.getenv("LOGS_PATH") + '/Fund_Rate_Algo_Orchestrator.log'
ALGO_CONFIG: None | dict
# ALGO_CONFIG: None | Algo_Config = None
async def orchestrator() -> None:
global ALGO_CONFIG
@@ -35,7 +34,7 @@ async def orchestrator() -> None:
VK_PUBSUB = VAL_KEY.pubsub()
VK_PUBSUB.subscribe(VK_IN)
print(f"Subscribed to '{VK_IN}'. Waiting for messages...")
logging.info(f"Subscribed to '{VK_IN}'. Waiting for messages...")
for message in VK_PUBSUB.listen():
if message['type'] == 'message':
@@ -46,7 +45,7 @@ async def orchestrator() -> None:
with open('/algo_local_drive/algo_config.json', 'r', encoding='utf-8') as f:
# ALGO_CONFIG = json.load(f, object_hook=lambda d: Algo_Config(**d))
ALGO_CONFIG = json.load(f)
ALGO_CONFIG['Config_Updated_Timestamp'] = timestamp
ALGO_CONFIG['Updated_Timestamp'] = timestamp
for k, v in data.items():
if ALGO_CONFIG.get(k, None) is not None:
@@ -54,12 +53,11 @@ async def orchestrator() -> None:
VAL_KEY.set(VK_OUT, json.dumps(ALGO_CONFIG))
with open('/algo_local_drive/algo_config.json', 'w', encoding='utf-8') as f:
# print('SAVING FILE')
json.dump(ALGO_CONFIG, f, indent=4)
print(f"Algo Config Updated @ {timestamp}; {data}")
logging.info(f"Algo Config Updated @ {timestamp}; {data}")
except valkey.exceptions.ConnectionError as e:
print(f"Could not connect to Valkey. Please check the publish server is up; {e}")
logging.info(f"Could not connect to Valkey. Please check the publish server is up; {e}")
except KeyboardInterrupt:
logging.info('ORCHESTRATOR SHUTTING DOWN...')
except Exception as e:
@@ -78,7 +76,7 @@ async def main() -> None:
with open('/algo_local_drive/algo_config.json', 'r', encoding='utf-8') as f:
# ALGO_CONFIG = json.load(f, object_hook=lambda d: Algo_Config(**d))
ALGO_CONFIG = json.load(f)
ALGO_CONFIG['Config_Updated_Timestamp'] = round(datetime.now().timestamp()*1000)
ALGO_CONFIG['Updated_Timestamp'] = round(datetime.now().timestamp()*1000)
# async with engine.connect() as CON:
await orchestrator()

View File

@@ -1,4 +1,4 @@
# tail -f Fund_Rate_Algo.log Fund_Rate_Aster_User.log Fund_Rate_Aster.log Fund_Rate_Extended_FR.log Fund_Rate_Extended_OB.log Fund_Rate_Extended_Trades.log Fund_Rate_Extended_User.log
# tail -f Fund_Rate_Algo_Orchestrator.log Fund_Rate_Algo.log Fund_Rate_Aster_User.log Fund_Rate_Aster.log Fund_Rate_Extended_FR.log Fund_Rate_Extended_OB.log Fund_Rate_Extended_Trades.log Fund_Rate_Extended_User.log
services:
# algo:

File diff suppressed because it is too large Load Diff

172
engine_best_funding_rate.py Normal file
View File

@@ -0,0 +1,172 @@
import asyncio
import json
import logging
import os
import time
import traceback
from dataclasses import dataclass, field
from datetime import datetime
from typing import AsyncContextManager
import pandas as pd
import requests
import valkey
from dotenv import load_dotenv
# from sqlalchemy.ext.asyncio import create_async_engine
### Structs ###
@dataclass(kw_only=False)
class Asset_Leverage:
exchange: str
lh_asset: str
rh_asset: str
max_leverage: int
max_notional: float
# max_leverage_notional: list = field(default_factory=list)
### MANUAL LEVERAGE DATA ###
LEVERAGE_BY_EXCH: list[Asset_Leverage] = [
Asset_Leverage('ASTER', 'BTC' , 'USDT', 150, 300_000), Asset_Leverage('EXTEND', 'BTC' , 'USD', 50, 4_000_000),
Asset_Leverage('ASTER', 'ETH' , 'USDT', 150, 300_000), Asset_Leverage('EXTEND', 'ETH' , 'USD', 50, 4_000_000),
Asset_Leverage('ASTER', 'LIT' , 'USDT', 50 , 2_500 ), Asset_Leverage('EXTEND', 'LIT' , 'USD', 25, 400_000 ),
Asset_Leverage('ASTER', 'CHIP' , 'USDT', 50 , 5_000 ), Asset_Leverage('EXTEND', 'CHIP' , 'USD', 5 , 100_000 ),
Asset_Leverage('ASTER', 'XAG' , 'USDT', 100, 50_000 ), Asset_Leverage('EXTEND', 'XAG' , 'USD', 10, 1_000_000),
Asset_Leverage('ASTER', '4' , 'USDT', 50 , 5_000 ), Asset_Leverage('EXTEND', '4' , 'USD', 5 , 100_000 ),
Asset_Leverage('ASTER', 'XPT' , 'USDT', 3 , 30_000 ), Asset_Leverage('EXTEND', 'XPT' , 'USD', 5 , 1_000_000),
Asset_Leverage('ASTER', 'XMR' , 'USDT', 50 , 10_000 ), Asset_Leverage('EXTEND', 'XMR' , 'USD', 25, 400_000 ),
Asset_Leverage('ASTER', 'WLFI' , 'USDT', 25 , 104_869), Asset_Leverage('EXTEND', 'WLFI' , 'USD', 10, 250_000 ),
Asset_Leverage('ASTER', 'TRUMP', 'USDT', 50 , 5_567 ), Asset_Leverage('EXTEND', 'TRUMP', 'USD', 25, 400_000 ),
Asset_Leverage('ASTER', 'INIT' , 'USDT', 50 , 5_000 ), Asset_Leverage('EXTEND', 'INIT' , 'USD', 5 , 100_000 ),
Asset_Leverage('ASTER', 'ZORA' , 'USDT', 5 , 100_000), Asset_Leverage('EXTEND', 'ZORA' , 'USD', 5 , 100_000 ),
Asset_Leverage('ASTER', 'ZEC' , 'USDT', 75 , 6_250 ), Asset_Leverage('EXTEND', 'ZEC' , 'USD', 10, 250_000 ),
]
df_leverage_by_exch = pd.DataFrame(LEVERAGE_BY_EXCH)
### Database ###
# CON: AsyncContextManager | None = None
VAL_KEY = None
VK_OUT = 'fr_engine_best_fund_rate_output'
### Logging ###
load_dotenv()
LOG_FILEPATH: str = os.getenv("LOGS_PATH") + '/Fund_Rate_Engine_BFR.log'
### CONSTANTS ###
LOOP_SLEEP_SEC: int = 5
REFRESH_MKT_INFO_EVERY_SEC: int = 90
REFRESH_MKT_VOLUME_EVERY_SEC: int = 30
### GLOBALS ###
Mkt_Info_Last_Refresh_TS_ms: int
Mkt_Volume_Last_Refresh_TS_ms: int
### Funcs - Load Data ###
def get_extended_markets_info() -> pd.DataFrame:
global Mkt_Info_Last_Refresh_TS_ms
r = json.loads(requests.get('https://api.starknet.extended.exchange/api/v1/info/markets').text)
df = pd.DataFrame(r['data'])
df['funding_rate'] = df['marketStats'].apply(lambda x: x.get('fundingRate',{}))
df['funding_rate_ts'] = df['marketStats'].apply(lambda x: x.get('nextFundingRate',{}))
df['min_order_size'] = df['tradingConfig'].apply(lambda x: x.get('minOrderSize',{}))
df['min_price_change'] = df['tradingConfig'].apply(lambda x: x.get('minPriceChange',{}))
df['max_leverage'] = df['tradingConfig'].apply(lambda x: x.get('maxLeverage',{}))
Mkt_Info_Last_Refresh_TS_ms = round(datetime.now().timestamp() * 1000)
print('Extend markets info refreshed successfully')
return df
def load_aster_current_fr() -> pd.DataFrame:
df = pd.DataFrame(json.loads(VAL_KEY.get('fund_rate_aster_all')))
df = df[['s','E','r','T']].rename({'s':'symbol','E':'funding_rate_updated_ts_ms','r':'funding_rate','T':'next_funding_ts'}, axis=1)
df['funding_rate_updated_dt'] = pd.to_datetime(df['funding_rate_updated_ts_ms'], unit='ms')
df['funding_rate'] = df['funding_rate'].astype(float)
df['time_delta_to_next_funding'] = pd.to_datetime(df['next_funding_ts'], unit='ms') - pd.Timestamp.now()
return df
def load_extend_current_fr(df_mkt_stats: pd.DataFrame) -> pd.DataFrame:
df = pd.DataFrame(json.loads(VAL_KEY.get('fund_rate_extended_all')))
df = df[['symbol','funding_rate_updated_ts_ms','funding_rate']]
df['funding_rate_updated_dt'] = pd.to_datetime(df['funding_rate_updated_ts_ms'], unit='ms')
df['funding_rate'] = df['funding_rate'].astype(float)
df = df.merge(df_mkt_stats[['name','assetName','status', 'funding_rate_ts','max_leverage']].rename({'name':'symbol','funding_rate_ts':'next_funding_ts'}, axis=1), on='symbol', how='left')
df = df.loc[df['status']=='ACTIVE',:]
df['USDT_Symbol'] = df['assetName'] + 'USDT'
df['time_delta_to_next_funding'] = pd.to_datetime(df['next_funding_ts'], unit='ms') - pd.Timestamp.now()
return df
async def loop() -> None:
global Mkt_Info_Last_Refresh_TS_ms
df_extend_mkt_stats = get_extended_markets_info()
try:
while True:
ts_arrival = round(datetime.now().timestamp() * 1000)
if ( ts_arrival - Mkt_Info_Last_Refresh_TS_ms ) > ( REFRESH_MKT_INFO_EVERY_SEC * 1000 ):
df_extend_mkt_stats = get_extended_markets_info()
df_aster_fr = load_aster_current_fr()
df_extend_fr = load_extend_current_fr(df_mkt_stats=df_extend_mkt_stats)
df_comb_fr = df_extend_fr.merge(df_aster_fr, left_on='USDT_Symbol', right_on='symbol', how='inner', suffixes=('_ext', '_ast'))
df_comb_fr['next_funding_at_same_time'] = (abs(df_comb_fr['time_delta_to_next_funding_ext'].dt.total_seconds() - df_comb_fr['time_delta_to_next_funding_ast'].dt.total_seconds()) / 60) < 1
df_comb_fr['net_funding_rate'] = (df_comb_fr[['funding_rate_ext', 'funding_rate_ast']].max(axis=1) - df_comb_fr[['funding_rate_ext', 'funding_rate_ast']].min(axis=1)).where(df_comb_fr['next_funding_at_same_time'], df_comb_fr['funding_rate_ext'])
df_comb_fr['net_funding_rate_abs'] = df_comb_fr['net_funding_rate'].abs()
### NET MULT ###
df_comb_fr = df_comb_fr.merge(df_leverage_by_exch.loc[df_leverage_by_exch['exchange']=='EXTEND'], left_on='assetName', right_on='lh_asset').merge(df_leverage_by_exch.loc[df_leverage_by_exch['exchange']=='ASTER'], left_on='assetName', right_on='lh_asset', suffixes=('_ext', '_ast'))
df_comb_fr['net_mult'] = 1 / ( ( 0.5 / df_comb_fr['max_leverage_ext'] ) + ( 0.5 / df_comb_fr['max_leverage_ast'] ) )
df_comb_fr['net_mult'] = df_comb_fr['net_mult'].round(2)
df_comb_fr['net_mult_x_net_fr_abs'] = df_comb_fr['net_funding_rate_abs'] * df_comb_fr['net_mult']
df_best_fr_rate = df_comb_fr[['symbol_ext','symbol_ast','net_mult_x_net_fr_abs','net_funding_rate_abs','net_funding_rate','next_funding_at_same_time']].sort_values(by='net_mult_x_net_fr_abs', ascending=False).reset_index(drop=True)
best_next_funding_pair = {'symbol_aster':df_best_fr_rate['symbol_ast'][0],'symbol_extended':df_best_fr_rate['symbol_ext'][0]}
VAL_KEY.set(VK_OUT, json.dumps(best_next_funding_pair))
print(best_next_funding_pair)
time.sleep(LOOP_SLEEP_SEC)
continue
except valkey.exceptions.ConnectionError as e:
logging.info(f"Could not connect to Valkey. Please check the publish server is up; {e}")
except KeyboardInterrupt:
logging.info('ORCHESTRATOR SHUTTING DOWN...')
except Exception as e:
logging.error(traceback.format_exc())
logging.critical(f'*** ORCHESTRATOR CRASHED: {e}')
### STARTUP ###
async def main() -> None:
global VAL_KEY
# global CON
VAL_KEY = valkey.Valkey(host='localhost', port=6379, db=0, decode_responses=True)
# engine = create_async_engine('mysql+asyncmy://root:pwd@localhost/fund_rate')
await loop()
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())

File diff suppressed because one or more lines are too long

View File

@@ -2,7 +2,7 @@
"cells": [
{
"cell_type": "code",
"execution_count": 1,
"execution_count": 9,
"id": "6c70a8c3",
"metadata": {},
"outputs": [],
@@ -27,7 +27,7 @@
},
{
"cell_type": "code",
"execution_count": 2,
"execution_count": 12,
"id": "ff971ca9",
"metadata": {},
"outputs": [],
@@ -42,50 +42,46 @@
"CONFIG = MAINNET_CONFIG\n",
"\n",
"ORDER_MARKET = \"ETH-USD\"\n",
"ORDER_SIDE = OrderSide.BUY\n",
"ORDER_QTY = Decimal(\"0.01\")\n",
"ORDER_PRICE = Decimal(\"2200.1\")"
"ORDER_PRICE = Decimal(\"2300.1\")\n",
"ORDER_SIDE = OrderSide.BUY"
]
},
{
"cell_type": "code",
"execution_count": 3,
"execution_count": 13,
"id": "fc2c6d2b",
"metadata": {},
"outputs": [],
"source": [
"client, trading_client = await extend_auth.create_auth_account_and_trading_client()"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "c366706f",
"metadata": {},
"outputs": [
{
"name": "stderr",
"output_type": "stream",
"text": [
"Error response from https://api.starknet.extended.exchange/api/v1/user/order: {\"status\":\"ERROR\",\"error\":{\"code\":1125,\"message\":\"Invalid price precision\"}}\n"
]
},
{
"ename": "ValueError",
"evalue": "Error response from https://api.starknet.extended.exchange/api/v1/user/order: code 400 - {\"status\":\"ERROR\",\"error\":{\"code\":1125,\"message\":\"Invalid price precision\"}}",
"output_type": "error",
"traceback": [
"\u001b[31m---------------------------------------------------------------------------\u001b[39m",
"\u001b[31mValueError\u001b[39m Traceback (most recent call last)",
"\u001b[36mCell\u001b[39m\u001b[36m \u001b[39m\u001b[32mIn[4]\u001b[39m\u001b[32m, line 1\u001b[39m\n\u001b[32m----> \u001b[39m\u001b[32m1\u001b[39m placed_order = await trading_client.place_order(\n\u001b[32m 2\u001b[39m market_name=ORDER_MARKET,\n\u001b[32m 3\u001b[39m amount_of_synthetic=ORDER_QTY,\n\u001b[32m 4\u001b[39m price=ORDER_PRICE,\n",
"\u001b[36mFile \u001b[39m\u001b[32m~/miniconda3/envs/py_313/lib/python3.13/site-packages/x10/perpetual/trading_client/trading_client.py:101\u001b[39m, in \u001b[36mPerpetualTradingClient.place_order\u001b[39m\u001b[34m(self, market_name, amount_of_synthetic, price, side, taker_fee, post_only, previous_order_id, expire_time, time_in_force, self_trade_protection_level, external_id, builder_fee, builder_id, reduce_only, tp_sl_type, take_profit, stop_loss)\u001b[39m\n\u001b[32m 78\u001b[39m expire_time = utc_now() + timedelta(hours=\u001b[32m1\u001b[39m)\n\u001b[32m 80\u001b[39m order = create_order_object(\n\u001b[32m 81\u001b[39m account=\u001b[38;5;28mself\u001b[39m.__stark_account,\n\u001b[32m 82\u001b[39m market=market,\n\u001b[32m (...)\u001b[39m\u001b[32m 99\u001b[39m stop_loss=stop_loss,\n\u001b[32m 100\u001b[39m )\n\u001b[32m--> \u001b[39m\u001b[32m101\u001b[39m \u001b[38;5;28;01mreturn\u001b[39;00m \u001b[38;5;28;01mawait\u001b[39;00m \u001b[38;5;28mself\u001b[39m.__order_management_module.place_order(order)\n",
"\u001b[36mFile \u001b[39m\u001b[32m~/miniconda3/envs/py_313/lib/python3.13/site-packages/x10/perpetual/trading_client/order_management_module.py:31\u001b[39m, in \u001b[36mOrderManagementModule.place_order\u001b[39m\u001b[34m(self, order)\u001b[39m\n\u001b[32m 28\u001b[39m LOGGER.debug(\u001b[33m\"\u001b[39m\u001b[33mPlacing an order: id=\u001b[39m\u001b[38;5;132;01m%s\u001b[39;00m\u001b[33m\"\u001b[39m, order.id)\n\u001b[32m 30\u001b[39m url = \u001b[38;5;28mself\u001b[39m._get_url(\u001b[33m\"\u001b[39m\u001b[33m/user/order\u001b[39m\u001b[33m\"\u001b[39m)\n\u001b[32m---> \u001b[39m\u001b[32m31\u001b[39m response = \u001b[38;5;28;01mawait\u001b[39;00m send_post_request(\n\u001b[32m 32\u001b[39m \u001b[38;5;28;01mawait\u001b[39;00m \u001b[38;5;28mself\u001b[39m.get_session(),\n\u001b[32m 33\u001b[39m url,\n\u001b[32m 34\u001b[39m PlacedOrderModel,\n\u001b[32m 35\u001b[39m json=order.to_api_request_json(exclude_none=\u001b[38;5;28;01mTrue\u001b[39;00m),\n\u001b[32m 36\u001b[39m api_key=\u001b[38;5;28mself\u001b[39m._get_api_key(),\n\u001b[32m 37\u001b[39m )\n\u001b[32m 38\u001b[39m \u001b[38;5;28;01mreturn\u001b[39;00m response\n",
"\u001b[36mFile \u001b[39m\u001b[32m~/miniconda3/envs/py_313/lib/python3.13/site-packages/x10/utils/http.py:173\u001b[39m, in \u001b[36msend_post_request\u001b[39m\u001b[34m(session, url, model_class, json, api_key, request_headers, response_code_to_exception)\u001b[39m\n\u001b[32m 171\u001b[39m \u001b[38;5;28;01masync\u001b[39;00m \u001b[38;5;28;01mwith\u001b[39;00m session.post(url, json=json, headers=headers) \u001b[38;5;28;01mas\u001b[39;00m response:\n\u001b[32m 172\u001b[39m response_text = \u001b[38;5;28;01mawait\u001b[39;00m response.text()\n\u001b[32m--> \u001b[39m\u001b[32m173\u001b[39m \u001b[30;43mhandle_known_errors\u001b[39;49m\u001b[30;43m(\u001b[39;49m\u001b[30;43murl\u001b[39;49m\u001b[30;43m,\u001b[39;49m\u001b[30;43m \u001b[39;49m\u001b[30;43mresponse_code_to_exception\u001b[39;49m\u001b[30;43m,\u001b[39;49m\u001b[30;43m \u001b[39;49m\u001b[30;43mresponse\u001b[39;49m\u001b[30;43m,\u001b[39;49m\u001b[30;43m \u001b[39;49m\u001b[30;43mresponse_text\u001b[39;49m\u001b[30;43m)\u001b[39;49m\n\u001b[32m 174\u001b[39m response_model = parse_response_to_model(response_text, model_class)\n\u001b[32m 176\u001b[39m \u001b[38;5;28;01mif\u001b[39;00m (response_model.status != ResponseStatus.OK) \u001b[38;5;129;01mor\u001b[39;00m (response_model.error \u001b[38;5;129;01mis\u001b[39;00m \u001b[38;5;129;01mnot\u001b[39;00m \u001b[38;5;28;01mNone\u001b[39;00m):\n",
"\u001b[36mFile \u001b[39m\u001b[32m~/miniconda3/envs/py_313/lib/python3.13/site-packages/x10/utils/http.py:243\u001b[39m, in \u001b[36mhandle_known_errors\u001b[39m\u001b[34m(url, response_code_handler, response, response_text)\u001b[39m\n\u001b[32m 241\u001b[39m \u001b[38;5;28;01mif\u001b[39;00m response.status > \u001b[32m299\u001b[39m:\n\u001b[32m 242\u001b[39m LOGGER.error(\u001b[33m\"\u001b[39m\u001b[33mError response from \u001b[39m\u001b[38;5;132;01m%s\u001b[39;00m\u001b[33m: \u001b[39m\u001b[38;5;132;01m%s\u001b[39;00m\u001b[33m\"\u001b[39m, url, response_text)\n\u001b[32m--> \u001b[39m\u001b[32m243\u001b[39m \u001b[38;5;28;01mraise\u001b[39;00m \u001b[38;5;167;01mValueError\u001b[39;00m(\u001b[33mf\u001b[39m\u001b[33m\"\u001b[39m\u001b[33mError response from \u001b[39m\u001b[38;5;132;01m{\u001b[39;00murl\u001b[38;5;132;01m}\u001b[39;00m\u001b[33m: code \u001b[39m\u001b[38;5;132;01m{\u001b[39;00mresponse.status\u001b[38;5;132;01m}\u001b[39;00m\u001b[33m - \u001b[39m\u001b[38;5;132;01m{\u001b[39;00mresponse_text\u001b[38;5;132;01m}\u001b[39;00m\u001b[33m\"\u001b[39m)\n",
"\u001b[31mValueError\u001b[39m: Error response from https://api.starknet.extended.exchange/api/v1/user/order: code 400 - {\"status\":\"ERROR\",\"error\":{\"code\":1125,\"message\":\"Invalid price precision\"}}"
"Unclosed client session\n",
"client_session: <aiohttp.client.ClientSession object at 0x7b2dc26ba850>\n"
]
}
],
"source": [
"client, trading_client = await extend_auth.create_auth_account_and_trading_client()"
]
},
{
"cell_type": "code",
"execution_count": 16,
"id": "07887649",
"metadata": {},
"outputs": [],
"source": [
"### Figure out how to flatten small residuals - market order with reduce only?)"
]
},
{
"cell_type": "code",
"execution_count": 14,
"id": "c366706f",
"metadata": {},
"outputs": [],
"source": [
"placed_order = await trading_client.place_order(\n",
" market_name=ORDER_MARKET,\n",
@@ -98,6 +94,27 @@
")"
]
},
{
"cell_type": "code",
"execution_count": 15,
"id": "ba85f1e9",
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
"WrappedApiResponse[PlacedOrderModel](status='OK', data=PlacedOrderModel(id=2049239831434784768, external_id='359148168147219671570709517544221313286652228166698232409514334035033828578'), error=None, pagination=None)"
]
},
"execution_count": 15,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"placed_order"
]
},
{
"cell_type": "code",
"execution_count": 5,

176
main.py
View File

@@ -5,7 +5,6 @@ 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
@@ -37,24 +36,30 @@ load_dotenv()
LOG_FILEPATH: str = os.getenv("LOGS_PATH") + '/Fund_Rate_Algo.log'
### Algo Config ###
ALGO_CONFIG: structs.Algo_Config = None
ALGO_CONFIG: structs.Algo_Config | None = None
MIN_TIME_TO_FUNDING: int
### CONSTANTS ###
### EXCHANGES ###
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
EXTEND = structs.Perpetual_Exchange(
mult = 50,
lh_asset = 'ETH',
rh_asset = 'USD',
symbol_asset_separator = '-',
)
### GLOBALS ###
ASTER_MULT = 150
EXTEND_MULT = 50
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
ASTER_MIN_ORDER_QTY = 0.001
EXTEND_MIN_ORDER_QTY = 0.01
@@ -121,7 +126,7 @@ 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
global ASTER
previous_notional_obj = ASTER_NOTIONAL_OBJ
@@ -159,32 +164,31 @@ async def get_aster_notional_position(resp: dict | None = None):
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}')
# ASTER.mult = float(d['leverage'])
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)}')
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
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
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_OBJ
global EXTEND_NOTIONAL_POSITION
global EXTEND_UNREALIZED_PNL
global EXTEND_MULT
global EXTEND
previous_notional_obj = EXTEND_NOTIONAL_OBJ
previous_notional_position = EXTEND_NOTIONAL_POSITION
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 = [dict(d) for d in resp if dict(d).get('market') == EXTEND.symbol]
if pos_dict:
pos_dict = pos_dict[0]
else:
@@ -195,7 +199,7 @@ async def get_extend_notional(resp: dict | None = None):
pos_dict['timestamp_arrival'] = round(datetime.now().timestamp()*1000)
else:
pos_dict = [dict(d) for d in resp if dict(d).get('market') == EXTEND_TICKER]
pos_dict = [dict(d) for d in resp if dict(d).get('market') == EXTEND.symbol]
if pos_dict:
pos_dict = pos_dict[0]
else:
@@ -223,12 +227,12 @@ async def get_extend_notional(resp: dict | None = None):
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}')
EXTEND.mult = pos_dict.get('leverage', EXTEND)
if abs(EXTEND_NOTIONAL_POSITION) > ALGO_CONFIG.Config.Max_Target_Notional*ALGO_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}; 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)}')
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():
@@ -264,7 +268,7 @@ async def aster_cancel_all_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])
r = await EXTEND_CLIENT.orders.mass_cancel(markets=[EXTEND.symbol])
logging.info(f'EXTEND CANCEL ALL OPEN ORDERS RESP: {r}')
### KILL ALGO ###
@@ -277,29 +281,39 @@ async def kill_algo():
### ALGO LOOP ###
async def run_algo():
global ASTER
global EXTEND
global ALGO_CONFIG
global MIN_TIME_TO_FUNDING
global ASTER_OPEN_ORDERS
global EXTEND_OPEN_ORDERS
global Last_Aster_Fill_Time_Ts
global Just_Rejected_Or_Expired
global Best_Symbol_by_Exchange
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)
ALGO_CONFIG = json.loads(VAL_KEY.get('fr_orchestrator_output'))
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.Min_Time_To_Funding_Minutes * 60 * 1000
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'))
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:
if ALGO_CONFIG.Overrides.Flip_Side_For_Testing:
ASTER_FUND_RATE = ASTER_FUND_RATE * -1
EXTEND_FUND_RATE = EXTEND_FUND_RATE * -1
@@ -370,16 +384,20 @@ async def run_algo():
if order_update_status in ['CANCELED','EXPIRED']:
logging.info(f'ASTER ORDER CANCELLED or EXPIRED: {order_id}')
ASTER_OPEN_ORDERS.pop(idx)
Just_Rejected_Or_Expired = True
utils.send_tg_alert(f'FR_ALGO - ASTER REJECTED ({order_id})')
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()
# await get_aster_collateral()
await get_aster_notional_position(resp=ASTER_WS_POS_UPDATES)
Last_Aster_Fill_Time_Ts = datetime.now().timestamp()*1000
utils.send_tg_alert(f'FR_ALGO - ASTER PARTIALLY FILLED ({order_id})')
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()
# await get_aster_collateral()
await get_aster_notional_position(resp=ASTER_WS_POS_UPDATES)
Last_Aster_Fill_Time_Ts = datetime.now().timestamp()*1000
utils.send_tg_alert(f'FR_ALGO - ASTER FILLED ({order_id})')
else:
logging.critical(f'EXTEND ORDER STATUS CHG TO UNEXPECTED VALUE, KILLING... ({order_id}): {order_orig_status} -> {order_update_status}')
@@ -401,6 +419,8 @@ async def run_algo():
if order_update_status in ['CANCELLED','EXPIRED','REJECTED']:
logging.info(f'EXTEND ORDER CANCELLED, REJECTED or EXPIRED: {order_id}')
EXTEND_OPEN_ORDERS.pop(idx)
Just_Rejected_Or_Expired = True
utils.send_tg_alert(f'FR_ALGO - EXTEND REJECTED ({order_id})')
elif order_update_status in ['PARTIALLY_FILLED']:
logging.info(f'EXTEND ORDER PARTIALLY FILLED: {order_id}')
await get_extend_collateral()
@@ -416,10 +436,20 @@ 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:
# 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']
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
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
@@ -429,20 +459,20 @@ async def run_algo():
if ALPHA_FUND_RATE < 0:
ALPHA_CARRY_SIDE = 'BUY'
ALPHA_TGT_NOTIONAL = ALGO_CONFIG.Max_Target_Notional
ALPHA_TGT_NOTIONAL = ALGO_CONFIG.Config.Max_Target_Notional
else:
ALPHA_CARRY_SIDE = 'SELL'
ALPHA_TGT_NOTIONAL = ALGO_CONFIG.Max_Target_Notional*-1
ALPHA_TGT_NOTIONAL = ALGO_CONFIG.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
return max([ASTER_FUND_RATE, EXTEND_FUND_RATE]) - min([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 >= ( (ALGO_CONFIG.Min_Fund_Rate_Pct_To_Trade*-1) / 100) ) and ( NEXT_NET_FUNDING_RATE <= ( ALGO_CONFIG.Min_Fund_Rate_Pct_To_Trade / 100 ) )
if Flags.NET_FUNDING_IS_ZERO or ALGO_CONFIG.Flatten_Open_Positions:
Flags.NET_FUNDING_IS_ZERO = ( NEXT_NET_FUNDING_RATE >= ( (ALGO_CONFIG.Config.Min_Fund_Rate_Pct_To_Trade*-1) / 100) ) and ( NEXT_NET_FUNDING_RATE <= ( ALGO_CONFIG.Config.Min_Fund_Rate_Pct_To_Trade / 100 ) )
if Flags.NET_FUNDING_IS_ZERO or ALGO_CONFIG.Overrides.Flatten_Open_Positions:
ALPHA_TGT_NOTIONAL = 0.00
# if ASTER_OPEN_ORDERS or EXTEND_OPEN_ORDERS:
# logging.info('NET FUNDING = 0.00; Cancelling Open Orders! then Waiting...')
@@ -507,26 +537,26 @@ async def run_algo():
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
Hedge_Ratio = abs(( abs( max([abs(float(EXTEND_NOTIONAL_POSITION)), 0.01]) / max([abs(float(ASTER_NOTIONAL_POSITION)), 0.01]) ) - 1 ) * 100)
# Hedge_Ratio = abs(( abs( max([abs(float(EXTEND_NOTIONAL_POSITION)), 0.01]) / max([abs(float(ASTER_NOTIONAL_POSITION)), 0.01]) ) - 1 ) * 100)
Hedge_Ratio = abs( ( EXTEND_NOTIONAL_POSITION + ASTER_NOTIONAL_POSITION ) / ASTER_NOTIONAL_POSITION ) * 100
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(f'''
LOOP SLEEP (SEC): {ALGO_CONFIG.Loop_Sleep_Sec}
FLIP SIDES FOR TESTING?: {ALGO_CONFIG.Flip_Side_For_Testing}; ASTER ORDER ENABLED? {ALGO_CONFIG.Allow_Ordering_Aster}; EXTEND ORDER ENABLED? {ALGO_CONFIG.Allow_Ordering_Extend}
LOOP SLEEP (SEC): {ALGO_CONFIG.Config.Loop_Sleep_Sec}
FLIP SIDES FOR TESTING?: {ALGO_CONFIG.Overrides.Flip_Side_For_Testing}; ASTER ORDER ENABLED? {ALGO_CONFIG.Overrides.Allow_Ordering_Aster}; EXTEND ORDER ENABLED? {ALGO_CONFIG.Overrides.Allow_Ordering_Extend}
{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} [Min: {ALGO_CONFIG.Min_Fund_Rate_Pct_To_Trade}]
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} [Min: {ALGO_CONFIG.Config.Min_Fund_Rate_Pct_To_Trade}]
ALPHA SIDE : {ALPHA_EXCH} [{ALPHA_CARRY_SIDE}]
TGT NOTIONAL: $ {abs(ALPHA_TGT_NOTIONAL):.2f}; Flatten Open Positions Flag? {ALGO_CONFIG.Flatten_Open_Positions}
TGT NOTIONAL: $ {abs(ALPHA_TGT_NOTIONAL):.2f}; Flatten Open Positions Flag? {ALGO_CONFIG.Overrides.Flatten_Open_Positions}
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}
@@ -543,35 +573,38 @@ async def run_algo():
--- EXTEND OPEN ORDERS ---
{EXTEND_OPEN_ORDERS}
''')
# ASTER: [ Available Collateral: {ASTER_AVAIL_COLLATERAL:.4f} ] | EXTEND: [ Available Collateral: {EXTEND_AVAIL_COLLATERAL:.4f} ]
# Try Making Hedge Order Contingent on Alpha Order Fills (Basically Hedge has to wait for sig Diff in Balance to order.) would improve when extended is thin (Overnight).
if ALGO_CONFIG.Log_Summary_Each_Loop:
if ALGO_CONFIG.Logging.Log_Summary_Each_Loop:
print_summary(use_logging=True)
if ALGO_CONFIG.Print_Summary_Each_Loop:
if ALGO_CONFIG.Logging.Print_Summary_Each_Loop:
print_summary(use_logging=False)
# print_summary()
### ROUTES ###
# MIN_EXPECTED_ALPHA_TO_TRADE = 0.0001
MIN_EXPECTED_ALPHA_TO_TRADE = -0.000001
# ALPHA RATIO CHECK
if not( ( Expected_Alpha_w_Taker > MIN_EXPECTED_ALPHA_TO_TRADE ) or ( ASTER_OPEN_ORDERS or EXTEND_OPEN_ORDERS ) or ALGO_CONFIG.Flatten_Open_Positions ):
# logging.info(f'Alpha Ratio too low ({ALPHA_RATIO:.8f}) and no Open Orders...')
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:
print(f'Alpha Ratio too low ({ALPHA_RATIO:.8f}) and no Open Orders...')
pass
elif ( Expected_Alpha_w_Taker <= MIN_EXPECTED_ALPHA_TO_TRADE ) and ( ASTER_OPEN_ORDERS or EXTEND_OPEN_ORDERS ) and Currently_Hedged:
elif ( Expected_Alpha_Net_FR_w_Taker <= MIN_EXPECTED_ALPHA_TO_TRADE ) and ( ASTER_OPEN_ORDERS or EXTEND_OPEN_ORDERS ) and Currently_Hedged:
await aster_cancel_all_orders()
await extend_cancel_all_orders()
logging.info('Expected_Alpha went away with open orders...cancelling since we are currently hedged...')
time.sleep( (1/1000)*100 ) # 100ms wait for ws cancel response
# time.sleep( (1/1000)*100 ) # 100ms wait for ws cancel response
else:
# logging.info(f'*** Alpha Ratio HIT - LETS ORDER: {ALPHA_RATIO:.8f}')
# ASTER
if ASTER_TGT_TAIL_ORDERABLE and ALGO_CONFIG.Allow_Ordering_Aster:
if ASTER_TGT_TAIL_ORDERABLE and ALGO_CONFIG.Overrides.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
price = ASTER_TOB_PX - ALGO_CONFIG.Config.Price_Worsener_Aster if side == 'BUY' else ASTER_TOB_PX + ALGO_CONFIG.Config.Price_Worsener_Aster
if abs( ( float(ASTER_TGT_TAIL_BASE_QTY)*float(price) ) + ASTER_NOTIONAL_POSITION ) > ALGO_CONFIG.Max_Target_Notional*1.01:
if abs( ( float(ASTER_TGT_TAIL_BASE_QTY)*float(price) ) + ASTER_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 - 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:
@@ -622,6 +655,7 @@ async def run_algo():
order_resp['original_price'] = price
order_resp['order_status'] = order_resp['status']
ASTER_OPEN_ORDERS.append(order_resp)
Just_Rejected_Or_Expired = False
utils.send_tg_alert(f'FR_ALGO - ASTER Order ({order_resp['orderId']}). 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)
@@ -634,13 +668,20 @@ async def run_algo():
await aster_cancel_all_orders()
# EXTEND
if EXTEND_TGT_TAIL_ORDERABLE and ALGO_CONFIG.Allow_Ordering_Extend:
symbol = EXTEND_TICKER
if EXTEND_TGT_TAIL_ORDERABLE and ALGO_CONFIG.Overrides.Allow_Ordering_Extend:
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
else:
post_only = True
price = EXTEND_TOB_PX
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)))
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:
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})')
await kill_algo()
if EXTEND_OPEN_ORDERS:
@@ -667,17 +708,20 @@ async def run_algo():
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(
market_name=symbol,
amount_of_synthetic=qty,
price=price,
side=side,
taker_fee=Decimal("0.00025"),
taker_fee=taker_fee,
previous_order_id=open_order_id,
post_only=post_only,
)
except Exception as e:
logging.error(f'EXTEND ORDER PLACEMENT FAILED - RESP: {order_resp}')
logging.error(f'EXTEND ORDER PLACEMENT FAILED: {e}')
logging.error(f'EXTEND ORDER PLACEMENT FAILED - POSTED: market_name:{symbol}, amount_of_synthetic:{qty}, price:{price}, side:{side},taker_fee:{taker_fee}, previous_order_id:{open_order_id}, post_only:{post_only}')
logging.error(traceback.format_exc())
order_resp_dict = dict(order_resp)
@@ -694,6 +738,7 @@ async def run_algo():
order_dict['side'] = str(side)
EXTEND_OPEN_ORDERS.append(order_dict)
Just_Rejected_Or_Expired = False
utils.send_tg_alert(f'FR_ALGO - EXTEND Order ({order_dict.get('id', None)}). 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)
@@ -710,8 +755,8 @@ async def run_algo():
if ASTER_OPEN_ORDERS or EXTEND_OPEN_ORDERS:
continue
else:
time.sleep(ALGO_CONFIG.Loop_Sleep_Sec)
# print(f'_____ End No Open Orders _____ (Algo Engine ms: {(time.time() - loop_start)*1000:.2f}); Sleeping for sec: {ALGO_CONFIG.Loop_Sleep_Sec:.0f}')
time.sleep(ALGO_CONFIG.Config.Loop_Sleep_Sec)
# print(f'_____ End No Open Orders _____ (Algo Engine ms: {(time.time() - loop_start)*1000:.2f}); Sleeping for sec: {ALGO_CONFIG.Config.Loop_Sleep_Sec:.0f}')
except KeyboardInterrupt:
logging.info('CANCELLING OPEN ORDERS')
@@ -736,10 +781,15 @@ async def main():
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)
ALGO_CONFIG = json.load(file)
ALGO_CONFIG = structs.Algo_Config(**ALGO_CONFIG)
VAL_KEY.set('fr_orchestrator_output', json.dumps(asdict(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()))
async with engine.connect() as CON:
### ASTER SETUP ###

View File

@@ -3,25 +3,39 @@ from dataclasses import dataclass, field
from typing import Any
import valkey
from pydantic import BaseModel
@dataclass(kw_only=True)
class Algo_Config:
Config_Updated_Timestamp: int
# @dataclass(kw_only=True)
class Algo_Config_Overrides(BaseModel):
Allow_Ordering_Aster: bool
Allow_Ordering_Extend: bool
Flatten_Open_Positions: bool
Flip_Side_For_Testing: bool
# @dataclass(kw_only=True)
class Algo_Config_Config(BaseModel):
Loop_Sleep_Sec: int
Max_Order_Over_Notional_Ratio: float
Max_Target_Notional: float
Min_Time_To_Funding_Minutes: int
Min_Fund_Rate_Pct_To_Trade: float
Price_Worsener_Aster: float
Price_Worsener_Extend: float
Switch_To_Taker_Seconds: int
Target_Open_Cash_Position: int
Log_Summary_Each_Loop: bool = False
Print_Summary_Each_Loop: bool = False
Flatten_Open_Positions: bool = False
Flip_Side_For_Testing: bool = False
# @dataclass(kw_only=True)
class Algo_Config_Logging(BaseModel):
Log_Summary_Each_Loop: bool
Print_Summary_Each_Loop: bool
# @dataclass(kw_only=True)
class Algo_Config(BaseModel):
Updated_Timestamp: int
Config: Algo_Config_Config
Logging: Algo_Config_Logging
Overrides: Algo_Config_Overrides
@dataclass(kw_only=True)
class Flags:

View File

@@ -22,4 +22,5 @@ nicegui
# grpcio-tools==1.76.0
x10-python-trading-starknet
eth-keys
eth-account
eth-account
pydantic

110
ws_aster_fund_rate_all.py Normal file
View File

@@ -0,0 +1,110 @@
import asyncio
import json
import logging
import socket
import traceback
from datetime import datetime, timezone
from typing import AsyncContextManager
import math
import numpy as np
import pandas as pd
import requests.packages.urllib3.util.connection as urllib3_cn # type: ignore
from sqlalchemy import text
import websockets
from sqlalchemy.ext.asyncio import create_async_engine
import valkey
import os
from dotenv import load_dotenv
import modules.utils as utils
### Allow only ipv4 ###
def allowed_gai_family():
return socket.AF_INET
urllib3_cn.allowed_gai_family = allowed_gai_family
### Database ###
USE_DB: bool = False
USE_VK: bool = True
VK_FUND_RATE_ALL = 'fund_rate_aster_all'
CON: AsyncContextManager | None = None
VAL_KEY = None
### Logging ###
load_dotenv()
LOG_FILEPATH: str = os.getenv("LOGS_PATH") + '/Fund_Rate_Aster_FR_ALL.log'
### Globals ###
WSS_URL = "wss://fstream.asterdex.com/ws/!markPrice@arr"
### Websocket ###
async def ws_stream():
async for websocket in websockets.connect(WSS_URL):
logging.info(f"Connected to {WSS_URL}")
try:
async for message in websocket:
if isinstance(message, str):
try:
data = json.loads(message)
if data:
VAL_KEY.set(VK_FUND_RATE_ALL, json.dumps(data))
# print(f'VK_SAVED: {len(data)}')
continue
else:
logging.info(f'Initial or unexpected data struct, skipping: {data}')
continue
except (json.JSONDecodeError, ValueError):
logging.warning(f'Message not in JSON format, skipping: {message}')
continue
else:
raise ValueError(f'Type: {type(data)} not expected: {message}')
except websockets.ConnectionClosed as e:
logging.error(f'Connection closed: {e}')
logging.error(traceback.format_exc())
continue
except Exception as e:
logging.error(f'Connection closed: {e}')
logging.error(traceback.format_exc())
### Startup ###
async def main():
global VAL_KEY
global CON
if USE_VK:
VAL_KEY = valkey.Valkey(host='localhost', port=6379, db=0)
else:
VAL_KEY = None
logging.warning("VALKEY NOT BEING USED, NO DATA WILL BE PUBLISHED")
if USE_DB:
engine = create_async_engine('mysql+asyncmy://root:pwd@localhost/fund_rate')
async with engine.connect() as CON:
# await create_rtds_btcusd_table(CON=CON)
await ws_stream()
else:
CON = None
logging.warning("DATABASE NOT BEING USED, NO DATA WILL BE RECORDED")
await ws_stream()
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}")
try:
asyncio.run(main())
except KeyboardInterrupt:
logging.info("Stream stopped")

View File

@@ -0,0 +1,133 @@
import asyncio
import json
import logging
import socket
import traceback
from datetime import datetime, timezone
from typing import AsyncContextManager
import math
import numpy as np
import pandas as pd
import requests.packages.urllib3.util.connection as urllib3_cn # type: ignore
from sqlalchemy import text
import websockets
from sqlalchemy.ext.asyncio import create_async_engine
import valkey
import os
from dotenv import load_dotenv
import modules.utils as utils
### Allow only ipv4 ###
def allowed_gai_family():
return socket.AF_INET
urllib3_cn.allowed_gai_family = allowed_gai_family
### Database ###
USE_DB: bool = False
USE_VK: bool = True
VK_FUND_RATE = 'fund_rate_extended'
VK_FUND_RATE_ALL = 'fund_rate_extended_all'
CON: AsyncContextManager | None = None
VAL_KEY = None
### Logging ###
load_dotenv()
LOG_FILEPATH: str = os.getenv("LOGS_PATH") + '/Fund_Rate_Extended_FR_ALL.log'
### Globals ###
WSS_URL = "wss://api.starknet.extended.exchange/stream.extended.exchange/v1/funding/"
LOCAL_FUNDING_RATES = []
def time_round_down(dt, interval_mins=5) -> int: # returns timestamp in seconds
interval_secs = interval_mins * 60
seconds = dt.timestamp()
rounded_seconds = math.floor(seconds / interval_secs) * interval_secs
return rounded_seconds
### Websocket ###
async def ws_stream():
global LOCAL_FUNDING_RATES
async for websocket in websockets.connect(WSS_URL):
logging.info(f"Connected to {WSS_URL}")
try:
async for message in websocket:
ts_arrival = round(datetime.now().timestamp()*1000)
if isinstance(message, str):
try:
data = json.loads(message)
if data.get('data', None) is not None:
# print(f'FR: {data}')
# fr_next_update_ts = (time_round_down(dt=datetime.now(timezone.utc), interval_mins=60)+(60*60))*1000
fr_update = {
'sequence_id': data['seq'],
'timestamp_arrival': ts_arrival,
'timestamp_msg': data['ts'],
'symbol': data['data']['m'],
'funding_rate': float(data['data']['f']),
'funding_rate_updated_ts_ms': data['data']['T'],
# 'next_funding_time_ts_ms': fr_next_update_ts,
}
LOCAL_FUNDING_RATES = utils.upsert_list_of_dicts_by_id(LOCAL_FUNDING_RATES, fr_update, id='symbol', seq_check_field='sequence_id')
VAL_KEY.set(VK_FUND_RATE_ALL, json.dumps(LOCAL_FUNDING_RATES))
# print(f'VK_SAVED: {data}')
continue
else:
logging.info(f'Initial or unexpected data struct, skipping: {data}')
continue
except (json.JSONDecodeError, ValueError):
logging.warning(f'Message not in JSON format, skipping: {message}')
continue
else:
raise ValueError(f'Type: {type(data)} not expected: {message}')
except websockets.ConnectionClosed as e:
logging.error(f'Connection closed: {e}')
logging.error(traceback.format_exc())
continue
except Exception as e:
logging.error(f'Connection closed: {e}')
logging.error(traceback.format_exc())
### Startup ###
async def main():
global VAL_KEY
global CON
if USE_VK:
VAL_KEY = valkey.Valkey(host='localhost', port=6379, db=0)
else:
VAL_KEY = None
logging.warning("VALKEY NOT BEING USED, NO DATA WILL BE PUBLISHED")
if USE_DB:
engine = create_async_engine('mysql+asyncmy://root:pwd@localhost/fund_rate')
async with engine.connect() as CON:
# await create_rtds_btcusd_table(CON=CON)
await ws_stream()
else:
CON = None
logging.warning("DATABASE NOT BEING USED, NO DATA WILL BE RECORDED")
await ws_stream()
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}")
try:
asyncio.run(main())
except KeyboardInterrupt:
logging.info("Stream stopped")