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, "Updated_Timestamp": 1777433705326,
"Allow_Ordering_Aster": false, "Config": {
"Allow_Ordering_Extend": false, "Loop_Sleep_Sec": 0.0,
"Loop_Sleep_Sec": 5.00, "Max_Order_Over_Notional_Ratio": 1.05,
"Max_Target_Notional": 0.00, "Max_Target_Notional": 0.0,
"Min_Time_To_Funding_Minutes": 10, "Min_Time_To_Funding_Minutes": 60,
"Price_Worsener_Aster": 0.0, "Min_Fund_Rate_Pct_To_Trade": 0.0,
"Price_Worsener_Extend": 0.0, "Price_Worsener_Aster": 0.0,
"Target_Open_Cash_Position": 10, "Price_Worsener_Extend": -0.1,
"Print_Summary_Each_Loop" : true, "Switch_To_Taker_Seconds": 1,
"Flip_Side_For_Testing": false "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": [ "cells": [
{ {
"cell_type": "code", "cell_type": "code",
"execution_count": 4, "execution_count": 22,
"id": "d1eed397", "id": "d1eed397",
"metadata": {}, "metadata": {},
"outputs": [], "outputs": [],
@@ -13,12 +13,13 @@
"import valkey\n", "import valkey\n",
"\n", "\n",
"with open('algo_config.json', 'r', encoding='utf-8') as file:\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", "cell_type": "code",
"execution_count": 5, "execution_count": 23,
"id": "c6151613", "id": "c6151613",
"metadata": {}, "metadata": {},
"outputs": [], "outputs": [],
@@ -38,23 +39,31 @@
"1" "1"
] ]
}, },
"execution_count": 12, "execution_count": 4,
"metadata": {}, "metadata": {},
"output_type": "execute_result" "output_type": "execute_result"
} }
], ],
"source": [ "source": [
"config_update = {\n", "config_update = {\n",
" # 'Min_Time_To_Funding_Minutes': 60,\n", " # 'Config': {\n",
" # 'Allow_Ordering_Aster': True,\n", " # 'Loop_Sleep_Sec': 0.00,\n",
" # 'Allow_Ordering_Extend': True,\n", " # 'Min_Time_To_Funding_Minutes': 60,\n",
" # 'Loop_Sleep_Sec': 0.00,\n", " # 'Min_Fund_Rate_Pct_To_Trade': 0.001,\n",
" # 'Min_Fund_Rate_Pct_To_Trade': 0.001,\n", " # 'Price_Worsener_Extend': 0.0,\n",
" # 'Flip_Side_For_Testing': False,\n", " # 'Price_Worsener_Aster': 0.0,\n",
" # 'Price_Worsener_Extend': 0.0,\n", " # 'Switch_To_Taker_Seconds': 1,\n",
" # 'Log_Summary_Each_Loop': False,\n", " # },\n",
" 'Print_Summary_Each_Loop': True,\n", " 'Logging': {\n",
" 'Flatten_Open_Positions': False,\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", "}\n",
"VAL_KEY.publish('fr_orchestrator_input', json.dumps(config_update))" "VAL_KEY.publish('fr_orchestrator_input', json.dumps(config_update))"
] ]

View File

@@ -1,16 +1,24 @@
{ {
"Config_Updated_Timestamp": 1777411434591, "Updated_Timestamp": 1777478176165,
"Allow_Ordering_Aster": true, "Config": {
"Allow_Ordering_Extend": true, "Loop_Sleep_Sec": 0.0,
"Loop_Sleep_Sec": 0.0, "Max_Order_Over_Notional_Ratio": 1.05,
"Max_Target_Notional": 0.0, "Max_Target_Notional": 0.0,
"Min_Time_To_Funding_Minutes": 60, "Min_Time_To_Funding_Minutes": 60,
"Min_Fund_Rate_Pct_To_Trade": 0.0, "Min_Fund_Rate_Pct_To_Trade": 0.0,
"Price_Worsener_Aster": 0.0, "Price_Worsener_Aster": 0.0,
"Price_Worsener_Extend": -0.1, "Price_Worsener_Extend": -0.1,
"Target_Open_Cash_Position": 10, "Switch_To_Taker_Seconds": 1,
"Log_Summary_Each_Loop": false, "Target_Open_Cash_Position": 10
"Print_Summary_Each_Loop": true, },
"Flatten_Open_Positions": false, "Logging": {
"Flip_Side_For_Testing": false "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' LOG_FILEPATH: str = os.getenv("LOGS_PATH") + '/Fund_Rate_Algo_Orchestrator.log'
ALGO_CONFIG: None | dict ALGO_CONFIG: None | dict
# ALGO_CONFIG: None | Algo_Config = None
async def orchestrator() -> None: async def orchestrator() -> None:
global ALGO_CONFIG global ALGO_CONFIG
@@ -35,7 +34,7 @@ async def orchestrator() -> None:
VK_PUBSUB = VAL_KEY.pubsub() VK_PUBSUB = VAL_KEY.pubsub()
VK_PUBSUB.subscribe(VK_IN) 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(): for message in VK_PUBSUB.listen():
if message['type'] == 'message': 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: 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, object_hook=lambda d: Algo_Config(**d))
ALGO_CONFIG = json.load(f) ALGO_CONFIG = json.load(f)
ALGO_CONFIG['Config_Updated_Timestamp'] = timestamp ALGO_CONFIG['Updated_Timestamp'] = timestamp
for k, v in data.items(): for k, v in data.items():
if ALGO_CONFIG.get(k, None) is not None: 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)) VAL_KEY.set(VK_OUT, json.dumps(ALGO_CONFIG))
with open('/algo_local_drive/algo_config.json', 'w', encoding='utf-8') as f: with open('/algo_local_drive/algo_config.json', 'w', encoding='utf-8') as f:
# print('SAVING FILE')
json.dump(ALGO_CONFIG, f, indent=4) 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: 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: except KeyboardInterrupt:
logging.info('ORCHESTRATOR SHUTTING DOWN...') logging.info('ORCHESTRATOR SHUTTING DOWN...')
except Exception as e: 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: 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, object_hook=lambda d: Algo_Config(**d))
ALGO_CONFIG = json.load(f) 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: # async with engine.connect() as CON:
await orchestrator() 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: services:
# algo: # 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": [ "cells": [
{ {
"cell_type": "code", "cell_type": "code",
"execution_count": 1, "execution_count": 9,
"id": "6c70a8c3", "id": "6c70a8c3",
"metadata": {}, "metadata": {},
"outputs": [], "outputs": [],
@@ -27,7 +27,7 @@
}, },
{ {
"cell_type": "code", "cell_type": "code",
"execution_count": 2, "execution_count": 12,
"id": "ff971ca9", "id": "ff971ca9",
"metadata": {}, "metadata": {},
"outputs": [], "outputs": [],
@@ -42,50 +42,46 @@
"CONFIG = MAINNET_CONFIG\n", "CONFIG = MAINNET_CONFIG\n",
"\n", "\n",
"ORDER_MARKET = \"ETH-USD\"\n", "ORDER_MARKET = \"ETH-USD\"\n",
"ORDER_SIDE = OrderSide.BUY\n",
"ORDER_QTY = Decimal(\"0.01\")\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", "cell_type": "code",
"execution_count": 3, "execution_count": 13,
"id": "fc2c6d2b", "id": "fc2c6d2b",
"metadata": {}, "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": [ "outputs": [
{ {
"name": "stderr", "name": "stderr",
"output_type": "stream", "output_type": "stream",
"text": [ "text": [
"Error response from https://api.starknet.extended.exchange/api/v1/user/order: {\"status\":\"ERROR\",\"error\":{\"code\":1125,\"message\":\"Invalid price precision\"}}\n" "Unclosed client session\n",
] "client_session: <aiohttp.client.ClientSession object at 0x7b2dc26ba850>\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\"}}"
] ]
} }
], ],
"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": [ "source": [
"placed_order = await trading_client.place_order(\n", "placed_order = await trading_client.place_order(\n",
" market_name=ORDER_MARKET,\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", "cell_type": "code",
"execution_count": 5, "execution_count": 5,

176
main.py
View File

@@ -5,7 +5,6 @@ import math
import os import os
import time import time
import traceback import traceback
from dataclasses import asdict, dataclass, field
from datetime import datetime, timezone from datetime import datetime, timezone
from decimal import ROUND_DOWN, Decimal from decimal import ROUND_DOWN, Decimal
from typing import AsyncContextManager from typing import AsyncContextManager
@@ -37,24 +36,30 @@ load_dotenv()
LOG_FILEPATH: str = os.getenv("LOGS_PATH") + '/Fund_Rate_Algo.log' LOG_FILEPATH: str = os.getenv("LOGS_PATH") + '/Fund_Rate_Algo.log'
### Algo Config ### ### Algo Config ###
ALGO_CONFIG: structs.Algo_Config = None ALGO_CONFIG: structs.Algo_Config | None = None
MIN_TIME_TO_FUNDING: int MIN_TIME_TO_FUNDING: int
### CONSTANTS ### ### EXCHANGES ###
ASTER = structs.Perpetual_Exchange( ASTER = structs.Perpetual_Exchange(
mult = 150, mult = 150,
lh_asset = 'ETH', lh_asset = 'ETH',
rh_asset = 'USDT', rh_asset = 'USDT',
symbol_asset_separator = '', symbol_asset_separator = '',
) )
EXTEND = structs.Perpetual_Exchange(
EXTEND_LH_ASSET: str = 'ETH' mult = 50,
EXTEND_RH_ASSET: str = 'USD' lh_asset = 'ETH',
EXTEND_TICKER: str = EXTEND_LH_ASSET + '-' + EXTEND_RH_ASSET rh_asset = 'USD',
symbol_asset_separator = '-',
)
### GLOBALS ### ### GLOBALS ###
ASTER_MULT = 150 Last_Aster_Fill_Time_Ts: float = 0.00
EXTEND_MULT = 50 Just_Rejected_Or_Expired: bool = False
Best_Symbol_by_Exchange: dict = {}
# ASTER_MULT = 150
# EXTEND_MULT = 50
ASTER_MIN_ORDER_QTY = 0.001 ASTER_MIN_ORDER_QTY = 0.001
EXTEND_MIN_ORDER_QTY = 0.01 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_OBJ
global ASTER_NOTIONAL_POSITION global ASTER_NOTIONAL_POSITION
global ASTER_UNREALIZED_PNL global ASTER_UNREALIZED_PNL
global ASTER_MULT global ASTER
previous_notional_obj = ASTER_NOTIONAL_OBJ 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 previous_notional_position = ASTER_NOTIONAL_POSITION
# if not resp: # this can never evaluate # if not resp: # this can never evaluate
# ASTER_MULT = float(d['leverage']) # ASTER.mult = float(d['leverage'])
# if abs(ASTER_NOTIONAL_POSITION) > ALGO_CONFIG.Max_Target_Notional*1.01: if abs(ASTER_NOTIONAL_POSITION) > ALGO_CONFIG.Config.Max_Target_Notional*ALGO_CONFIG.Config.Max_Order_Over_Notional_Ratio:
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}')
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() await kill_algo()
# if ASTER_NOTIONAL_POSITION != previous_notional_position: # 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(): async def get_extend_collateral():
global EXTEND_AVAIL_COLLATERAL global EXTEND_AVAIL_COLLATERAL
get_bals = dict(dict(await EXTEND_CLIENT.account.get_balance()).get('data', {})) 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): async def get_extend_notional(resp: dict | None = None):
global EXTEND_NOTIONAL_OBJ global EXTEND_NOTIONAL_OBJ
global EXTEND_NOTIONAL_POSITION global EXTEND_NOTIONAL_POSITION
global EXTEND_UNREALIZED_PNL global EXTEND_UNREALIZED_PNL
global EXTEND_MULT global EXTEND
previous_notional_obj = EXTEND_NOTIONAL_OBJ previous_notional_obj = EXTEND_NOTIONAL_OBJ
previous_notional_position = EXTEND_NOTIONAL_POSITION previous_notional_position = EXTEND_NOTIONAL_POSITION
if not resp: if not resp:
resp = dict(await EXTEND_CLIENT.account.get_positions()).get('data', []) 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: if pos_dict:
pos_dict = pos_dict[0] pos_dict = pos_dict[0]
else: else:
@@ -195,7 +199,7 @@ async def get_extend_notional(resp: dict | None = None):
pos_dict['timestamp_arrival'] = round(datetime.now().timestamp()*1000) pos_dict['timestamp_arrival'] = round(datetime.now().timestamp()*1000)
else: 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: if pos_dict:
pos_dict = pos_dict[0] pos_dict = pos_dict[0]
else: 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}') logging.info(f'EXTEND BAD SIDE ON POSITION UPDATE: {pos_dict}')
EXTEND_NOTIONAL_POSITION = notional_pos_sided - float(EXTEND_UNREALIZED_PNL) EXTEND_NOTIONAL_POSITION = notional_pos_sided - float(EXTEND_UNREALIZED_PNL)
EXTEND_MULT = pos_dict.get('leverage', EXTEND_MULT) EXTEND.mult = pos_dict.get('leverage', EXTEND)
if abs(EXTEND_NOTIONAL_POSITION) > ALGO_CONFIG.Max_Target_Notional*2.01: 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}') 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() await kill_algo()
if EXTEND_NOTIONAL_POSITION != previous_notional_position: 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 ### ### EXCHANGE INFO ###
async def get_aster_exch_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}') logging.info(f'ASTER CANCEL ALL OPEN ORDERS RESP: {r}')
async def extend_cancel_all_orders(): 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}') logging.info(f'EXTEND CANCEL ALL OPEN ORDERS RESP: {r}')
### KILL ALGO ### ### KILL ALGO ###
@@ -277,29 +281,39 @@ async def kill_algo():
### ALGO LOOP ### ### ALGO LOOP ###
async def run_algo(): async def run_algo():
global ASTER
global EXTEND
global ALGO_CONFIG global ALGO_CONFIG
global MIN_TIME_TO_FUNDING global MIN_TIME_TO_FUNDING
global ASTER_OPEN_ORDERS global ASTER_OPEN_ORDERS
global EXTEND_OPEN_ORDERS global EXTEND_OPEN_ORDERS
global Last_Aster_Fill_Time_Ts
global Just_Rejected_Or_Expired
global Best_Symbol_by_Exchange
try: try:
while True: while True:
loop_start = time.time() loop_start = time.time()
# print('__________Start___________') # print('__________Start___________')
### ALGO CONIFG ### ### 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)
MIN_TIME_TO_FUNDING = ALGO_CONFIG.Min_Time_To_Funding_Minutes * 60 * 1000 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.Config.Min_Time_To_Funding_Minutes * 60 * 1000
### Load Data from Feedhandlers ### ### 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')) ASTER_FUND_RATE_DICT = json.loads(VAL_KEY.get('fund_rate_aster'))
EXTENDED_FUND_RATE_DICT = json.loads(VAL_KEY.get('fund_rate_extended')) EXTENDED_FUND_RATE_DICT = json.loads(VAL_KEY.get('fund_rate_extended'))
ASTER_FUND_RATE = float(ASTER_FUND_RATE_DICT.get('funding_rate', 0)) ASTER_FUND_RATE = float(ASTER_FUND_RATE_DICT.get('funding_rate', 0))
EXTEND_FUND_RATE = float(EXTENDED_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 ASTER_FUND_RATE = ASTER_FUND_RATE * -1
EXTEND_FUND_RATE = EXTEND_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']: if order_update_status in ['CANCELED','EXPIRED']:
logging.info(f'ASTER ORDER CANCELLED or EXPIRED: {order_id}') logging.info(f'ASTER ORDER CANCELLED or EXPIRED: {order_id}')
ASTER_OPEN_ORDERS.pop(idx) 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']: elif order_update_status in ['PARTIALLY_FILLED']:
logging.info(f'ASTER ORDER PARTIALLY FILLED: {order_id}') logging.info(f'ASTER ORDER PARTIALLY FILLED: {order_id}')
await get_aster_collateral() # await get_aster_collateral()
await get_aster_notional_position() 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})') utils.send_tg_alert(f'FR_ALGO - ASTER PARTIALLY FILLED ({order_id})')
elif order_update_status in ['FILLED']: elif order_update_status in ['FILLED']:
logging.info(f'ASTER ORDER FILLED: {order_id}') logging.info(f'ASTER ORDER FILLED: {order_id}')
ASTER_OPEN_ORDERS.pop(idx) ASTER_OPEN_ORDERS.pop(idx)
await get_aster_collateral() # await get_aster_collateral()
await get_aster_notional_position() 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})') utils.send_tg_alert(f'FR_ALGO - ASTER FILLED ({order_id})')
else: else:
logging.critical(f'EXTEND ORDER STATUS CHG TO UNEXPECTED VALUE, KILLING... ({order_id}): {order_orig_status} -> {order_update_status}') 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']: if order_update_status in ['CANCELLED','EXPIRED','REJECTED']:
logging.info(f'EXTEND ORDER CANCELLED, REJECTED or EXPIRED: {order_id}') logging.info(f'EXTEND ORDER CANCELLED, REJECTED or EXPIRED: {order_id}')
EXTEND_OPEN_ORDERS.pop(idx) 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']: elif order_update_status in ['PARTIALLY_FILLED']:
logging.info(f'EXTEND ORDER PARTIALLY FILLED: {order_id}') logging.info(f'EXTEND ORDER PARTIALLY FILLED: {order_id}')
await get_extend_collateral() 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}') 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)) 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 = min_between_fundings < 5
if ( abs(ASTER_FUND_RATE) > abs(EXTEND_FUND_RATE) ) and FUNDINGS_AT_SAME_TIME_NEXT_HR: if ( abs(ASTER_FUND_RATE) > abs(EXTEND_FUND_RATE) ) and FUNDINGS_AT_SAME_TIME_NEXT_HR:
ALPHA_EXCH = 'ASTER' ALPHA_EXCH = 'ASTER'
ALPHA_FUND_RATE = ASTER_FUND_RATE ALPHA_FUND_RATE = ASTER_FUND_RATE
@@ -429,20 +459,20 @@ async def run_algo():
if ALPHA_FUND_RATE < 0: if ALPHA_FUND_RATE < 0:
ALPHA_CARRY_SIDE = 'BUY' ALPHA_CARRY_SIDE = 'BUY'
ALPHA_TGT_NOTIONAL = ALGO_CONFIG.Max_Target_Notional ALPHA_TGT_NOTIONAL = ALGO_CONFIG.Config.Max_Target_Notional
else: else:
ALPHA_CARRY_SIDE = 'SELL' 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: def calc_next_net_fund_rate(FUNDINGS_AT_SAME_TIME_NEXT_HR: bool) -> float:
if FUNDINGS_AT_SAME_TIME_NEXT_HR: 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: else:
return EXTEND_FUND_RATE return EXTEND_FUND_RATE
NEXT_NET_FUNDING_RATE = calc_next_net_fund_rate(FUNDINGS_AT_SAME_TIME_NEXT_HR) 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 ) ) 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.Flatten_Open_Positions: if Flags.NET_FUNDING_IS_ZERO or ALGO_CONFIG.Overrides.Flatten_Open_Positions:
ALPHA_TGT_NOTIONAL = 0.00 ALPHA_TGT_NOTIONAL = 0.00
# if ASTER_OPEN_ORDERS or EXTEND_OPEN_ORDERS: # if ASTER_OPEN_ORDERS or EXTEND_OPEN_ORDERS:
# logging.info('NET FUNDING = 0.00; Cancelling Open Orders! then Waiting...') # 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 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 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 Currently_Hedged = Hedge_Ratio < 1.00
def print_summary(use_logging: bool = False): def print_summary(use_logging: bool = False):
OUT: print | logging.info = logging.info if use_logging else print OUT: print | logging.info = logging.info if use_logging else print
OUT(f''' OUT(f'''
LOOP SLEEP (SEC): {ALGO_CONFIG.Loop_Sleep_Sec} LOOP SLEEP (SEC): {ALGO_CONFIG.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} 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()):}) {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_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: {'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} ] 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} ] 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}] 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_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} 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 ---
{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). # 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) 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(use_logging=False)
# print_summary() # print_summary()
### ROUTES ### ### ROUTES ###
# MIN_EXPECTED_ALPHA_TO_TRADE = 0.0001 # MIN_EXPECTED_ALPHA_TO_TRADE = 0.0001
MIN_EXPECTED_ALPHA_TO_TRADE = -0.000001 MIN_EXPECTED_ALPHA_TO_TRADE = -0.000001
# ALPHA RATIO CHECK # 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 ): 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) ):
# logging.info(f'Alpha Ratio too low ({ALPHA_RATIO:.8f}) and no Open Orders...') if ALGO_CONFIG.Logging.Print_Summary_Each_Loop:
print(f'Alpha Ratio too low ({ALPHA_RATIO:.8f}) and no Open Orders...')
pass 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 aster_cancel_all_orders()
await extend_cancel_all_orders() await extend_cancel_all_orders()
logging.info('Expected_Alpha went away with open orders...cancelling since we are currently hedged...') 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: else:
# logging.info(f'*** Alpha Ratio HIT - LETS ORDER: {ALPHA_RATIO:.8f}') # logging.info(f'*** Alpha Ratio HIT - LETS ORDER: {ALPHA_RATIO:.8f}')
# ASTER # 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 symbol = ASTER.symbol
side = 'BUY' if ASTER_TGT_TAIL_BASE_QTY > 0.00 else 'SELL' side = 'BUY' if ASTER_TGT_TAIL_BASE_QTY > 0.00 else 'SELL'
qty = str(abs(ASTER_TGT_TAIL_BASE_QTY)) 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})') 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() await kill_algo()
if ASTER_OPEN_ORDERS: if ASTER_OPEN_ORDERS:
@@ -622,6 +655,7 @@ async def run_algo():
order_resp['original_price'] = price order_resp['original_price'] = price
order_resp['order_status'] = order_resp['status'] order_resp['order_status'] = order_resp['status']
ASTER_OPEN_ORDERS.append(order_resp) 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}') 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}') logging.info(f'ASTER ORDER PLACED SUCCESS: {order_resp}')
print_summary(use_logging=True) print_summary(use_logging=True)
@@ -634,13 +668,20 @@ async def run_algo():
await aster_cancel_all_orders() await aster_cancel_all_orders()
# EXTEND # EXTEND
if EXTEND_TGT_TAIL_ORDERABLE and ALGO_CONFIG.Allow_Ordering_Extend: if EXTEND_TGT_TAIL_ORDERABLE and ALGO_CONFIG.Overrides.Allow_Ordering_Extend:
symbol = EXTEND_TICKER 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 side = OrderSide.BUY if EXTEND_TGT_TAIL_BASE_QTY > 0.00 else OrderSide.SELL
qty = Decimal(str(abs(EXTEND_TGT_TAIL_BASE_QTY))) qty = Decimal(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})') 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() await kill_algo()
if EXTEND_OPEN_ORDERS: if EXTEND_OPEN_ORDERS:
@@ -667,17 +708,20 @@ async def run_algo():
logging.info('EXTEND OPEN ORDER NO PX CHG; SKIPPING') logging.info('EXTEND OPEN ORDER NO PX CHG; SKIPPING')
else: else:
try: try:
taker_fee = taker_fee=Decimal("0.00000") if post_only else Decimal("0.00025")
order_resp = await EXTEND_CLIENT.place_order( order_resp = await EXTEND_CLIENT.place_order(
market_name=symbol, market_name=symbol,
amount_of_synthetic=qty, amount_of_synthetic=qty,
price=price, price=price,
side=side, side=side,
taker_fee=Decimal("0.00025"), taker_fee=taker_fee,
previous_order_id=open_order_id, previous_order_id=open_order_id,
post_only=post_only,
) )
except Exception as e: except Exception as e:
logging.error(f'EXTEND ORDER PLACEMENT FAILED - RESP: {order_resp}') 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: {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()) logging.error(traceback.format_exc())
order_resp_dict = dict(order_resp) order_resp_dict = dict(order_resp)
@@ -694,6 +738,7 @@ async def run_algo():
order_dict['side'] = str(side) order_dict['side'] = str(side)
EXTEND_OPEN_ORDERS.append(order_dict) 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}') 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}') logging.info(f'EXTEND ORDER PLACED SUCCESS: {order_dict}')
print_summary(use_logging=True) print_summary(use_logging=True)
@@ -710,8 +755,8 @@ async def run_algo():
if ASTER_OPEN_ORDERS or EXTEND_OPEN_ORDERS: if ASTER_OPEN_ORDERS or EXTEND_OPEN_ORDERS:
continue continue
else: else:
time.sleep(ALGO_CONFIG.Loop_Sleep_Sec) 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.Loop_Sleep_Sec:.0f}') # 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: except KeyboardInterrupt:
logging.info('CANCELLING OPEN ORDERS') logging.info('CANCELLING OPEN ORDERS')
@@ -736,10 +781,15 @@ async def main():
engine = create_async_engine('mysql+asyncmy://root:pwd@localhost/fund_rate') engine = create_async_engine('mysql+asyncmy://root:pwd@localhost/fund_rate')
with open('algo_config.json', 'r', encoding='utf-8') as file: with open('algo_config.json', 'r', encoding='utf-8') as file:
ALGO_CONFIG = json.load(file, object_hook=lambda d: structs.Algo_Config(**d)) ALGO_CONFIG = json.load(file)
ALGO_CONFIG.Max_Target_Notional = float(min([ASTER_MULT, EXTEND_MULT]) * ALGO_CONFIG.Target_Open_Cash_Position) 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: async with engine.connect() as CON:
### ASTER SETUP ### ### ASTER SETUP ###

View File

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

View File

@@ -23,3 +23,4 @@ nicegui
x10-python-trading-starknet x10-python-trading-starknet
eth-keys 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")