added extend symbol change for ws

This commit is contained in:
2026-04-30 04:32:49 +00:00
parent dc3409ac40
commit 1ac0909c21
20 changed files with 28960 additions and 1221 deletions

View File

@@ -2,7 +2,7 @@
"cells": [
{
"cell_type": "code",
"execution_count": 22,
"execution_count": 4,
"id": "d1eed397",
"metadata": {},
"outputs": [],
@@ -11,6 +11,7 @@
"import json\n",
"from dataclasses import dataclass, asdict\n",
"import valkey\n",
"import modules.utils as utils\n",
"\n",
"with open('algo_config.json', 'r', encoding='utf-8') as file:\n",
" ALGO_CONFIG = json.load(file)\n",
@@ -19,7 +20,7 @@
},
{
"cell_type": "code",
"execution_count": 23,
"execution_count": 5,
"id": "c6151613",
"metadata": {},
"outputs": [],
@@ -29,7 +30,7 @@
},
{
"cell_type": "code",
"execution_count": null,
"execution_count": 38,
"id": "d83c61e5",
"metadata": {},
"outputs": [
@@ -39,30 +40,31 @@
"1"
]
},
"execution_count": 4,
"execution_count": 38,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"config_update = {\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",
" 'Config': {\n",
" # 'Loop_Sleep_Sec': 0.00,\n",
" # 'Min_Time_To_Funding_Minutes': 60,\n",
" # 'Min_Fund_Rate_Pct_To_Trade': 0.0002,\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",
" # 'Log_Summary_Each_Loop': False,\n",
" 'Print_Summary_Each_Loop': False,\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",
" # # 'Allow_Ordering_Aster': True,\n",
" # # 'Allow_Ordering_Extend': True,\n",
" # # 'Allow_Symbol_Change': False,\n",
" # # 'Flip_Side_For_Testing': False,\n",
" # # 'Flatten_Open_Positions': False,\n",
" # },\n",
"}\n",
"VAL_KEY.publish('fr_orchestrator_input', json.dumps(config_update))"
@@ -70,29 +72,30 @@
},
{
"cell_type": "code",
"execution_count": 93,
"id": "45fae761",
"execution_count": 52,
"id": "f5260342",
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
"5.0"
"1"
]
},
"execution_count": 93,
"execution_count": 52,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"json.loads(VAL_KEY.get('fr_orchestrator_output'), object_hook=lambda d: structs.Algo_Config(**d)).Loop_Sleep_Sec"
"order = {'order_id':'test_1'}\n",
"VAL_KEY.publish('fr_engine_orders_input', json.dumps(order))"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "98c500cc",
"id": "940586bb",
"metadata": {},
"outputs": [],
"source": []
@@ -100,7 +103,15 @@
{
"cell_type": "code",
"execution_count": null,
"id": "f2cf3325",
"id": "cd600e0e",
"metadata": {},
"outputs": [],
"source": []
},
{
"cell_type": "code",
"execution_count": null,
"id": "db52edf9",
"metadata": {},
"outputs": [],
"source": []
@@ -112,92 +123,6 @@
"outputs": [],
"source": []
},
{
"cell_type": "code",
"execution_count": 31,
"id": "a0df43de",
"metadata": {},
"outputs": [],
"source": [
"pos = json.loads(VAL_KEY.get('fr_aster_user_positions'))"
]
},
{
"cell_type": "code",
"execution_count": 32,
"id": "ca526c8a",
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
"[{'timestamp_arrival': 1777303258987,\n",
" 'timestamp_msg': 1777303258979,\n",
" 'timestamp_transaction': 1777303258950,\n",
" 'event_reason_type': 'ORDER',\n",
" 'symbol': 'ETHUSDT',\n",
" 'position_amount': 0.226,\n",
" 'entry_price': 2284.28,\n",
" 'accumulated_realized_pre_fees': 8.24392002,\n",
" 'unrealized_pnl': 0.0,\n",
" 'margin_type': 'cross',\n",
" 'isolated_wallet': 0.0,\n",
" 'position_side': 'BOTH'}]"
]
},
"execution_count": 32,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"pos"
]
},
{
"cell_type": "code",
"execution_count": 33,
"id": "f788b6df",
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
"Timestamp('2026-04-27 15:20:58.987000')"
]
},
"execution_count": 33,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"import pandas as pd\n",
"pd.to_datetime(1777303258987, unit='ms')"
]
},
{
"cell_type": "code",
"execution_count": 34,
"id": "855f980b",
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
"Timestamp('2026-04-27 15:20:58.979000')"
]
},
"execution_count": 34,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"import pandas as pd\n",
"pd.to_datetime(1777303258979, unit='ms')"
]
},
{
"cell_type": "code",
"execution_count": 35,
@@ -219,6 +144,118 @@
"pd.to_datetime(1777303258950, unit='ms')"
]
},
{
"cell_type": "code",
"execution_count": 57,
"id": "5f7535df",
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
"'{\"ASTER\": {\"mult\": 150, \"lh_asset\": \"ETH\", \"rh_asset\": \"USD\", \"symbol_asset_separator\": \"\"}, \"EXTEND\": {\"mult\": 50, \"lh_asset\": \"ETH\", \"rh_asset\": \"USD\", \"symbol_asset_separator\": \"-\"}}'"
]
},
"execution_count": 57,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"VAL_KEY.get('fr_algo_working_symbol')"
]
},
{
"cell_type": "code",
"execution_count": 51,
"id": "3acaa8cd",
"metadata": {},
"outputs": [],
"source": [
"ASTER = structs.Perpetual_Exchange(\n",
" mult = 150,\n",
" lh_asset = 'ETH',\n",
" rh_asset = 'USD',\n",
" symbol_asset_separator = '',\n",
")\n",
"EXTEND = structs.Perpetual_Exchange(\n",
" mult = 50,\n",
" lh_asset = 'ETH',\n",
" rh_asset = 'USD',\n",
" symbol_asset_separator = '-',\n",
")"
]
},
{
"cell_type": "code",
"execution_count": 52,
"id": "b417adad",
"metadata": {},
"outputs": [],
"source": [
"best_symbol_by_exchange: dict = json.loads(s=VAL_KEY.get(name='fr_algo_working_symbol')) # ty:ignore[invalid-argument-type]\n",
"best_symbol_by_exchange_aster = structs.Perpetual_Exchange(**asdict(ASTER))\n",
"best_symbol_by_exchange_extend = structs.Perpetual_Exchange(**asdict(EXTEND))"
]
},
{
"cell_type": "code",
"execution_count": 53,
"id": "fa5a8e85",
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
"Perpetual_Exchange(mult=50, lh_asset='ETH', rh_asset='USD', symbol_asset_separator='-')"
]
},
"execution_count": 53,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"best_symbol_by_exchange_extend"
]
},
{
"cell_type": "code",
"execution_count": 54,
"id": "fb81441a",
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
"True"
]
},
"execution_count": 54,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"\n",
"VAL_KEY.set('fr_algo_working_symbol', json.dumps({'ASTER': asdict(best_symbol_by_exchange_aster), 'EXTEND': asdict(best_symbol_by_exchange_extend)}))"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "09571e38",
"metadata": {},
"outputs": [],
"source": []
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": []
},
{
"cell_type": "code",
"execution_count": null,

View File

@@ -1,5 +1,5 @@
{
"Updated_Timestamp": 1777478176165,
"Updated_Timestamp": 1777496051056,
"Config": {
"Loop_Sleep_Sec": 0.0,
"Max_Order_Over_Notional_Ratio": 1.05,
@@ -13,11 +13,12 @@
},
"Logging": {
"Log_Summary_Each_Loop": false,
"Print_Summary_Each_Loop": true
"Print_Summary_Each_Loop": false
},
"Overrides": {
"Allow_Ordering_Aster": true,
"Allow_Ordering_Extend": true,
"Allow_Symbol_Change": false,
"Flatten_Open_Positions": false,
"Flip_Side_For_Testing": false
}

View File

@@ -4,11 +4,10 @@ import logging
import os
import traceback
from datetime import datetime
from typing import AsyncContextManager
import valkey
from dotenv import load_dotenv
# from sqlalchemy.ext.asyncio import create_async_engine
import modules.utils as utils
'''
TO DO:
@@ -16,75 +15,62 @@ TO DO:
'''
### Database ###
CON: AsyncContextManager | None = None
VAL_KEY = None
VK_IN = 'fr_orchestrator_input'
VK_OUT = 'fr_orchestrator_output'
VK_IN: str = 'fr_orchestrator_input'
VK_OUT: str = 'fr_orchestrator_output'
# CONFIG_FILEPATH: str = '/algo_local_drive/algo_config.json'
CONFIG_FILEPATH: str = 'algo_config.json'
### Logging ###
load_dotenv()
LOG_FILEPATH: str = os.getenv("LOGS_PATH") + '/Fund_Rate_Algo_Orchestrator.log'
LOG_FILEPATH: str = f'{os.getenv("LOGS_PATH")}/Fund_Rate_Algo_Orchestrator.log'
ALGO_CONFIG: None | dict
async def orchestrator() -> None:
global ALGO_CONFIG
async def main() -> None:
VAL_KEY: valkey.Valkey = valkey.Valkey(host='localhost', port=6379, db=0, decode_responses=True)
try:
VK_PUBSUB = VAL_KEY.pubsub()
VK_PUBSUB: valkey.client.PubSub = VAL_KEY.pubsub()
VK_PUBSUB.subscribe(VK_IN)
logging.info(f"Subscribed to '{VK_IN}'. Waiting for messages...")
logging.info(msg=f"Subscribed to '{VK_IN}'. Waiting for messages...")
for message in VK_PUBSUB.listen():
if message['type'] == 'message':
timestamp = round(datetime.now().timestamp()*1000)
data = json.loads(message['data'])
# channel = message['channel']
timestamp: int = round(number=datetime.now().timestamp()*1000)
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['Updated_Timestamp'] = timestamp
for k, v in data.items():
if ALGO_CONFIG.get(k, None) is not None:
ALGO_CONFIG[k] = v
# Receive Update Msg from PubSub
data: dict = json.loads(s=message['data'])
VAL_KEY.set(VK_OUT, json.dumps(ALGO_CONFIG))
with open('/algo_local_drive/algo_config.json', 'w', encoding='utf-8') as f:
json.dump(ALGO_CONFIG, f, indent=4)
logging.info(f"Algo Config Updated @ {timestamp}; {data}")
# Load Config File
with open(file=CONFIG_FILEPATH, mode='r', encoding='utf-8') as f:
Algo_Config: dict = json.load(fp=f)
Algo_Config['Updated_Timestamp'] = timestamp
# Update Config w Update Data
Algo_Config: dict = utils.rec_set_dict(orig_dict=Algo_Config, new_dict=data)
# Set VK KV w Updated Config
VAL_KEY.set(name=VK_OUT, value=json.dumps(obj=Algo_Config))
# Save Updated Config to File
with open(file=CONFIG_FILEPATH, mode='w', encoding='utf-8') as f:
json.dump(obj=Algo_Config, fp=f, indent=4)
logging.info(msg=f"Algo Config Updated @ {timestamp}; {data}")
except valkey.exceptions.ConnectionError as e:
logging.info(f"Could not connect to Valkey. Please check the publish server is up; {e}")
logging.info(msg=f"Could not connect to Valkey. Please check the publish server is up; {e}")
except KeyboardInterrupt:
logging.info('ORCHESTRATOR SHUTTING DOWN...')
logging.info(msg='ORCHESTRATOR SHUTTING DOWN...')
except Exception as e:
logging.error(traceback.format_exc())
logging.critical(f'*** ORCHESTRATOR CRASHED: {e}')
logging.error(msg=traceback.format_exc())
logging.critical(msg=f'*** ORCHESTRATOR CRASHED: {e}')
### MAIN STARTUP ###
async def main() -> None:
global VAL_KEY
global CON
global ALGO_CONFIG
VAL_KEY = valkey.Valkey(host='localhost', port=6379, db=0, decode_responses=True)
# engine = create_async_engine('mysql+asyncmy://root:pwd@localhost/fund_rate')
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['Updated_Timestamp'] = round(datetime.now().timestamp()*1000)
# async with engine.connect() as CON:
await orchestrator()
if __name__ == '__main__':
START_TIME = round(datetime.now().timestamp()*1000)
START_TIME: int = round(number=datetime.now().timestamp()*1000)
logging.info(f'Log FilePath: {LOG_FILEPATH}')
logging.info(msg=f'Log FilePath: {LOG_FILEPATH}')
logging.basicConfig(
force=True,
@@ -93,6 +79,6 @@ if __name__ == '__main__':
format='%(asctime)s - %(levelname)s - %(message)s',
filemode='w'
)
logging.info(f"STARTED: {START_TIME}")
logging.info(msg=f"STARTED: {START_TIME}")
asyncio.run(main())

26480
aster.ipynb

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -4,53 +4,27 @@ import logging
import os
import time
import traceback
from dataclasses import dataclass, field
from dataclasses import asdict
from datetime import datetime
from typing import AsyncContextManager
import modules.structs as structs
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)
import modules.manual_leverage as leverage
### 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)
df_leverage_by_exch = pd.DataFrame(data=leverage.LEVERAGE_BY_EXCH)
### Database ###
# CON: AsyncContextManager | None = None
VAL_KEY = None
VK_OUT = 'fr_engine_best_fund_rate_output'
VAL_KEY: valkey.Valkey
VK_OUT: str = 'fr_engine_best_fund_rate_output'
### Logging ###
load_dotenv()
LOG_FILEPATH: str = os.getenv("LOGS_PATH") + '/Fund_Rate_Engine_BFR.log'
LOG_FILEPATH: str = f'{os.getenv(key="LOGS_PATH")}/Fund_Rate_Engine_BFR.log'
### CONSTANTS ###
LOOP_SLEEP_SEC: int = 5
@@ -65,9 +39,9 @@ Mkt_Volume_Last_Refresh_TS_ms: int
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)
r: dict = json.loads(s=requests.get(url='https://api.starknet.extended.exchange/api/v1/info/markets').text)
df = pd.DataFrame(r['data'])
df: pd.DataFrame = pd.DataFrame(data=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',{}))
@@ -81,9 +55,9 @@ def get_extended_markets_info() -> pd.DataFrame:
return df
def load_aster_current_fr() -> pd.DataFrame:
df = pd.DataFrame(json.loads(VAL_KEY.get('fund_rate_aster_all')))
df = pd.DataFrame(data=json.loads(s=VAL_KEY.get(name='fund_rate_aster_all'))) # ty:ignore[invalid-argument-type]
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: pd.DataFrame = 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()
@@ -91,17 +65,17 @@ def load_aster_current_fr() -> pd.DataFrame:
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 = pd.DataFrame(data=json.loads(s=VAL_KEY.get(name='fund_rate_extended_all'))) # ty:ignore[invalid-argument-type]
df = df[['symbol','funding_rate_updated_ts_ms','funding_rate']]
df: pd.DataFrame = 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: pd.DataFrame = df.merge(df_mkt_stats[['name','assetName','status', 'funding_rate_ts']].rename({'name':'symbol','funding_rate_ts':'next_funding_ts'}, axis=1), on='symbol', how='left')
df: pd.DataFrame = 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()
df['time_delta_to_next_funding'] = pd.to_datetime(arg=df['next_funding_ts'], unit='ms') - pd.Timestamp.now()
return df
@@ -124,25 +98,40 @@ async def loop() -> None:
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 = df_comb_fr.merge(right=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]}
df_best_fr_rate: pd.DataFrame = df_comb_fr[['symbol_ext','symbol_ast','max_leverage_ext','max_leverage_ast','lh_asset_ext','lh_asset_ast','rh_asset_ext','rh_asset_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)
ASTER = structs.Perpetual_Exchange(
mult = int(df_best_fr_rate['max_leverage_ast'][0]),
lh_asset = df_best_fr_rate['lh_asset_ast'][0],
rh_asset = df_best_fr_rate['rh_asset_ast'][0],
symbol_asset_separator = '',
)
EXTEND = structs.Perpetual_Exchange(
mult = int(df_best_fr_rate['max_leverage_ext'][0]),
lh_asset = df_best_fr_rate['lh_asset_ext'][0],
rh_asset = df_best_fr_rate['rh_asset_ext'][0],
symbol_asset_separator = '-',
)
best_next_funding_pair: dict[str, dict] = {'ASTER': asdict(obj=ASTER), 'EXTEND': asdict(obj=EXTEND)}
VAL_KEY.set(VK_OUT, json.dumps(best_next_funding_pair))
print(best_next_funding_pair)
VAL_KEY.set(name=VK_OUT, value=json.dumps(obj=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...')
logging.info('SHUTTING DOWN...')
except Exception as e:
logging.error(traceback.format_exc())
logging.critical(f'*** ORCHESTRATOR CRASHED: {e}')
logging.critical(f'*** CRASHED: {e}')
### STARTUP ###
@@ -156,9 +145,9 @@ async def main() -> None:
await loop()
if __name__ == '__main__':
START_TIME = round(datetime.now().timestamp()*1000)
START_TIME = round(number=datetime.now().timestamp()*1000)
logging.info(f'Log FilePath: {LOG_FILEPATH}')
logging.info(msg=f'Log FilePath: {LOG_FILEPATH}')
logging.basicConfig(
force=True,
@@ -167,6 +156,6 @@ if __name__ == '__main__':
format='%(asctime)s - %(levelname)s - %(message)s',
filemode='w'
)
logging.info(f"STARTED: {START_TIME}")
logging.info(msg=f"STARTED: {START_TIME}")
asyncio.run(main())

119
engine_orders.py Normal file
View File

@@ -0,0 +1,119 @@
import asyncio
import json
import logging
import os
import traceback
from datetime import datetime
import time
import valkey
from dotenv import load_dotenv
import modules.utils as utils
import multiprocessing as mp
import threading
from typing import Any
'''
TO DO:
- Insert config changes into database for analysis later / general tracking
'''
LOCAL_ORDERS: list = []
### Database ###
VK_POS_ASTER: str = 'fr_aster_user_positions'
VK_IN: str = 'fr_engine_orders_input'
VK_OUT: str = 'fr_engine_orders_output'
VAL_KEY: valkey.Valkey = valkey.Valkey(host='localhost', port=6379, db=0, decode_responses=True)
### Logging ###
load_dotenv()
LOG_FILEPATH: str = f'{os.getenv("LOGS_PATH")}/Fund_Rate_Engine_Orders.log'
# async def work_order(order: dict) -> dict:
# LOCAL_ORDERS.append(order)
# while LOCAL_ORDERS:
# print(f'working order...{order}')
# time.sleep(5)
# print(f'{order}...order posted/replaced/etc')
# VAL_KEY.set(name=VK_OUT, value=json.dumps(order))
# return order
def receive_orders():
global LOCAL_ORDERS
VK_PUBSUB: valkey.client.PubSub = VAL_KEY.pubsub()
VK_PUBSUB.subscribe(VK_IN)
for message in VK_PUBSUB.listen():
if message['type'] == 'message':
ts_arrival: int = round(number=datetime.now().timestamp()*1000)
# Receive Update Msg from PubSub
data: dict = json.loads(s=message['data'])
print(data)
# def receive_notional_updates():
# VK_PUBSUB: valkey.client.PubSub = VAL_KEY.pubsub()
# VK_PUBSUB.subscribe(VK_IN)
# for message in VK_PUBSUB.listen():
# if message['type'] == 'message':
# ts_arrival: int = round(number=datetime.now().timestamp()*1000)
# # Receive Update Msg from PubSub
# data: dict = json.loads(s=message['data'])
# print(data)
# LOCAL_ORDERS.append(data)
async def main() -> None:
global LOCAL_ORDERS
try:
thread = threading.Thread(target=receive_orders)
thread.daemon = True
# thread.start()
thread.join()
# while True:
# print(f"Subscribed to '{VK_IN}'. Waiting for messages...")
# print(f'LOCAL_ORDERS: {LOCAL_ORDERS}')
# aster_position_updates: Any = VAL_KEY.get(name=VK_POS_ASTER)
# aster_position_updates: list = json.loads(s=aster_position_updates) if aster_position_updates is not None else []
# print(f'Aster Pos Updates: {aster_position_updates}')
# time.sleep(5)
except valkey.exceptions.ConnectionError as e:
logging.info(msg=f"Could not connect to Valkey. Please check the publish server is up; {e}")
except KeyboardInterrupt:
logging.info(msg='ORCHESTRATOR SHUTTING DOWN...')
except Exception as e:
logging.error(msg=traceback.format_exc())
logging.critical(msg=f'*** ORCHESTRATOR CRASHED: {e}')
if __name__ == '__main__':
START_TIME: int = round(number=datetime.now().timestamp()*1000)
logging.info(msg=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(msg=f"STARTED: {START_TIME}")
asyncio.run(main())

View File

@@ -2,7 +2,7 @@
"cells": [
{
"cell_type": "code",
"execution_count": 9,
"execution_count": 2,
"id": "6c70a8c3",
"metadata": {},
"outputs": [],
@@ -27,7 +27,7 @@
},
{
"cell_type": "code",
"execution_count": 12,
"execution_count": 3,
"id": "ff971ca9",
"metadata": {},
"outputs": [],
@@ -49,19 +49,10 @@
},
{
"cell_type": "code",
"execution_count": 13,
"execution_count": 4,
"id": "fc2c6d2b",
"metadata": {},
"outputs": [
{
"name": "stderr",
"output_type": "stream",
"text": [
"Unclosed client session\n",
"client_session: <aiohttp.client.ClientSession object at 0x7b2dc26ba850>\n"
]
}
],
"outputs": [],
"source": [
"client, trading_client = await extend_auth.create_auth_account_and_trading_client()"
]
@@ -146,7 +137,7 @@
},
{
"cell_type": "code",
"execution_count": 6,
"execution_count": 5,
"id": "8dd8aa73",
"metadata": {},
"outputs": [],
@@ -156,23 +147,44 @@
},
{
"cell_type": "code",
"execution_count": 8,
"execution_count": 7,
"id": "50bb753e",
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
"x10.utils.http.WrappedApiResponse[List[PositionModel]]"
]
},
"execution_count": 7,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"type(d)"
]
},
{
"cell_type": "code",
"execution_count": 10,
"id": "ade14392",
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
"{'status': 'OK', 'data': [], 'error': None, 'pagination': None}"
"list"
]
},
"execution_count": 8,
"execution_count": 10,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"dict(d)"
"type(dict(d).get('data'))"
]
},
{

136
main.py
View File

@@ -1,3 +1,5 @@
from x10.utils.http import WrappedApiResponse
from x10.perpetual.trading_client.trading_client import PerpetualTradingClient
import asyncio
import json
import logging
@@ -8,7 +10,7 @@ import traceback
from datetime import datetime, timezone
from decimal import ROUND_DOWN, Decimal
from typing import AsyncContextManager
from dataclasses import dataclass, asdict
from typing import Any
import numpy as np
import pandas as pd
@@ -19,24 +21,24 @@ import valkey
from dotenv import load_dotenv
from sqlalchemy import text
from sqlalchemy.ext.asyncio import create_async_engine
from x10.models.order import OrderSide
from x10.models.order import OrderSide, PlacedOrderModel
import modules.utils as utils
import modules.aster_auth as aster_auth
import modules.extended_auth as extend_auth
import modules.structs as structs
### Database ###
EXTEND_CLIENT = None
CON: AsyncContextManager | None = None
VAL_KEY = None
### Clients ###
EXTEND_CLIENT: PerpetualTradingClient
CON: AsyncContextManager
VAL_KEY: valkey.Valkey
### Logging ###
load_dotenv()
LOG_FILEPATH: str = os.getenv("LOGS_PATH") + '/Fund_Rate_Algo.log'
LOG_FILEPATH: str = f'{os.getenv(key="LOGS_PATH")}/Fund_Rate_Algo.log'
### Algo Config ###
ALGO_CONFIG: structs.Algo_Config | None = None
ALGO_CONFIG: structs.Algo_Config
MIN_TIME_TO_FUNDING: int
### EXCHANGES ###
@@ -56,7 +58,6 @@ EXTEND = structs.Perpetual_Exchange(
### GLOBALS ###
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
@@ -122,7 +123,7 @@ async def get_aster_collateral():
r = await aster_auth.post_authenticated_url(fut_acct_balances)
ASTER_AVAIL_COLLATERAL = float([d for d in r if d.get('asset')==ASTER.rh_asset][0].get('availableBalance'))
async def get_aster_notional_position(resp: dict | None = None):
async def get_aster_notional_position(resp: list | None = None):
global ASTER_NOTIONAL_OBJ
global ASTER_NOTIONAL_POSITION
global ASTER_UNREALIZED_PNL
@@ -131,14 +132,14 @@ async def get_aster_notional_position(resp: dict | None = None):
previous_notional_obj = ASTER_NOTIONAL_OBJ
if not resp:
fut_acct_positionRisk = {
fut_acct_positionRisk: dict = {
"url": "/fapi/v3/positionRisk",
"method": "GET",
"params": {
'symbol': ASTER.symbol,
}
}
resp = await aster_auth.post_authenticated_url(fut_acct_positionRisk)
resp: list = await aster_auth.post_authenticated_url(req=fut_acct_positionRisk) # ty:ignore[invalid-assignment]
d = [x for x in resp if x.get('symbol', None) == ASTER.symbol][0]
d['timestamp_arrival'] = round(datetime.now().timestamp()*1000)
else:
@@ -168,8 +169,8 @@ async def get_aster_notional_position(resp: dict | None = None):
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)}')
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)}')
async def get_extend_collateral():
global EXTEND_AVAIL_COLLATERAL
@@ -177,7 +178,7 @@ async def get_extend_collateral():
get_bals = dict(dict(await EXTEND_CLIENT.account.get_balance()).get('data', {}))
EXTEND_AVAIL_COLLATERAL = get_bals.get('available_for_trade', 0) if get_bals.get('collateral_name', None)==EXTEND.rh_asset else 0
async def get_extend_notional(resp: dict | None = None):
async def get_extend_notional(resp: list | None = None):
global EXTEND_NOTIONAL_OBJ
global EXTEND_NOTIONAL_POSITION
global EXTEND_UNREALIZED_PNL
@@ -238,15 +239,15 @@ async def get_extend_notional(resp: dict | None = None):
async def get_aster_exch_info():
global ASTER_MIN_ORDER_QTY
fut_acct_exchangeInfo = {
fut_acct_exchangeInfo: dict = {
"url": "/fapi/v3/exchangeInfo",
"method": "GET",
"params": {}
}
r = await aster_auth.post_authenticated_url(fut_acct_exchangeInfo)
s = r['symbols']
d = [d for d in s if d.get('symbol', None) == 'ETHUSDT'][0]
f = [f for f in d['filters'] if f.get('filterType', None) == 'LOT_SIZE'][0]
r: dict = await aster_auth.post_authenticated_url(fut_acct_exchangeInfo) # ty:ignore[invalid-assignment]
s: list = r['symbols']
d: dict = [d for d in s if d.get('symbol', None) == 'ETHUSDT'][0]
f: dict = [f for f in d['filters'] if f.get('filterType', None) == 'LOT_SIZE'][0]
ASTER_MIN_ORDER_QTY = float(f['minQty'])
async def get_extend_exch_info():
@@ -290,7 +291,7 @@ async def run_algo():
global EXTEND_OPEN_ORDERS
global Last_Aster_Fill_Time_Ts
global Just_Rejected_Or_Expired
global Best_Symbol_by_Exchange
# global Best_Symbol_by_Exchange
try:
while True:
@@ -298,20 +299,22 @@ async def run_algo():
# print('__________Start___________')
### ALGO CONIFG ###
ALGO_CONFIG = json.loads(VAL_KEY.get('fr_orchestrator_output'))
ALGO_CONFIG = json.loads(VAL_KEY.get('fr_orchestrator_output')) # ty:ignore[invalid-argument-type]
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 ###
Best_Symbol_by_Exchange = json.loads(VAL_KEY.get('fr_engine_best_fund_rate_output'))
best_symbol_by_exchange: dict = json.loads(s=VAL_KEY.get(name='fr_engine_best_fund_rate_output')) # ty:ignore[invalid-argument-type]
best_symbol_by_exchange_aster = structs.Perpetual_Exchange(**best_symbol_by_exchange['ASTER'])
best_symbol_by_exchange_extend = structs.Perpetual_Exchange(**best_symbol_by_exchange['EXTEND'])
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_DICT = json.loads(VAL_KEY.get('fund_rate_aster')) # ty:ignore[invalid-argument-type]
EXTENDED_FUND_RATE_DICT = json.loads(VAL_KEY.get('fund_rate_extended')) # ty:ignore[invalid-argument-type]
ASTER_FUND_RATE = float(ASTER_FUND_RATE_DICT.get('funding_rate', 0))
EXTEND_FUND_RATE = float(EXTENDED_FUND_RATE_DICT.get('funding_rate', 0))
ASTER_FUND_RATE: float = float(ASTER_FUND_RATE_DICT.get('funding_rate', 0))
EXTEND_FUND_RATE: float = float(EXTENDED_FUND_RATE_DICT.get('funding_rate', 0))
if ALGO_CONFIG.Overrides.Flip_Side_For_Testing:
ASTER_FUND_RATE = ASTER_FUND_RATE * -1
@@ -321,26 +324,26 @@ async def run_algo():
EXTEND_FUND_RATE_TIME = float(EXTENDED_FUND_RATE_DICT.get('next_funding_time_ts_ms', 0))
EXTEND_FUND_RATE_TIME = max([EXTEND_FUND_RATE_TIME, 0])
ASTER_TICKER_DICT = json.loads(VAL_KEY.get('fut_ticker_aster'))
EXTENDED_TICKER_DICT = json.loads(VAL_KEY.get('fut_ticker_extended'))
ASTER_TICKER_DICT = json.loads(VAL_KEY.get('fut_ticker_aster')) # ty:ignore[invalid-argument-type]
EXTENDED_TICKER_DICT = json.loads(VAL_KEY.get('fut_ticker_extended')) # ty:ignore[invalid-argument-type]
### Manage Local Collateral Using Updates from WS ###
ASTER_WS_COLLATERAL_UPDATES = VAL_KEY.get('fr_aster_user_positions')
ASTER_WS_COLLATERAL_UPDATES = json.loads(ASTER_WS_COLLATERAL_UPDATES) if ASTER_WS_COLLATERAL_UPDATES is not None else []
ASTER_WS_COLLATERAL_UPDATES = json.loads(ASTER_WS_COLLATERAL_UPDATES) if ASTER_WS_COLLATERAL_UPDATES is not None else [] # ty:ignore[invalid-argument-type]
EXTEND_WS_COLLATERAL_UPDATES = VAL_KEY.get('fr_extended_user_positions')
EXTEND_WS_COLLATERAL_UPDATES = json.loads(EXTEND_WS_COLLATERAL_UPDATES) if EXTEND_WS_COLLATERAL_UPDATES is not None else []
EXTEND_WS_COLLATERAL_UPDATES = json.loads(EXTEND_WS_COLLATERAL_UPDATES) if EXTEND_WS_COLLATERAL_UPDATES is not None else [] # ty:ignore[invalid-argument-type]
### Manage Local Notionals Using Updates from WS ###
ASTER_WS_POS_UPDATES = VAL_KEY.get('fr_aster_user_positions')
ASTER_WS_POS_UPDATES = json.loads(ASTER_WS_POS_UPDATES) if ASTER_WS_POS_UPDATES is not None else []
EXTEND_WS_POS_UPDATES = VAL_KEY.get('fr_extended_user_positions')
EXTEND_WS_POS_UPDATES = json.loads(EXTEND_WS_POS_UPDATES) if EXTEND_WS_POS_UPDATES is not None else []
ASTER_WS_POS_UPDATES: Any = VAL_KEY.get(name='fr_aster_user_positions')
ASTER_WS_POS_UPDATES: list = json.loads(s=ASTER_WS_POS_UPDATES) if ASTER_WS_POS_UPDATES is not None else []
EXTEND_WS_POS_UPDATES: Any = VAL_KEY.get('fr_extended_user_positions')
EXTEND_WS_POS_UPDATES: list = json.loads(EXTEND_WS_POS_UPDATES) if EXTEND_WS_POS_UPDATES is not None else []
### Manage Local Orders Using Updates from WS ###
ASTER_WS_ORDER_UPDATES = VAL_KEY.get('fr_aster_user_orders')
ASTER_WS_ORDER_UPDATES = json.loads(ASTER_WS_ORDER_UPDATES) if ASTER_WS_ORDER_UPDATES is not None else []
EXTEND_WS_ORDER_UPDATES = VAL_KEY.get('fr_extended_user_orders')
EXTEND_WS_ORDER_UPDATES = json.loads(EXTEND_WS_ORDER_UPDATES) if EXTEND_WS_ORDER_UPDATES is not None else []
ASTER_WS_ORDER_UPDATES: Any = VAL_KEY.get('fr_aster_user_orders')
ASTER_WS_ORDER_UPDATES: list = json.loads(ASTER_WS_ORDER_UPDATES) if ASTER_WS_ORDER_UPDATES is not None else []
EXTEND_WS_ORDER_UPDATES: Any = VAL_KEY.get('fr_extended_user_orders')
EXTEND_WS_ORDER_UPDATES: list = json.loads(EXTEND_WS_ORDER_UPDATES) if EXTEND_WS_ORDER_UPDATES is not None else []
# CHECK NO MORE THAN 1 OPEN ORDER ON EITHER EXCHANGE #
if len(ASTER_OPEN_ORDERS) > 1 or len(EXTEND_OPEN_ORDERS) > 1:
@@ -409,9 +412,9 @@ async def run_algo():
order_update = [dict(ou) for ou in EXTEND_WS_ORDER_UPDATES if dict(ou).get('order_id', None) == order_id]
if len(order_update) > 0:
order_update = order_update[0]
order_update_status = order_update.get('status')
order_status_changed = order_orig_status.upper() != order_update_status.upper()
order_update: dict = order_update[0]
order_update_status: str = order_update['status']
order_status_changed: bool = order_orig_status.upper() != order_update_status.upper()
if order_status_changed:
logging.info(f'EXTEND ORDER ({order_id}): {order_orig_status} -> {order_update_status}')
@@ -436,14 +439,18 @@ 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:
# if (best_symbol_by_exchange_aster.symbol != ASTER.symbol) or (best_symbol_by_exchange_extend.symbol != EXTEND.symbol):
# if abs( ASTER_NOTIONAL_POSITION ) > 0.00 or abs( EXTEND_NOTIONAL_POSITION ) > 0.00:
# 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']
# print('Balances Flattened - Updating to Trade New Symbols:')
# print(f' ASTER.symbol -> {best_symbol_by_exchange_aster.symbol}')
# print(f' EXTEND.symbol -> {best_symbol_by_exchange_extend.symbol}')
# ALGO_CONFIG.Overrides.Flatten_Open_Positions = False
# ASTER = best_symbol_by_exchange_aster
# EXTEND = best_symbol_by_exchange_extend
# VAL_KEY.set(name='fr_algo_working_symbol', value=json.dumps(obj={'ASTER': asdict(obj=ASTER), 'EXTEND': asdict(obj=EXTEND)}))
@@ -542,7 +549,7 @@ async def run_algo():
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: Any = logging.info if use_logging else print
OUT(f'''
LOOP SLEEP (SEC): {ALGO_CONFIG.Config.Loop_Sleep_Sec}
@@ -566,6 +573,7 @@ async def run_algo():
FEES : TAKER: {0.0002:.2%}; Expected Alpha w Taker = {Expected_Alpha_Net_FR-0.0002:.6f} [w/o FR: {Expected_Alpha_w_Taker:.6f}]
HEDGE: {Hedge_Ratio:.2f}% <= {1:.2f}%: {Currently_Hedged} [{EXTEND_NOTIONAL_POSITION:.2f} / {ASTER_NOTIONAL_POSITION:.2f}]
MKT : Aster: {ASTER.symbol} (best: {best_symbol_by_exchange_aster.symbol}) | Extend: {ASTER.symbol} (best: {best_symbol_by_exchange_extend.symbol})
--- ASTER OPEN ORDERS ---
{ASTER_OPEN_ORDERS}
@@ -583,8 +591,8 @@ async def run_algo():
### ROUTES ###
# MIN_EXPECTED_ALPHA_TO_TRADE = 0.0001
MIN_EXPECTED_ALPHA_TO_TRADE = -0.000001
MIN_EXPECTED_ALPHA_TO_TRADE = 0.0001
# MIN_EXPECTED_ALPHA_TO_TRADE = -0.000001
# ALPHA RATIO CHECK
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:
@@ -614,7 +622,7 @@ async def run_algo():
logging.info('ASTER OPEN ORDER NO PX CHG; SKIPPING')
place_order = False
else:
cancel_order = {
cancel_order: dict = {
"url": "/fapi/v3/order",
"method": "DELETE",
"params": {
@@ -622,7 +630,7 @@ async def run_algo():
'orderId': open_order_id,
}
}
cr = await aster_auth.post_authenticated_url(cancel_order)
cr: dict = await aster_auth.post_authenticated_url(cancel_order) # ty:ignore[invalid-assignment]
if cr.get('status', None) == 'CANCELED':
ASTER_OPEN_ORDERS.pop(0)
place_order = True
@@ -637,7 +645,7 @@ async def run_algo():
logging.info('ASTER TRYNG TO ORDER 0.00 BASE QTY, SKIPPING')
if place_order:
price = Decimal(str(price)).quantize(Decimal(str(0.01)), rounding=ROUND_DOWN)
price: Decimal = Decimal(str(price)).quantize(Decimal(str(0.01)), rounding=ROUND_DOWN)
post_order = {
"url": "/fapi/v3/order",
"method": "POST",
@@ -650,7 +658,7 @@ async def run_algo():
'price': price,
}
}
order_resp = await aster_auth.post_authenticated_url(post_order)
order_resp: dict = await aster_auth.post_authenticated_url(post_order) # ty:ignore[invalid-assignment]
if order_resp.get('orderId', None) is not None:
order_resp['original_price'] = price
order_resp['order_status'] = order_resp['status']
@@ -672,14 +680,14 @@ async def run_algo():
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
price: Decimal = Decimal(value=str(EXTEND_TOB_PX - ALGO_CONFIG.Config.Price_Worsener_Extend if side == 'BUY' else EXTEND_TOB_PX + ALGO_CONFIG.Config.Price_Worsener_Extend)).quantize(Decimal(str(0.1)), rounding=ROUND_DOWN)
else:
post_only = True
price = EXTEND_TOB_PX
price: Decimal = Decimal(value=str(EXTEND_TOB_PX)).quantize(Decimal(str(0.1)), rounding=ROUND_DOWN)
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)))
qty = Decimal(value=str(abs(EXTEND_TGT_TAIL_BASE_QTY)))
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})')
@@ -703,13 +711,13 @@ async def run_algo():
open_order_px = 0
place_order = True
if place_order:
price = Decimal(str(price)).quantize(Decimal(str(0.1)), rounding=ROUND_DOWN)
price: Decimal = Decimal(str(price)).quantize(Decimal(str(0.1)), rounding=ROUND_DOWN)
if round(open_order_px - float(price), 2) == 0.00:
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(
order_resp: WrappedApiResponse[PlacedOrderModel] = await EXTEND_CLIENT.place_order(
market_name=symbol,
amount_of_synthetic=qty,
price=price,
@@ -780,16 +788,14 @@ async def main():
VAL_KEY = valkey.Valkey(host='localhost', port=6379, db=0, decode_responses=True)
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', mode='r', encoding='utf-8') as file:
ALGO_CONFIG = json.load(file)
ALGO_CONFIG = structs.Algo_Config(**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()))
VAL_KEY.set(name='fr_orchestrator_output', value=json.dumps(obj=ALGO_CONFIG.model_dump()))
VAL_KEY.set(name='fr_algo_working_symbol', value=json.dumps(obj={'ASTER': asdict(obj=ASTER), 'EXTEND': asdict(obj=EXTEND)}))
async with engine.connect() as CON:
### ASTER SETUP ###

View File

@@ -3,50 +3,51 @@ from dotenv import load_dotenv
import os
import time
import threading
import urllib
from urllib import parse
from eth_account.messages import encode_typed_data
from eth_account import Account
from eth_account.datastructures import SignedMessage
load_dotenv()
user = os.getenv("RABBY_WALLET")
signer = os.getenv("ASTER_API_WALLET_ADDRESS")
private_key = os.getenv("ASTER_API_PRIVATE_KEY")
USER: str = os.getenv(key="RABBY_WALLET") # ty:ignore[invalid-assignment]
SIGNER: str = os.getenv(key="ASTER_API_WALLET_ADDRESS") # ty:ignore[invalid-assignment]
PRIVATE_KEY: str = os.getenv(key="ASTER_API_PRIVATE_KEY") # ty:ignore[invalid-assignment]
_last_ms = 0
_i = 0
async def post_authenticated_url(req: dict) -> dict:
typed_data = {
"types": {
"EIP712Domain": [
{"name": "name", "type": "string"},
{"name": "version", "type": "string"},
{"name": "chainId", "type": "uint256"},
{"name": "verifyingContract", "type": "address"}
],
"Message": [
{ "name": "msg", "type": "string" }
]
},
"primaryType": "Message",
"domain": {
"name": "AsterSignTransaction",
"version": "1",
"chainId": 1666,
"verifyingContract": "0x0000000000000000000000000000000000000000"
},
"message": {
"msg": "$msg"
async def post_authenticated_url(req: dict) -> list | dict:
typed_data: dict = {
"types": {
"EIP712Domain": [
{"name": "name", "type": "string"},
{"name": "version", "type": "string"},
{"name": "chainId", "type": "uint256"},
{"name": "verifyingContract", "type": "address"}
],
"Message": [
{ "name": "msg", "type": "string" },
]
},
"primaryType": "Message",
"domain": {
"name": "AsterSignTransaction",
"version": "1",
"chainId": 1666,
"verifyingContract": "0x0000000000000000000000000000000000000000"
},
"message": {
"msg": "$msg"
}
}
}
headers = {
headers: dict[str, str] = {
'Content-Type': 'application/x-www-form-urlencoded',
'User-Agent': 'PythonApp/1.0'
}
host = 'https://fapi.asterdex.com'
host: str = 'https://fapi.asterdex.com'
def get_nonce():
_nonce_lock = threading.Lock()
@@ -71,38 +72,34 @@ async def post_authenticated_url(req: dict) -> dict:
)
return Account.sign_message(message, private_key=private_key)
async def send_by_url(req):
async def send_by_url(req) -> list | dict: # ty:ignore[invalid-return-type]
my_dict = req['params'].copy()
url = host + req['url']
method = req['method']
my_dict['nonce'] = str(get_nonce())
my_dict['user'] = user
my_dict['signer'] = signer
my_dict['nonce'] = str(object=get_nonce())
my_dict['user'] = USER
my_dict['signer'] = SIGNER
param = urllib.parse.urlencode(my_dict)
param: str = parse.urlencode(query=my_dict)
typed_data['message']['msg'] = param
signed = sign_typed_data(typed_data, private_key)
signed: SignedMessage = sign_typed_data(data=typed_data, private_key=PRIVATE_KEY)
full_url = url + '?' + param + '&signature=' + signed.signature.hex()
# print(full_url)
full_url: str = url + '?' + param + '&signature=' + signed.signature.hex()
if method == 'GET':
res = requests.get(full_url, headers=headers)
res: requests.Response = requests.get(url=full_url, headers=headers)
# print(res.status_code, res.text)
return res.json()
elif method == 'POST':
res = requests.post(full_url, headers=headers)
# print(res.status_code, res.text)
res: requests.Response = requests.post(url=full_url, headers=headers)
return res.json()
elif method == 'PUT':
res = requests.put(full_url, headers=headers)
# print(res.status_code, res.text)
res: requests.Response = requests.put(url=full_url, headers=headers)
return res.json()
elif method == 'DELETE':
res = requests.delete(full_url, headers=headers)
# print(res.status_code, res.text)
res: requests.Response = requests.delete(url=full_url, headers=headers)
return res.json()
return await send_by_url(req=req)

View File

@@ -0,0 +1,38 @@
from dataclasses import dataclass
@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', 'ASTER', 'USDT', 75 , 20_000 ), Asset_Leverage('EXTEND', 'ASTER', 'USD', 25, 400_000 ),
Asset_Leverage('ASTER', 'AAVE' , 'USDT', 10 , 115_290), Asset_Leverage('EXTEND', 'AAVE' , 'USD', 50, 500_000 ),
Asset_Leverage('ASTER', '4' , 'USDT', 50 , 5_000 ), Asset_Leverage('EXTEND', '4' , 'USD', 5 , 100_000 ),
Asset_Leverage('ASTER', 'BNB' , 'USDT', 100, 10_000 ), Asset_Leverage('EXTEND', 'BNB' , 'USD', 50, 500_000 ),
Asset_Leverage('ASTER', 'BTC' , 'USDT', 150, 300_000), Asset_Leverage('EXTEND', 'BTC' , 'USD', 50, 4_000_000),
Asset_Leverage('ASTER', 'CHIP' , 'USDT', 50 , 5_000 ), Asset_Leverage('EXTEND', 'CHIP' , 'USD', 5 , 100_000 ),
Asset_Leverage('ASTER', 'CLU' , 'USDT', 50 , 10_000 ), Asset_Leverage('EXTEND', 'WTI' , 'USD', 5 , 1_000_000),
Asset_Leverage('ASTER', 'DOGE' , 'USDT', 75 , 80_000 ), Asset_Leverage('EXTEND', 'DOGE' , 'USD', 50, 500_000 ),
Asset_Leverage('ASTER', 'ENA' , 'USDT', 25 , 30_473 ), Asset_Leverage('EXTEND', 'ENA' , 'USD', 50, 500_000 ),
Asset_Leverage('ASTER', 'ETH' , 'USDT', 150, 300_000), Asset_Leverage('EXTEND', 'ETH' , 'USD', 50, 4_000_000),
Asset_Leverage('ASTER', 'HYPE' , 'USDT', 300, 1_000 ), Asset_Leverage('EXTEND', 'HYPE' , 'USD', 50, 1_000_000),
Asset_Leverage('ASTER', 'INIT' , 'USDT', 50 , 5_000 ), Asset_Leverage('EXTEND', 'INIT' , 'USD', 5 , 100_000 ),
Asset_Leverage('ASTER', 'LIT' , 'USDT', 50 , 2_500 ), Asset_Leverage('EXTEND', 'LIT' , 'USD', 25, 400_000 ),
Asset_Leverage('ASTER', 'SOL' , 'USDT', 100, 50_000 ), Asset_Leverage('EXTEND', 'SOL' , 'USD', 50, 1_000_000),
Asset_Leverage('ASTER', 'SUI' , 'USDT', 75 , 5_416 ), Asset_Leverage('EXTEND', 'SUI' , 'USD', 50, 500_000 ),
Asset_Leverage('ASTER', 'TRUMP', 'USDT', 50 , 5_567 ), Asset_Leverage('EXTEND', 'TRUMP', 'USD', 25, 400_000 ),
Asset_Leverage('ASTER', 'WLFI' , 'USDT', 25 , 104_869), Asset_Leverage('EXTEND', 'WLFI' , 'USD', 10, 250_000 ),
Asset_Leverage('ASTER', 'XAG' , 'USDT', 100, 50_000 ), Asset_Leverage('EXTEND', 'XAG' , 'USD', 10, 1_000_000),
Asset_Leverage('ASTER', 'XAU' , 'USDT', 75 , 2_500 ), Asset_Leverage('EXTEND', 'XAU' , 'USD', 25, 2_000_000),
Asset_Leverage('ASTER', 'XMR' , 'USDT', 50 , 10_000 ), Asset_Leverage('EXTEND', 'XMR' , 'USD', 25, 400_000 ),
Asset_Leverage('ASTER', 'XPT' , 'USDT', 3 , 30_000 ), Asset_Leverage('EXTEND', 'XPT' , 'USD', 5 , 1_000_000),
Asset_Leverage('ASTER', 'XRP' , 'USDT', 100, 40_000 ), Asset_Leverage('EXTEND', 'XRP' , 'USD', 50, 500_000 ),
Asset_Leverage('ASTER', 'ZEC' , 'USDT', 75 , 6_250 ), Asset_Leverage('EXTEND', 'ZEC' , 'USD', 10, 250_000 ),
Asset_Leverage('ASTER', 'ZORA' , 'USDT', 5 , 100_000), Asset_Leverage('EXTEND', 'ZORA' , 'USD', 5 , 100_000 ),
]

View File

@@ -9,9 +9,11 @@ from pydantic import BaseModel
class Algo_Config_Overrides(BaseModel):
Allow_Ordering_Aster: bool
Allow_Ordering_Extend: bool
Allow_Symbol_Change: bool
Flatten_Open_Positions: bool
Flip_Side_For_Testing: bool
# @dataclass(kw_only=True)
class Algo_Config_Config(BaseModel):
Loop_Sleep_Sec: int
@@ -51,8 +53,8 @@ class Valkey_Stream:
none_fill: Any = None
async def update(self):
r = self.client.get(self.channel)
self.data = json.loads(r) if r is not None else self.none_fill
r: str = self.client.get(name=self.channel) # ty:ignore[invalid-assignment]
self.data = json.loads(s=r) if r is not None else self.none_fill
@dataclass(kw_only=True)
@@ -162,40 +164,40 @@ class Perpetual_Exchange:
rh_asset: str
symbol_asset_separator: str = ''
async def update(self):
await self.Collateral_Updates.update()
await self.Order_Updates.update()
await self.Position_Updates.update()
await self.Funding_Rate.update()
# async def update(self):
# await self.Collateral_Updates.update()
# await self.Order_Updates.update()
# await self.Position_Updates.update()
# await self.Funding_Rate.update()
def __post_init__(self) -> None:
self.symbol = f'{self.lh_asset.upper()}{self.symbol_asset_separator}{self.rh_asset.upper()}'
@dataclass(kw_only=True)
class Aster(Perpetual_Exchange):
name: str = 'Aster'
lh_asset: str = 'ETH'
rh_asset: str = 'USDT'
# @dataclass(kw_only=True)
# class Aster(Perpetual_Exchange):
# name: str = 'Aster'
# lh_asset: str = 'ETH'
# rh_asset: str = 'USDT'
def __post_init__(self):
super().__post_init__()
self.Order_Updates = Order_Updates(Valkey=Valkey_Stream(channel = 'fr_aster_user_balances', none_fills = []))
self.Collateral_Updates = Collateral(Valkey=Valkey_Stream(channel = 'fr_aster_user_orders', none_fills = []))
self.Position_Updates = Open_Positions(Valkey=Valkey_Stream(channel = 'fr_aster_user_positions', none_fills = []))
self.Funding_Rate - Funding_Rate(Valkey=Valkey_Stream(channel = 'fund_rate_aster', none_fills = None))
# def __post_init__(self):
# super().__post_init__()
# self.Order_Updates = Order_Updates(Valkey=Valkey_Stream(channel = 'fr_aster_user_balances', none_fills = []))
# self.Collateral_Updates = Collateral(Valkey=Valkey_Stream(channel = 'fr_aster_user_orders', none_fills = []))
# self.Position_Updates = Open_Positions(Valkey=Valkey_Stream(channel = 'fr_aster_user_positions', none_fills = []))
# self.Funding_Rate - Funding_Rate(Valkey=Valkey_Stream(channel = 'fund_rate_aster', none_fills = None))
@dataclass(kw_only=True)
class Extend(Perpetual_Exchange):
name: str = 'Extended'
lh_asset: str = 'ETH'
rh_asset: str = 'USD'
symbol_asset_separator: str = '-'
# @dataclass(kw_only=True)
# class Extend(Perpetual_Exchange):
# name: str = 'Extended'
# lh_asset: str = 'ETH'
# rh_asset: str = 'USD'
# symbol_asset_separator: str = '-'
def __post_init__(self):
super().__post_init__()
self.Order_Updates = Order_Updates(Valkey=Valkey_Stream(channel = 'fr_aster_user_balances', none_fills = []))
self.Collateral_Updates = Collateral(Valkey=Valkey_Stream(channel = 'fr_aster_user_orders', none_fills = []))
self.Position_Updates = Open_Positions(Valkey=Valkey_Stream(channel = 'fr_aster_user_positions', none_fills = []))
self.Funding_Rate - Funding_Rate(Valkey=Valkey_Stream(channel = 'fund_rate_aster', none_fills = None))
# def __post_init__(self):
# super().__post_init__()
# self.Order_Updates = Order_Updates(Valkey=Valkey_Stream(channel = 'fr_aster_user_balances', none_fills = []))
# self.Collateral_Updates = Collateral(Valkey=Valkey_Stream(channel = 'fr_aster_user_orders', none_fills = []))
# self.Position_Updates = Open_Positions(Valkey=Valkey_Stream(channel = 'fr_aster_user_positions', none_fills = []))
# self.Funding_Rate - Funding_Rate(Valkey=Valkey_Stream(channel = 'fund_rate_aster', none_fills = None))

View File

@@ -5,7 +5,7 @@ import os
load_dotenv()
def upsert_list_of_dicts_by_id(list_of_dicts, new_dict, id='id', seq_check_field: str | None = None):
def upsert_list_of_dicts_by_id(list_of_dicts, new_dict, id='id', seq_check_field: str | None = None) -> list[dict]:
for index, item in enumerate(list_of_dicts):
if item.get(id) == new_dict.get(id):
if seq_check_field is not None:
@@ -25,4 +25,19 @@ def send_tg_alert(msg: str):
url = f'https://api.telegram.org/bot{token}/sendMessage?chat_id={chat_id}'
response = requests.post(url, json={'text': str(str(msg)[:250])}, timeout=10)
return response.json()
return response.json()
def rec_set_dict(orig_dict, new_dict, allow_new_fields: bool = False) -> dict:
for k, v in new_dict.items():
if isinstance(v, dict):
rec_set_dict(orig_dict=orig_dict[k], new_dict=v)
else:
if allow_new_fields:
orig_dict[k] = v
else:
if orig_dict.get(k, None) is not None:
orig_dict[k] = v
else:
logging.warning(msg=f'rec_set_dict: encountered nonexistent key: "{k}"; skipping')
return orig_dict

View File

@@ -5,7 +5,7 @@ import socket
import traceback
from datetime import datetime
from typing import AsyncContextManager
import time
import numpy as np
import pandas as pd
import requests.packages.urllib3.util.connection as urllib3_cn # type: ignore
@@ -26,35 +26,79 @@ urllib3_cn.allowed_gai_family = allowed_gai_family
### Database ###
USE_DB: bool = True
USE_VK: bool = True
CON: AsyncContextManager
VAL_KEY: valkey.Valkey
VK_FUND_RATE = 'fund_rate_aster'
VK_TICKER = 'fut_ticker_aster'
VK_LAST_TRADE = 'fut_last_trade_aster'
CON: AsyncContextManager | None = None
VAL_KEY = None
### Logging ###
load_dotenv()
LOG_FILEPATH: str = os.getenv("LOGS_PATH") + '/Fund_Rate_Aster.log'
LOG_FILEPATH: str = f'{os.getenv(key="LOGS_PATH")}/Fund_Rate_Aster.log'
### CONSTANTS ###
SYMBOL: str = 'ETHUSDT'
STREAM_MARKPRICE: str = f'{SYMBOL.lower()}@markPrice@1s'
STREAM_BOOKTICKER: str = f'{SYMBOL.lower()}@bookTicker'
STREAM_TRADES: str = f'{SYMBOL.lower()}@aggTrade'
### Globals ###
WSS_URL = f"wss://fstream.asterdex.com/stream?streams={STREAM_MARKPRICE}/{STREAM_BOOKTICKER}/{STREAM_TRADES}"
WSS_URL: str = f"wss://fstream.asterdex.com/stream?streams={STREAM_MARKPRICE}/{STREAM_BOOKTICKER}/{STREAM_TRADES}"
ALLOW_SYMBOL_CHG: bool = False
### Funcs ###
async def subscribe_streams(websocket, streams: list[str]) -> None:
logging.info(f'Trying to sub: {streams}')
msg = {
"method": "SUBSCRIBE",
"params": streams,
"id": int(round(number=datetime.now().timestamp()*1000))
}
await websocket.send(json.dumps(obj=msg))
logging.info(f'Success sub: {streams}')
async def unsubscribe_streams(websocket, streams: list[str]) -> None:
logging.info(f'Trying to unsub: {streams}')
msg = {
"method": "UNSUBSCRIBE",
"params": streams,
"id": int(round(number=datetime.now().timestamp()*1000))
}
await websocket.send(json.dumps(obj=msg))
logging.info(f'Success unsub: {streams}')
### Websocket ###
async def ws_stream():
async for websocket in websockets.connect(WSS_URL):
logging.info(f"Connected to {WSS_URL}")
global SYMBOL
global STREAM_MARKPRICE
global STREAM_BOOKTICKER
global STREAM_TRADES
async for websocket in websockets.connect(WSS_URL, ping_interval=5):
logging.info(msg=f"Connected to {WSS_URL}")
try:
async for message in websocket:
### Update Symbol if Algo Outputs Change ###
if ALLOW_SYMBOL_CHG:
best_symbol_by_exchange: dict = json.loads(s=VAL_KEY.get(name='fr_algo_working_symbol')) # ty:ignore[invalid-argument-type]
best_symbol: str = f'{best_symbol_by_exchange['ASTER']['lh_asset']}{best_symbol_by_exchange['ASTER']['rh_asset']}'
if best_symbol != SYMBOL:
logging.info(f'Symbol Change: {SYMBOL} -> {best_symbol}')
SYMBOL = best_symbol
await unsubscribe_streams(websocket = websocket, streams=[STREAM_MARKPRICE,STREAM_BOOKTICKER,STREAM_TRADES])
STREAM_MARKPRICE = f'{SYMBOL.lower()}@markPrice@1s'
STREAM_BOOKTICKER = f'{SYMBOL.lower()}@bookTicker'
STREAM_TRADES = f'{SYMBOL.lower()}@aggTrade'
await subscribe_streams(websocket = websocket, streams=[STREAM_MARKPRICE,STREAM_BOOKTICKER,STREAM_TRADES])
continue
ts_arrival = round(datetime.now().timestamp()*1000)
if isinstance(message, str):
try:
try:
data = json.loads(message)
channel = data.get('stream', None)
if channel is not None:
@@ -132,8 +176,8 @@ async def main():
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")
raise NotImplementedError('Cannot run without VK')
if USE_DB:
engine = create_async_engine('mysql+asyncmy://root:pwd@localhost/fund_rate')
@@ -141,9 +185,8 @@ async def main():
await aster_db.create_fr_aster_mkt_trades(CON=CON)
await ws_stream()
else:
CON = None
logging.warning("DATABASE NOT BEING USED, NO DATA WILL BE RECORDED")
await ws_stream()
raise NotImplementedError('Cannot run without DB')
if __name__ == '__main__':

View File

@@ -3,16 +3,15 @@ import json
import logging
import socket
import traceback
from datetime import datetime, timezone
from datetime import datetime
from typing import AsyncContextManager
import math
import numpy as np
import pandas as pd
# import numpy as np
# import pandas as pd
import requests.packages.urllib3.util.connection as urllib3_cn # type: ignore
from sqlalchemy import text
# from sqlalchemy import text
import websockets
from sqlalchemy.ext.asyncio import create_async_engine
# from sqlalchemy.ext.asyncio import create_async_engine
import valkey
import os
from dotenv import load_dotenv
@@ -25,31 +24,30 @@ 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'
VK_CHANNEL = 'fund_rate_aster_all'
CON: AsyncContextManager | None = None
VAL_KEY = None
CON: AsyncContextManager
VAL_KEY: valkey.Valkey
### Logging ###
load_dotenv()
LOG_FILEPATH: str = os.getenv("LOGS_PATH") + '/Fund_Rate_Aster_FR_ALL.log'
LOG_FILEPATH: str = f'{os.getenv(key="LOGS_PATH")}/Fund_Rate_Aster_FR_ALL.log'
### Globals ###
WSS_URL = "wss://fstream.asterdex.com/ws/!markPrice@arr"
WSS_URL: str = "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}")
logging.info(msg=f"Connected to {WSS_URL}")
try:
async for message in websocket:
if isinstance(message, str):
try:
data = json.loads(message)
data: dict = json.loads(message)
if data:
VAL_KEY.set(VK_FUND_RATE_ALL, json.dumps(data))
VAL_KEY.set(name=VK_CHANNEL, value=json.dumps(obj=data))
# print(f'VK_SAVED: {len(data)}')
continue
else:
@@ -61,39 +59,35 @@ async def ws_stream():
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
logging.error(msg=f'Connection closed: {e}')
logging.error(msg=traceback.format_exc())
utils.send_tg_alert(msg=f'WS: {VK_CHANNEL} - Failure: {e}')
except Exception as e:
logging.error(f'Connection closed: {e}')
logging.error(traceback.format_exc())
logging.error(msg=f'Connection closed: {e}')
logging.error(msg=traceback.format_exc())
utils.send_tg_alert(msg=f'WS: {VK_CHANNEL} - Failure: {e}')
### 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")
VAL_KEY = valkey.Valkey(host='localhost', port=6379, db=0)
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()
raise NotImplementedError('DB not implemented for this ws.')
# engine = create_async_engine('mysql+asyncmy://root:pwd@localhost/fund_rate')
# async with engine.connect() as 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)
START_TIME: int = round(number=datetime.now().timestamp()*1000)
logging.info(f'Log FilePath: {LOG_FILEPATH}')
logging.info(msg=f'Log FilePath: {LOG_FILEPATH}')
logging.basicConfig(
force=True,
@@ -102,9 +96,9 @@ if __name__ == '__main__':
format='%(asctime)s - %(levelname)s - %(message)s',
filemode='w'
)
logging.info(f"STARTED: {START_TIME}")
logging.info(msg=f"STARTED: {START_TIME}")
try:
asyncio.run(main())
except KeyboardInterrupt:
logging.info("Stream stopped")
logging.info(msg="Stream stopped")

View File

@@ -1,20 +1,19 @@
import asyncio
import json
import logging
import os
import socket
import traceback
from datetime import datetime
from typing import AsyncContextManager
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
import websockets
from dotenv import load_dotenv
from sqlalchemy.ext.asyncio import create_async_engine
import modules.aster_auth as aster_auth
import modules.aster_db as aster_db
import modules.db as db
@@ -28,84 +27,84 @@ urllib3_cn.allowed_gai_family = allowed_gai_family
### Database ###
USE_DB: bool = True
USE_VK: bool = True
VK_ORDERS_TRADES = 'fr_aster_user_orders'
VK_MARGIN_CALLS = 'fr_aster_user_margin_calls'
VK_BALANCES = 'fr_aster_user_balances'
VK_POSITIONS = 'fr_aster_user_positions'
CON: AsyncContextManager | None = None
VAL_KEY = None
CON: AsyncContextManager
VAL_KEY: valkey.Valkey
VK_ORDERS_TRADES: str = 'fr_aster_user_orders'
VK_MARGIN_CALLS: str = 'fr_aster_user_margin_calls'
VK_BALANCES: str = 'fr_aster_user_balances'
VK_POSITIONS: str = 'fr_aster_user_positions'
### Logging ###
load_dotenv()
LOG_FILEPATH: str = os.getenv("LOGS_PATH") + '/Fund_Rate_Aster_User.log'
LOG_FILEPATH: str = f'{os.getenv(key="LOGS_PATH")}/Fund_Rate_Aster_User.log'
### CONSTANTS ###
WSS_URL = "wss://fstream.asterdex.com/ws/"
LOCAL_RECENT_UPDATES_LOOKBACK_SEC = 30
WSS_URL: str = "wss://fstream.asterdex.com/ws/"
LOCAL_RECENT_UPDATES_LOOKBACK_SEC: int = 30
### Globals ###
LISTEN_KEY: str | None = None
LISTEN_KEY_LAST_UPDATE_TS_S: int = 0
LISTEN_KEY_PUT_INTERVAL_SEC = 1800
Listen_Key: str
Listen_Key_Last_Update_TS_S: int = 0
Listen_Key_Put_Interval_Sec: int = 1800
LOCAL_RECENT_ORDERS: list = []
LOCAL_RECENT_MARGIN_CALLS: list = []
LOCAL_RECENT_BALANCES: list = []
LOCAL_RECENT_POSITIONS: list = []
Local_Recent_Orders: list[dict] = []
Local_Recent_Margin_Calls: list[dict] = []
Local_Recent_Balances: list[dict] = []
Local_Recent_Positions: list[dict] = []
async def get_new_listen_key() -> str:
global LISTEN_KEY_LAST_UPDATE_TS_S
global Listen_Key_Last_Update_TS_S
listen_key_request = {
listen_key_request: dict = {
"url": "/fapi/v3/listenKey",
"method": "POST",
"params": {}
}
r = await aster_auth.post_authenticated_url(listen_key_request)
listen_key = r.get('listenKey', None)
r: dict = await aster_auth.post_authenticated_url(listen_key_request) # ty:ignore[invalid-assignment]
listen_key: str = r.get('listenKey', '')
print(f'LISTEN KEY: {listen_key}')
if listen_key is not None:
LISTEN_KEY_LAST_UPDATE_TS_S = round(datetime.now().timestamp())
if listen_key:
Listen_Key_Last_Update_TS_S = round(number=datetime.now().timestamp())
return listen_key
else:
raise ValueError(f'Listen Key is None; Failed to Update. response: {r}')
raise ValueError(f'Listen Key is empty; Failed to Update. response: {r}')
async def listen_key_interval():
global LISTEN_KEY
global Listen_Key
while True:
await asyncio.sleep(LISTEN_KEY_PUT_INTERVAL_SEC)
LISTEN_KEY = await get_new_listen_key()
await asyncio.sleep(delay=Listen_Key_Put_Interval_Sec)
Listen_Key = await get_new_listen_key()
### Websocket ###
async def ws_stream():
global LISTEN_KEY
global LOCAL_RECENT_ORDERS
global LOCAL_RECENT_MARGIN_CALLS
global LOCAL_RECENT_BALANCES
global LOCAL_RECENT_POSITIONS
global Listen_Key
global Local_Recent_Orders
global Local_Recent_Margin_Calls
global Local_Recent_Balances
global Local_Recent_Positions
LISTEN_KEY = await get_new_listen_key()
Listen_Key = await get_new_listen_key()
async for websocket in websockets.connect(WSS_URL+LISTEN_KEY):
logging.info(f"Connected to {WSS_URL}")
asyncio.create_task(listen_key_interval())
async for websocket in websockets.connect(uri=WSS_URL+Listen_Key, ping_interval=5):
logging.info(msg=f"Connected to {WSS_URL}")
asyncio.create_task(coro=listen_key_interval())
try:
async for message in websocket:
ts_arrival = round(datetime.now().timestamp()*1000)
ts_arrival: int = round(number=datetime.now().timestamp()*1000)
if isinstance(message, str):
try:
data = json.loads(message)
channel = data.get('e', None)
if channel is not None:
LOOKBACK_MIN_TS_MS = ts_arrival - (LOCAL_RECENT_UPDATES_LOOKBACK_SEC*1000)
data: dict = json.loads(s=message)
channel: str = data.get('e', '')
if channel:
lookback_min_ts_ms: int = ts_arrival - (LOCAL_RECENT_UPDATES_LOOKBACK_SEC*1000)
match channel:
case 'ORDER_TRADE_UPDATE':
# logging.info(f'ORDER_TRADE_UPDATE: {data}')
new_order_update = {
new_order_update: dict = {
'timestamp_arrival': ts_arrival,
'timestamp_msg': data['E'],
'timestamp_transaction': data['T'],
@@ -141,11 +140,11 @@ async def ws_stream():
'callback_rate': float(data['o'].get("cr", 0)), # :"5.0", // Callback Rate, only puhed with TRAILING_STOP_MARKET order
'realized_profit': float(data['o']["rp"]), # :"0" // Realized Profit of the trade
}
LOCAL_RECENT_ORDERS = utils.upsert_list_of_dicts_by_id(LOCAL_RECENT_ORDERS, new_order_update, id='order_id', seq_check_field='timestamp_msg')
LOCAL_RECENT_ORDERS = [t for t in LOCAL_RECENT_ORDERS if t.get('timestamp_arrival', 0) >= LOOKBACK_MIN_TS_MS]
Local_Recent_Orders = utils.upsert_list_of_dicts_by_id(Local_Recent_Orders, new_order_update, id='order_id', seq_check_field='timestamp_msg')
Local_Recent_Orders = [t for t in Local_Recent_Orders if t.get('timestamp_arrival', 0) >= lookback_min_ts_ms]
VAL_KEY_OBJ = json.dumps(LOCAL_RECENT_ORDERS)
VAL_KEY.set(VK_ORDERS_TRADES, VAL_KEY_OBJ)
VAL_KEY_OBJ: str = json.dumps(obj=Local_Recent_Orders)
VAL_KEY.set(name=VK_ORDERS_TRADES, value=VAL_KEY_OBJ)
await db.insert_df_to_mysql(table_name='fr_aster_user_order_trade', params=new_order_update, CON=CON)
continue
@@ -153,7 +152,7 @@ async def ws_stream():
# logging.info(f'MARGIN_CALL: {data}')
list_for_df = []
for p in list(data['p']):
margin_call_update = {
margin_call_update: dict = {
'timestamp_arrival': ts_arrival,
'timestamp_msg': data['E'],
'cross_wallet_balance': float(data.get('cw', 0)),
@@ -168,11 +167,11 @@ async def ws_stream():
'maint_margin_required': float(p["mm"]), # :"1.614445" // Maintenance Margin Required
}
list_for_df.append(margin_call_update)
LOCAL_RECENT_MARGIN_CALLS = utils.upsert_list_of_dicts_by_id(LOCAL_RECENT_MARGIN_CALLS, margin_call_update, id='symbol', seq_check_field='timestamp_msg')
LOCAL_RECENT_MARGIN_CALLS = [t for t in LOCAL_RECENT_MARGIN_CALLS if t.get('timestamp_arrival', 0) >= LOOKBACK_MIN_TS_MS]
Local_Recent_Margin_Calls = utils.upsert_list_of_dicts_by_id(Local_Recent_Margin_Calls, margin_call_update, id='symbol', seq_check_field='timestamp_msg')
Local_Recent_Margin_Calls = [t for t in Local_Recent_Margin_Calls if t.get('timestamp_arrival', 0) >= lookback_min_ts_ms]
VAL_KEY_OBJ = json.dumps(LOCAL_RECENT_MARGIN_CALLS)
VAL_KEY.set(VK_MARGIN_CALLS, VAL_KEY_OBJ)
VAL_KEY_OBJ: str = json.dumps(obj=Local_Recent_Margin_Calls)
VAL_KEY.set(name=VK_MARGIN_CALLS, value=VAL_KEY_OBJ)
await db.insert_df_to_mysql(table_name='fr_aster_user_margin', params=list_for_df, CON=CON)
continue
@@ -183,7 +182,7 @@ async def ws_stream():
### Balance Updates ###
if len(list(data['a']['B'])) > 0:
for b in list(data['a']['B']):
balance_update = {
balance_update: dict = {
'timestamp_arrival': ts_arrival,
'timestamp_msg': data['E'],
'timestamp_transaction': data['T'],
@@ -196,13 +195,13 @@ async def ws_stream():
'balance_change_excl_pnl_comms': float(b['bc']),
}
list_for_df_bal.append(balance_update)
LOCAL_RECENT_BALANCES = utils.upsert_list_of_dicts_by_id(LOCAL_RECENT_BALANCES, balance_update, id='asset', seq_check_field='timestamp_msg')
LOCAL_RECENT_BALANCES = [t for t in LOCAL_RECENT_BALANCES if t.get('timestamp_arrival', 0) >= LOOKBACK_MIN_TS_MS]
VAL_KEY.set(VK_BALANCES, json.dumps(LOCAL_RECENT_BALANCES))
Local_Recent_Balances = utils.upsert_list_of_dicts_by_id(Local_Recent_Balances, balance_update, id='asset', seq_check_field='timestamp_msg')
Local_Recent_Balances = [t for t in Local_Recent_Balances if t.get('timestamp_arrival', 0) >= lookback_min_ts_ms]
VAL_KEY.set(name=VK_BALANCES, value=json.dumps(obj=Local_Recent_Balances))
### Position Updates ###
if len(list(data['a']['P'])) > 0:
for p in list(data['a']['P']):
position_update = {
position_update: dict = {
'timestamp_arrival': ts_arrival,
'timestamp_msg': data['E'],
'timestamp_transaction': data['T'],
@@ -219,33 +218,34 @@ async def ws_stream():
'position_side': p['ps'],
}
list_for_df_pos.append(position_update)
LOCAL_RECENT_POSITIONS = utils.upsert_list_of_dicts_by_id(LOCAL_RECENT_POSITIONS, position_update, id='symbol', seq_check_field='timestamp_msg')
LOCAL_RECENT_POSITIONS = [t for t in LOCAL_RECENT_POSITIONS if t.get('timestamp_arrival', 0) >= LOOKBACK_MIN_TS_MS]
VAL_KEY.set(VK_POSITIONS, json.dumps(LOCAL_RECENT_POSITIONS))
Local_Recent_Positions = utils.upsert_list_of_dicts_by_id(Local_Recent_Positions, position_update, id='symbol', seq_check_field='timestamp_msg')
Local_Recent_Positions = [t for t in Local_Recent_Positions if t.get('timestamp_arrival', 0) >= lookback_min_ts_ms]
VAL_KEY.set(name=VK_POSITIONS, value=json.dumps(obj=Local_Recent_Positions))
if list_for_df_bal:
await db.insert_df_to_mysql(table_name='fr_aster_user_account_bal', params=list_for_df_bal, CON=CON)
if list_for_df_pos:
await db.insert_df_to_mysql(table_name='fr_aster_user_account_pos', params=list_for_df_pos, CON=CON)
continue
case 'listenKeyExpired':
raise('Listen Key Has Expired; Failed to Update Properly. Restarting.')
raise ValueError('Listen Key Has Expired; Failed to Update Properly. Restarting.')
case _:
logging.warning(f'UNMATCHED OTHER MSG: {data}')
logging.warning(msg=f'UNMATCHED OTHER MSG: {data}')
else:
logging.info(f'Initial or unexpected data struct, skipping: {data}')
logging.info(msg=f'Initial or unexpected data struct, skipping: {data}')
continue
except (json.JSONDecodeError, ValueError):
logging.warning(f'Message not in JSON format, skipping: {message}')
logging.warning(msg=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
logging.error(msg=f'Connection closed: {e}')
logging.error(msg=traceback.format_exc())
utils.send_tg_alert(msg=f'WS_Aster_User - Failure: {e}')
except Exception as e:
logging.error(f'Connection closed: {e}')
logging.error(traceback.format_exc())
logging.error(msg=f'Connection closed: {e}')
logging.error(msg=traceback.format_exc())
utils.send_tg_alert(msg=f'WS_Aster_User - Failure: {e}')
async def main():
@@ -255,8 +255,8 @@ async def main():
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")
raise NotImplementedError('Cannot run without Valkey')
if USE_DB:
engine = create_async_engine('mysql+asyncmy://root:pwd@localhost/fund_rate')
@@ -267,15 +267,14 @@ async def main():
await aster_db.create_fr_aster_user_account_pos(CON=CON)
await ws_stream()
else:
CON = None
logging.warning("DATABASE NOT BEING USED, NO DATA WILL BE RECORDED")
await ws_stream()
raise NotImplementedError('Cannot run without DB')
if __name__ == '__main__':
START_TIME = round(datetime.now().timestamp()*1000)
START_TIME: int = round(number=datetime.now().timestamp()*1000)
logging.info(f'Log FilePath: {LOG_FILEPATH}')
logging.info(msg=f'Log FilePath: {LOG_FILEPATH}')
logging.basicConfig(
force=True,
@@ -284,9 +283,9 @@ if __name__ == '__main__':
format='%(asctime)s - %(levelname)s - %(message)s',
filemode='w'
)
logging.info(f"STARTED: {START_TIME}")
logging.info(msg=f"STARTED: {START_TIME}")
try:
asyncio.run(main())
except KeyboardInterrupt:
logging.info("Stream stopped")
logging.info(msg="Stream stopped")

View File

@@ -28,23 +28,18 @@ USE_DB: bool = False
USE_VK: bool = True
VK_FUND_RATE = 'fund_rate_extended'
CON: AsyncContextManager | None = None
VAL_KEY = None
CON: AsyncContextManager
VAL_KEY: valkey.Valkey
### Logging ###
load_dotenv()
LOG_FILEPATH: str = os.getenv("LOGS_PATH") + '/Fund_Rate_Extended_FR.log'
LOG_FILEPATH: str = f'{os.getenv("LOGS_PATH")}/Fund_Rate_Extended_FR.log'
### CONSTANTS ###
WS_SYMBOL: str = 'ETH-USD'
FUNDING_RATE_INTERVAL_MIN = 60
SYMBOL: str = 'ETH-USD'
### Globals ###
WSS_URL = f"wss://api.starknet.extended.exchange/stream.extended.exchange/v1/funding/{WS_SYMBOL}"
# HIST_TRADES = np.empty((0, 3))
# HIST_TRADES_LOOKBACK_SEC = 6
ALLOW_SYMBOL_CHG: bool = False
def time_round_down(dt, interval_mins=5) -> int: # returns timestamp in seconds
interval_secs = interval_mins * 60
@@ -53,113 +48,63 @@ def time_round_down(dt, interval_mins=5) -> int: # returns timestamp in seconds
return rounded_seconds
# ### Database Funcs ###
# async def create_rtds_btcusd_table(
# CON: AsyncContextManager,
# engine: str = 'mysql', # mysql | duckdb
# ) -> None:
# if CON is None:
# logging.info("NO DB CONNECTION, SKIPPING Create Statements")
# else:
# if engine == 'mysql':
# logging.info('Creating Table if Does Not Exist: binance_btcusd_trades')
# await CON.execute(text("""
# CREATE TABLE IF NOT EXISTS binance_btcusd_trades (
# timestamp_arrival BIGINT,
# timestamp_msg BIGINT,
# timestamp_value BIGINT,
# value DOUBLE,
# qty DOUBLE
# );
# """))
# await CON.commit()
# else:
# raise ValueError('Only MySQL engine is implemented')
# async def insert_rtds_btcusd_table(
# timestamp_arrival: int,
# timestamp_msg: int,
# timestamp_value: int,
# value: float,
# qty: float,
# CON: AsyncContextManager,
# engine: str = 'mysql', # mysql | duckdb
# ) -> None:
# params={
# 'timestamp_arrival': timestamp_arrival,
# 'timestamp_msg': timestamp_msg,
# 'timestamp_value': timestamp_value,
# 'value': value,
# 'qty': qty,
# }
# if CON is None:
# logging.info("NO DB CONNECTION, SKIPPING Insert Statements")
# else:
# if engine == 'mysql':
# await CON.execute(text("""
# INSERT INTO binance_btcusd_trades
# (
# timestamp_arrival,
# timestamp_msg,
# timestamp_value,
# value,
# qty
# )
# VALUES
# (
# :timestamp_arrival,
# :timestamp_msg,
# :timestamp_value,
# :value,
# :qty
# )
# """),
# parameters=params
# )
# await CON.commit()
# else:
# raise ValueError('Only MySQL engine is implemented')
### 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:
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
VAL_KEY_OBJ = json.dumps({
'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,
})
VAL_KEY.set(VK_FUND_RATE, VAL_KEY_OBJ)
global SYMBOL
while True:
CHANGE_SYMBOL = False
WSS_URL = f"wss://api.starknet.extended.exchange/stream.extended.exchange/v1/funding/{SYMBOL}"
async for websocket in websockets.connect(WSS_URL):
if CHANGE_SYMBOL:
break
logging.info(f"Connected to {WSS_URL}")
try:
async for message in websocket:
### Update Symbol if Algo Outputs Change ###
if ALLOW_SYMBOL_CHG:
best_symbol_by_exchange: dict = json.loads(s=VAL_KEY.get(name='fr_algo_working_symbol')) # ty:ignore[invalid-argument-type]
best_symbol: str = f'{best_symbol_by_exchange['EXTEND']['lh_asset']}-{best_symbol_by_exchange['EXTEND']['rh_asset']}'
if best_symbol != SYMBOL:
logging.info(f'Symbol Change: {SYMBOL} -> {best_symbol}')
SYMBOL = best_symbol
CHANGE_SYMBOL = True
await websocket.close()
break
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
VAL_KEY_OBJ = json.dumps({
'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,
})
VAL_KEY.set(VK_FUND_RATE, VAL_KEY_OBJ)
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:
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())
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())
async def main():
@@ -169,16 +114,16 @@ async def main():
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")
raise NotImplementedError('Cannot run without VK')
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()
raise NotImplementedError('DB not implemented')
# 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()

View File

@@ -27,58 +27,76 @@ USE_DB: bool = False
USE_VK: bool = True
VK_TICKER = 'fut_ticker_extended'
CON: AsyncContextManager | None = None
VAL_KEY = None
CON: AsyncContextManager
VAL_KEY: valkey.Valkey
### Logging ###
load_dotenv()
LOG_FILEPATH: str = os.getenv("LOGS_PATH") + '/Fund_Rate_Extended_OB.log'
LOG_FILEPATH: str = f'{os.getenv("LOGS_PATH")}/Fund_Rate_Extended_OB.log'
### CONSTANTS ###
WS_SYMBOL: str = 'ETH-USD'
SYMBOL: str = 'ETH-USD'
### Globals ###
WSS_URL = f"wss://api.starknet.extended.exchange/stream.extended.exchange/v1/orderbooks/{WS_SYMBOL}?depth=1"
ALLOW_SYMBOL_CHG: bool = False
### 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:
ts_arrival = round(datetime.now().timestamp()*1000)
if isinstance(message, str):
try:
data = json.loads(message)
if data.get('type', None) is not None:
# print(f'OB: {data}')
VAL_KEY_OBJ = json.dumps({
'sequence_id': data['seq'],
'timestamp_arrival': ts_arrival,
'timestamp_msg': data['ts'],
'symbol': data['data']['m'],
'best_bid_px': float(data['data']['b'][0]['p']),
'best_bid_qty': float(data['data']['b'][0]['q']),
'best_ask_px': float(data['data']['a'][0]['p']),
'best_ask_qty': float(data['data']['a'][0]['q']),
})
VAL_KEY.set(VK_TICKER, VAL_KEY_OBJ)
global SYMBOL
while True:
CHANGE_SYMBOL = False
WSS_URL = f"wss://api.starknet.extended.exchange/stream.extended.exchange/v1/orderbooks/{SYMBOL}?depth=1"
async for websocket in websockets.connect(WSS_URL):
if CHANGE_SYMBOL:
break
logging.info(f"Connected to {WSS_URL}")
try:
async for message in websocket:
### Update Symbol if Algo Outputs Change ###
if ALLOW_SYMBOL_CHG:
best_symbol_by_exchange: dict = json.loads(s=VAL_KEY.get(name='fr_algo_working_symbol')) # ty:ignore[invalid-argument-type]
best_symbol: str = f'{best_symbol_by_exchange['EXTEND']['lh_asset']}-{best_symbol_by_exchange['EXTEND']['rh_asset']}'
if best_symbol != SYMBOL:
logging.info(f'Symbol Change: {SYMBOL} -> {best_symbol}')
SYMBOL = best_symbol
CHANGE_SYMBOL = True
await websocket.close()
break
ts_arrival = round(datetime.now().timestamp()*1000)
if isinstance(message, str):
try:
data = json.loads(message)
if data.get('type', None) is not None:
# print(f'OB: {data}')
VAL_KEY_OBJ = json.dumps({
'sequence_id': data['seq'],
'timestamp_arrival': ts_arrival,
'timestamp_msg': data['ts'],
'symbol': data['data']['m'],
'best_bid_px': float(data['data']['b'][0]['p']),
'best_bid_qty': float(data['data']['b'][0]['q']),
'best_ask_px': float(data['data']['a'][0]['p']),
'best_ask_qty': float(data['data']['a'][0]['q']),
})
VAL_KEY.set(VK_TICKER, VAL_KEY_OBJ)
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:
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())
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())
async def main():
@@ -88,16 +106,16 @@ async def main():
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")
raise NotImplementedError('Cannot run without VK')
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()
raise NotImplementedError('DB not implemented')
# 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()

View File

@@ -29,68 +29,88 @@ USE_DB: bool = True
USE_VK: bool = True
VK_LAST_TRADE = 'fut_last_trade_extended'
CON: AsyncContextManager | None = None
VAL_KEY = None
CON: AsyncContextManager
VAL_KEY: valkey.Valkey
### Logging ###
load_dotenv()
LOG_FILEPATH: str = os.getenv("LOGS_PATH") + '/Fund_Rate_Extended_Trades.log'
LOG_FILEPATH: str = f'{os.getenv("LOGS_PATH")}/Fund_Rate_Extended_Trades.log'
### CONSTANTS ###
WS_SYMBOL: str = 'ETH-USD'
SYMBOL: str = 'ETH-USD'
### Globals ###
WSS_URL = f"wss://api.starknet.extended.exchange/stream.extended.exchange/v1/publicTrades/{WS_SYMBOL}"
ALLOW_SYMBOL_CHG: bool = False
### 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:
ts_arrival = round(datetime.now().timestamp()*1000)
if isinstance(message, str):
try:
data = json.loads(message)
if data.get('data', None) is not None:
if data['seq'] == 1: # Skip first msg that has historical trades
global SYMBOL
while True:
CHANGE_SYMBOL: bool = False
WSS_URL = f"wss://api.starknet.extended.exchange/stream.extended.exchange/v1/publicTrades/{SYMBOL}"
async for websocket in websockets.connect(WSS_URL):
if CHANGE_SYMBOL:
break
logging.info(f"Connected to {WSS_URL}")
try:
async for message in websocket:
### Update Symbol if Algo Outputs Change ###
if ALLOW_SYMBOL_CHG:
best_symbol_by_exchange: dict = json.loads(s=VAL_KEY.get(name='fr_algo_working_symbol')) # ty:ignore[invalid-argument-type]
best_symbol: str = f'{best_symbol_by_exchange['EXTEND']['lh_asset']}-{best_symbol_by_exchange['EXTEND']['rh_asset']}'
if best_symbol != SYMBOL:
logging.info(f'Symbol Change: {SYMBOL} -> {best_symbol}')
SYMBOL = best_symbol
CHANGE_SYMBOL = True
await websocket.close()
break
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(data)
if data['seq'] == 1: # Skip first msg that has historical trades
continue
list_for_df = []
for t in data['data']:
trade_obj = {
'sequence_id': data['seq'],
'timestamp_arrival': ts_arrival,
'timestamp_msg': data['ts'],
'timestamp_trade': t['T'],
'symbol': t['m'],
'side_taker': t['S'],
'trade_type': t['tT'],
'price': float(t['p']),
'qty': float(t['q']),
'trade_id': t['i'],
'is_buyer_mkt_maker': True if t['S']=='SELL' else False,
}
list_for_df.append(trade_obj)
# VAL_KEY.set(VK_LAST_TRADE, json.dumps(trade_obj))
if USE_DB:
await db.insert_df_to_mysql(table_name='fr_extended_mkt_trades', params=list_for_df, CON=CON)
pass
continue
list_for_df = []
for t in data['data']:
trade_obj = {
'sequence_id': data['seq'],
'timestamp_arrival': ts_arrival,
'timestamp_msg': data['ts'],
'timestamp_trade': t['T'],
'symbol': t['m'],
'side_taker': t['S'],
'trade_type': t['tT'],
'price': float(t['p']),
'qty': float(t['q']),
'trade_id': t['i'],
'is_buyer_mkt_maker': True if t['S']=='SELL' else False,
}
list_for_df.append(trade_obj)
# VAL_KEY.set(VK_LAST_TRADE, json.dumps(trade_obj))
if USE_DB:
await db.insert_df_to_mysql(table_name='fr_extended_mkt_trades', params=list_for_df, CON=CON)
pass
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:
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())
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())
async def main():
@@ -100,8 +120,8 @@ async def main():
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")
raise NotImplementedError('Cannot run without VK')
if USE_DB:
engine = create_async_engine('mysql+asyncmy://root:pwd@localhost/fund_rate')
@@ -109,9 +129,9 @@ async def main():
await extended_db.create_fr_extended_mkt_trades(CON=CON)
await ws_stream()
else:
CON = None
logging.warning("DATABASE NOT BEING USED, NO DATA WILL BE RECORDED")
await ws_stream()
raise NotImplementedError('Cannot run without DB')
if __name__ == '__main__':

View File

@@ -32,16 +32,16 @@ VK_ORDERS = 'fr_extended_user_orders'
VK_TRADES = 'fr_extended_user_trades'
VK_BALANCES = 'fr_extended_user_balances'
VK_POSITIONS = 'fr_extended_user_positions'
CON: AsyncContextManager | None = None
VAL_KEY = None
CON: AsyncContextManager
VAL_KEY: valkey.Valkey
### Logging ###
load_dotenv()
LOG_FILEPATH: str = os.getenv("LOGS_PATH") + '/Fund_Rate_Extended_User.log'
LOG_FILEPATH: str = f'{os.getenv("LOGS_PATH")}/Fund_Rate_Extended_User.log'
### CONSTANTS ###
WSS_URL = "wss://api.starknet.extended.exchange/stream.extended.exchange/v1/account"
API_KEY = os.getenv('EXTENDED_API_KEY')
API_KEY: str = os.getenv('EXTENDED_API_KEY') # ty:ignore[invalid-assignment]
LOCAL_RECENT_UPDATES_LOOKBACK_SEC = 30
### Globals ###
@@ -226,8 +226,8 @@ async def main():
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")
raise NotImplementedError('Cannot run without VK')
if USE_DB:
engine = create_async_engine('mysql+asyncmy://root:pwd@localhost/fund_rate')
@@ -238,9 +238,9 @@ async def main():
await extended_db.create_fr_extended_user_trade(CON=CON)
await ws_stream()
else:
CON = None
logging.warning("DATABASE NOT BEING USED, NO DATA WILL BE RECORDED")
await ws_stream()
raise NotImplementedError('Cannot run without DB')
# await ws_stream()
if __name__ == '__main__':