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": [ "cells": [
{ {
"cell_type": "code", "cell_type": "code",
"execution_count": 22, "execution_count": 4,
"id": "d1eed397", "id": "d1eed397",
"metadata": {}, "metadata": {},
"outputs": [], "outputs": [],
@@ -11,6 +11,7 @@
"import json\n", "import json\n",
"from dataclasses import dataclass, asdict\n", "from dataclasses import dataclass, asdict\n",
"import valkey\n", "import valkey\n",
"import modules.utils as utils\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)\n", " ALGO_CONFIG = json.load(file)\n",
@@ -19,7 +20,7 @@
}, },
{ {
"cell_type": "code", "cell_type": "code",
"execution_count": 23, "execution_count": 5,
"id": "c6151613", "id": "c6151613",
"metadata": {}, "metadata": {},
"outputs": [], "outputs": [],
@@ -29,7 +30,7 @@
}, },
{ {
"cell_type": "code", "cell_type": "code",
"execution_count": null, "execution_count": 38,
"id": "d83c61e5", "id": "d83c61e5",
"metadata": {}, "metadata": {},
"outputs": [ "outputs": [
@@ -39,30 +40,31 @@
"1" "1"
] ]
}, },
"execution_count": 4, "execution_count": 38,
"metadata": {}, "metadata": {},
"output_type": "execute_result" "output_type": "execute_result"
} }
], ],
"source": [ "source": [
"config_update = {\n", "config_update = {\n",
" # 'Config': {\n", " 'Config': {\n",
" # 'Loop_Sleep_Sec': 0.00,\n", " # 'Loop_Sleep_Sec': 0.00,\n",
" # 'Min_Time_To_Funding_Minutes': 60,\n", " # 'Min_Time_To_Funding_Minutes': 60,\n",
" # 'Min_Fund_Rate_Pct_To_Trade': 0.001,\n", " # 'Min_Fund_Rate_Pct_To_Trade': 0.0002,\n",
" # 'Price_Worsener_Extend': 0.0,\n", " # 'Price_Worsener_Extend': 0.0,\n",
" # 'Price_Worsener_Aster': 0.0,\n", " # 'Price_Worsener_Aster': 0.0,\n",
" # 'Switch_To_Taker_Seconds': 1,\n", " # 'Switch_To_Taker_Seconds': 1,\n",
" # },\n", " },\n",
" 'Logging': {\n", " 'Logging': {\n",
" 'Log_Summary_Each_Loop': False,\n", " # 'Log_Summary_Each_Loop': False,\n",
" 'Print_Summary_Each_Loop': True,\n", " 'Print_Summary_Each_Loop': False,\n",
" },\n", " },\n",
" # 'Overrides': {\n", " # 'Overrides': {\n",
" # 'Allow_Ordering_Aster': True,\n", " # # 'Allow_Ordering_Aster': True,\n",
" # 'Allow_Ordering_Extend': True,\n", " # # 'Allow_Ordering_Extend': True,\n",
" # 'Flip_Side_For_Testing': False,\n", " # # 'Allow_Symbol_Change': False,\n",
" # 'Flatten_Open_Positions': False,\n", " # # 'Flip_Side_For_Testing': False,\n",
" # # 'Flatten_Open_Positions': False,\n",
" # },\n", " # },\n",
"}\n", "}\n",
"VAL_KEY.publish('fr_orchestrator_input', json.dumps(config_update))" "VAL_KEY.publish('fr_orchestrator_input', json.dumps(config_update))"
@@ -70,29 +72,30 @@
}, },
{ {
"cell_type": "code", "cell_type": "code",
"execution_count": 93, "execution_count": 52,
"id": "45fae761", "id": "f5260342",
"metadata": {}, "metadata": {},
"outputs": [ "outputs": [
{ {
"data": { "data": {
"text/plain": [ "text/plain": [
"5.0" "1"
] ]
}, },
"execution_count": 93, "execution_count": 52,
"metadata": {}, "metadata": {},
"output_type": "execute_result" "output_type": "execute_result"
} }
], ],
"source": [ "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", "cell_type": "code",
"execution_count": null, "execution_count": null,
"id": "98c500cc", "id": "940586bb",
"metadata": {}, "metadata": {},
"outputs": [], "outputs": [],
"source": [] "source": []
@@ -100,7 +103,15 @@
{ {
"cell_type": "code", "cell_type": "code",
"execution_count": null, "execution_count": null,
"id": "f2cf3325", "id": "cd600e0e",
"metadata": {},
"outputs": [],
"source": []
},
{
"cell_type": "code",
"execution_count": null,
"id": "db52edf9",
"metadata": {}, "metadata": {},
"outputs": [], "outputs": [],
"source": [] "source": []
@@ -112,92 +123,6 @@
"outputs": [], "outputs": [],
"source": [] "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", "cell_type": "code",
"execution_count": 35, "execution_count": 35,
@@ -219,6 +144,118 @@
"pd.to_datetime(1777303258950, unit='ms')" "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", "cell_type": "code",
"execution_count": null, "execution_count": null,

View File

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

View File

@@ -4,11 +4,10 @@ import logging
import os import os
import traceback import traceback
from datetime import datetime from datetime import datetime
from typing import AsyncContextManager
import valkey import valkey
from dotenv import load_dotenv from dotenv import load_dotenv
# from sqlalchemy.ext.asyncio import create_async_engine import modules.utils as utils
''' '''
TO DO: TO DO:
@@ -16,75 +15,62 @@ TO DO:
''' '''
### Database ### ### Database ###
CON: AsyncContextManager | None = None VK_IN: str = 'fr_orchestrator_input'
VAL_KEY = None VK_OUT: str = 'fr_orchestrator_output'
VK_IN = 'fr_orchestrator_input'
VK_OUT = 'fr_orchestrator_output' # CONFIG_FILEPATH: str = '/algo_local_drive/algo_config.json'
CONFIG_FILEPATH: str = 'algo_config.json'
### Logging ### ### Logging ###
load_dotenv() 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 main() -> None:
VAL_KEY: valkey.Valkey = valkey.Valkey(host='localhost', port=6379, db=0, decode_responses=True)
async def orchestrator() -> None:
global ALGO_CONFIG
try: try:
VK_PUBSUB = VAL_KEY.pubsub() VK_PUBSUB: valkey.client.PubSub = VAL_KEY.pubsub()
VK_PUBSUB.subscribe(VK_IN) 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(): for message in VK_PUBSUB.listen():
if message['type'] == 'message': if message['type'] == 'message':
timestamp = round(datetime.now().timestamp()*1000) timestamp: int = round(number=datetime.now().timestamp()*1000)
data = json.loads(message['data'])
# channel = message['channel']
with open('/algo_local_drive/algo_config.json', 'r', encoding='utf-8') as f: # Receive Update Msg from PubSub
# ALGO_CONFIG = json.load(f, object_hook=lambda d: Algo_Config(**d)) data: dict = json.loads(s=message['data'])
ALGO_CONFIG = json.load(f)
ALGO_CONFIG['Updated_Timestamp'] = timestamp
for k, v in data.items(): # Load Config File
if ALGO_CONFIG.get(k, None) is not None: with open(file=CONFIG_FILEPATH, mode='r', encoding='utf-8') as f:
ALGO_CONFIG[k] = v Algo_Config: dict = json.load(fp=f)
Algo_Config['Updated_Timestamp'] = timestamp
VAL_KEY.set(VK_OUT, json.dumps(ALGO_CONFIG))
with open('/algo_local_drive/algo_config.json', 'w', encoding='utf-8') as f: # Update Config w Update Data
json.dump(ALGO_CONFIG, f, indent=4) Algo_Config: dict = utils.rec_set_dict(orig_dict=Algo_Config, new_dict=data)
logging.info(f"Algo Config Updated @ {timestamp}; {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: 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: except KeyboardInterrupt:
logging.info('ORCHESTRATOR SHUTTING DOWN...') logging.info(msg='ORCHESTRATOR SHUTTING DOWN...')
except Exception as e: except Exception as e:
logging.error(traceback.format_exc()) logging.error(msg=traceback.format_exc())
logging.critical(f'*** ORCHESTRATOR CRASHED: {e}') 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__': 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( logging.basicConfig(
force=True, force=True,
@@ -93,6 +79,6 @@ if __name__ == '__main__':
format='%(asctime)s - %(levelname)s - %(message)s', format='%(asctime)s - %(levelname)s - %(message)s',
filemode='w' filemode='w'
) )
logging.info(f"STARTED: {START_TIME}") logging.info(msg=f"STARTED: {START_TIME}")
asyncio.run(main()) 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 os
import time import time
import traceback import traceback
from dataclasses import dataclass, field from dataclasses import asdict
from datetime import datetime from datetime import datetime
from typing import AsyncContextManager from typing import AsyncContextManager
import modules.structs as structs
import pandas as pd import pandas as pd
import requests import requests
import valkey import valkey
from dotenv import load_dotenv from dotenv import load_dotenv
import modules.manual_leverage as leverage
# 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 ### ### MANUAL LEVERAGE DATA ###
LEVERAGE_BY_EXCH: list[Asset_Leverage] = [ df_leverage_by_exch = pd.DataFrame(data=leverage.LEVERAGE_BY_EXCH)
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 ### ### Database ###
# CON: AsyncContextManager | None = None # CON: AsyncContextManager | None = None
VAL_KEY = None VAL_KEY: valkey.Valkey
VK_OUT = 'fr_engine_best_fund_rate_output' VK_OUT: str = 'fr_engine_best_fund_rate_output'
### Logging ### ### Logging ###
load_dotenv() 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 ### ### CONSTANTS ###
LOOP_SLEEP_SEC: int = 5 LOOP_SLEEP_SEC: int = 5
@@ -65,9 +39,9 @@ Mkt_Volume_Last_Refresh_TS_ms: int
def get_extended_markets_info() -> pd.DataFrame: def get_extended_markets_info() -> pd.DataFrame:
global Mkt_Info_Last_Refresh_TS_ms 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'] = df['marketStats'].apply(lambda x: x.get('fundingRate',{}))
df['funding_rate_ts'] = df['marketStats'].apply(lambda x: x.get('nextFundingRate',{})) 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_order_size'] = df['tradingConfig'].apply(lambda x: x.get('minOrderSize',{}))
@@ -81,9 +55,9 @@ def get_extended_markets_info() -> pd.DataFrame:
return df return df
def load_aster_current_fr() -> pd.DataFrame: 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_updated_dt'] = pd.to_datetime(df['funding_rate_updated_ts_ms'], unit='ms')
df['funding_rate'] = df['funding_rate'].astype(float) 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() 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 return df
def load_extend_current_fr(df_mkt_stats: pd.DataFrame) -> pd.DataFrame: 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_updated_dt'] = pd.to_datetime(df['funding_rate_updated_ts_ms'], unit='ms')
df['funding_rate'] = df['funding_rate'].astype(float) 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: 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 = df.loc[df['status']=='ACTIVE',:] df: pd.DataFrame = df.loc[df['status']=='ACTIVE',:]
df['USDT_Symbol'] = df['assetName'] + 'USDT' 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 return df
@@ -124,25 +98,40 @@ async def loop() -> None:
df_comb_fr['net_funding_rate_abs'] = df_comb_fr['net_funding_rate'].abs() df_comb_fr['net_funding_rate_abs'] = df_comb_fr['net_funding_rate'].abs()
### NET MULT ### ### 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'] = 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'] = 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_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) 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)
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)) ASTER = structs.Perpetual_Exchange(
print(best_next_funding_pair) 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(name=VK_OUT, value=json.dumps(obj=best_next_funding_pair))
# print(best_next_funding_pair)
time.sleep(LOOP_SLEEP_SEC) time.sleep(LOOP_SLEEP_SEC)
continue continue
except valkey.exceptions.ConnectionError as e: except valkey.exceptions.ConnectionError as e:
logging.info(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('SHUTTING DOWN...')
except Exception as e: except Exception as e:
logging.error(traceback.format_exc()) logging.error(traceback.format_exc())
logging.critical(f'*** ORCHESTRATOR CRASHED: {e}') logging.critical(f'*** CRASHED: {e}')
### STARTUP ### ### STARTUP ###
@@ -156,9 +145,9 @@ async def main() -> None:
await loop() await loop()
if __name__ == '__main__': 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( logging.basicConfig(
force=True, force=True,
@@ -167,6 +156,6 @@ if __name__ == '__main__':
format='%(asctime)s - %(levelname)s - %(message)s', format='%(asctime)s - %(levelname)s - %(message)s',
filemode='w' filemode='w'
) )
logging.info(f"STARTED: {START_TIME}") logging.info(msg=f"STARTED: {START_TIME}")
asyncio.run(main()) 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": [ "cells": [
{ {
"cell_type": "code", "cell_type": "code",
"execution_count": 9, "execution_count": 2,
"id": "6c70a8c3", "id": "6c70a8c3",
"metadata": {}, "metadata": {},
"outputs": [], "outputs": [],
@@ -27,7 +27,7 @@
}, },
{ {
"cell_type": "code", "cell_type": "code",
"execution_count": 12, "execution_count": 3,
"id": "ff971ca9", "id": "ff971ca9",
"metadata": {}, "metadata": {},
"outputs": [], "outputs": [],
@@ -49,19 +49,10 @@
}, },
{ {
"cell_type": "code", "cell_type": "code",
"execution_count": 13, "execution_count": 4,
"id": "fc2c6d2b", "id": "fc2c6d2b",
"metadata": {}, "metadata": {},
"outputs": [ "outputs": [],
{
"name": "stderr",
"output_type": "stream",
"text": [
"Unclosed client session\n",
"client_session: <aiohttp.client.ClientSession object at 0x7b2dc26ba850>\n"
]
}
],
"source": [ "source": [
"client, trading_client = await extend_auth.create_auth_account_and_trading_client()" "client, trading_client = await extend_auth.create_auth_account_and_trading_client()"
] ]
@@ -146,7 +137,7 @@
}, },
{ {
"cell_type": "code", "cell_type": "code",
"execution_count": 6, "execution_count": 5,
"id": "8dd8aa73", "id": "8dd8aa73",
"metadata": {}, "metadata": {},
"outputs": [], "outputs": [],
@@ -156,23 +147,44 @@
}, },
{ {
"cell_type": "code", "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", "id": "ade14392",
"metadata": {}, "metadata": {},
"outputs": [ "outputs": [
{ {
"data": { "data": {
"text/plain": [ "text/plain": [
"{'status': 'OK', 'data': [], 'error': None, 'pagination': None}" "list"
] ]
}, },
"execution_count": 8, "execution_count": 10,
"metadata": {}, "metadata": {},
"output_type": "execute_result" "output_type": "execute_result"
} }
], ],
"source": [ "source": [
"dict(d)" "type(dict(d).get('data'))"
] ]
}, },
{ {

132
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 asyncio
import json import json
import logging import logging
@@ -8,7 +10,7 @@ import traceback
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
from dataclasses import dataclass, asdict
from typing import Any from typing import Any
import numpy as np import numpy as np
import pandas as pd import pandas as pd
@@ -19,24 +21,24 @@ import valkey
from dotenv import load_dotenv from dotenv import load_dotenv
from sqlalchemy import text from sqlalchemy import text
from sqlalchemy.ext.asyncio import create_async_engine 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.utils as utils
import modules.aster_auth as aster_auth import modules.aster_auth as aster_auth
import modules.extended_auth as extend_auth import modules.extended_auth as extend_auth
import modules.structs as structs import modules.structs as structs
### Database ### ### Clients ###
EXTEND_CLIENT = None EXTEND_CLIENT: PerpetualTradingClient
CON: AsyncContextManager | None = None CON: AsyncContextManager
VAL_KEY = None VAL_KEY: valkey.Valkey
### Logging ### ### Logging ###
load_dotenv() 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 ###
ALGO_CONFIG: structs.Algo_Config | None = None ALGO_CONFIG: structs.Algo_Config
MIN_TIME_TO_FUNDING: int MIN_TIME_TO_FUNDING: int
### EXCHANGES ### ### EXCHANGES ###
@@ -56,7 +58,6 @@ EXTEND = structs.Perpetual_Exchange(
### GLOBALS ### ### GLOBALS ###
Last_Aster_Fill_Time_Ts: float = 0.00 Last_Aster_Fill_Time_Ts: float = 0.00
Just_Rejected_Or_Expired: bool = False Just_Rejected_Or_Expired: bool = False
Best_Symbol_by_Exchange: dict = {}
# ASTER_MULT = 150 # ASTER_MULT = 150
# EXTEND_MULT = 50 # EXTEND_MULT = 50
@@ -122,7 +123,7 @@ async def get_aster_collateral():
r = await aster_auth.post_authenticated_url(fut_acct_balances) 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')) 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_OBJ
global ASTER_NOTIONAL_POSITION global ASTER_NOTIONAL_POSITION
global ASTER_UNREALIZED_PNL global ASTER_UNREALIZED_PNL
@@ -131,14 +132,14 @@ async def get_aster_notional_position(resp: dict | None = None):
previous_notional_obj = ASTER_NOTIONAL_OBJ previous_notional_obj = ASTER_NOTIONAL_OBJ
if not resp: if not resp:
fut_acct_positionRisk = { fut_acct_positionRisk: dict = {
"url": "/fapi/v3/positionRisk", "url": "/fapi/v3/positionRisk",
"method": "GET", "method": "GET",
"params": { "params": {
'symbol': ASTER.symbol, '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 = [x for x in resp if x.get('symbol', None) == ASTER.symbol][0]
d['timestamp_arrival'] = round(datetime.now().timestamp()*1000) d['timestamp_arrival'] = round(datetime.now().timestamp()*1000)
else: else:
@@ -168,7 +169,7 @@ 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: 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}') 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():
@@ -177,7 +178,7 @@ async def get_extend_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: list | 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
@@ -238,15 +239,15 @@ async def get_extend_notional(resp: dict | None = None):
async def get_aster_exch_info(): async def get_aster_exch_info():
global ASTER_MIN_ORDER_QTY global ASTER_MIN_ORDER_QTY
fut_acct_exchangeInfo = { fut_acct_exchangeInfo: dict = {
"url": "/fapi/v3/exchangeInfo", "url": "/fapi/v3/exchangeInfo",
"method": "GET", "method": "GET",
"params": {} "params": {}
} }
r = await aster_auth.post_authenticated_url(fut_acct_exchangeInfo) r: dict = await aster_auth.post_authenticated_url(fut_acct_exchangeInfo) # ty:ignore[invalid-assignment]
s = r['symbols'] s: list = r['symbols']
d = [d for d in s if d.get('symbol', None) == 'ETHUSDT'][0] d: dict = [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] f: dict = [f for f in d['filters'] if f.get('filterType', None) == 'LOT_SIZE'][0]
ASTER_MIN_ORDER_QTY = float(f['minQty']) ASTER_MIN_ORDER_QTY = float(f['minQty'])
async def get_extend_exch_info(): async def get_extend_exch_info():
@@ -290,7 +291,7 @@ async def run_algo():
global EXTEND_OPEN_ORDERS global EXTEND_OPEN_ORDERS
global Last_Aster_Fill_Time_Ts global Last_Aster_Fill_Time_Ts
global Just_Rejected_Or_Expired global Just_Rejected_Or_Expired
global Best_Symbol_by_Exchange # global Best_Symbol_by_Exchange
try: try:
while True: while True:
@@ -298,20 +299,22 @@ async def run_algo():
# print('__________Start___________') # print('__________Start___________')
### ALGO CONIFG ### ### 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 = structs.Algo_Config(**ALGO_CONFIG)
ALGO_CONFIG.Config.Max_Target_Notional = float(min([ASTER.mult, EXTEND.mult]) * ALGO_CONFIG.Config.Target_Open_Cash_Position) 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 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')) 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')) 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')) 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)) ASTER_FUND_RATE: float = 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 = float(EXTENDED_FUND_RATE_DICT.get('funding_rate', 0))
if ALGO_CONFIG.Overrides.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
@@ -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 = float(EXTENDED_FUND_RATE_DICT.get('next_funding_time_ts_ms', 0))
EXTEND_FUND_RATE_TIME = max([EXTEND_FUND_RATE_TIME, 0]) EXTEND_FUND_RATE_TIME = max([EXTEND_FUND_RATE_TIME, 0])
ASTER_TICKER_DICT = json.loads(VAL_KEY.get('fut_ticker_aster')) 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')) EXTENDED_TICKER_DICT = json.loads(VAL_KEY.get('fut_ticker_extended')) # ty:ignore[invalid-argument-type]
### Manage Local Collateral Using Updates from WS ### ### Manage Local Collateral Using Updates from WS ###
ASTER_WS_COLLATERAL_UPDATES = VAL_KEY.get('fr_aster_user_positions') 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 = 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 ### ### Manage Local Notionals Using Updates from WS ###
ASTER_WS_POS_UPDATES = VAL_KEY.get('fr_aster_user_positions') ASTER_WS_POS_UPDATES: Any = VAL_KEY.get(name='fr_aster_user_positions')
ASTER_WS_POS_UPDATES = json.loads(ASTER_WS_POS_UPDATES) if ASTER_WS_POS_UPDATES is not None else [] 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 = VAL_KEY.get('fr_extended_user_positions') EXTEND_WS_POS_UPDATES: Any = 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 [] 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 ### ### Manage Local Orders Using Updates from WS ###
ASTER_WS_ORDER_UPDATES = VAL_KEY.get('fr_aster_user_orders') ASTER_WS_ORDER_UPDATES: Any = 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 [] ASTER_WS_ORDER_UPDATES: list = 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: Any = 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 [] 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 # # CHECK NO MORE THAN 1 OPEN ORDER ON EITHER EXCHANGE #
if len(ASTER_OPEN_ORDERS) > 1 or len(EXTEND_OPEN_ORDERS) > 1: 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] 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: if len(order_update) > 0:
order_update = order_update[0] order_update: dict = order_update[0]
order_update_status = order_update.get('status') order_update_status: str = order_update['status']
order_status_changed = order_orig_status.upper() != order_update_status.upper() order_status_changed: bool = order_orig_status.upper() != order_update_status.upper()
if order_status_changed: if order_status_changed:
logging.info(f'EXTEND ORDER ({order_id}): {order_orig_status} -> {order_update_status}') 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}') 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 (best_symbol_by_exchange_aster.symbol != ASTER.symbol) or (best_symbol_by_exchange_extend.symbol != EXTEND.symbol):
# if abs( ASTER_NOTIONAL_POSITION ) > 0 or abs( EXTEND_NOTIONAL_POSITION ) > 0: # if abs( ASTER_NOTIONAL_POSITION ) > 0.00 or abs( EXTEND_NOTIONAL_POSITION ) > 0.00:
# print('Symbol switch - Flattening Positions') # print('Symbol switch - Flattening Positions')
# ALGO_CONFIG.Overrides.Flatten_Open_Positions = True # ALGO_CONFIG.Overrides.Flatten_Open_Positions = True
# else: # else:
# print(f'Balances Flattened - Updating to Trade New Symbol: ASTER.symbol -> {Best_Symbol_by_Exchange['symbol_aster']}') # print('Balances Flattened - Updating to Trade New Symbols:')
# # ASTER.symbol = Best_Symbol_by_Exchange['symbol_aster'] # print(f' ASTER.symbol -> {best_symbol_by_exchange_aster.symbol}')
# # EXTEND.symbol = Best_Symbol_by_Exchange['symbol_extended'] # 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 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: Any = logging.info if use_logging else print
OUT(f''' OUT(f'''
LOOP SLEEP (SEC): {ALGO_CONFIG.Config.Loop_Sleep_Sec} 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}] 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}] 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 ---
{ASTER_OPEN_ORDERS} {ASTER_OPEN_ORDERS}
@@ -583,8 +591,8 @@ async def run_algo():
### 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_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 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: 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') logging.info('ASTER OPEN ORDER NO PX CHG; SKIPPING')
place_order = False place_order = False
else: else:
cancel_order = { cancel_order: dict = {
"url": "/fapi/v3/order", "url": "/fapi/v3/order",
"method": "DELETE", "method": "DELETE",
"params": { "params": {
@@ -622,7 +630,7 @@ async def run_algo():
'orderId': open_order_id, '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': if cr.get('status', None) == 'CANCELED':
ASTER_OPEN_ORDERS.pop(0) ASTER_OPEN_ORDERS.pop(0)
place_order = True place_order = True
@@ -637,7 +645,7 @@ async def run_algo():
logging.info('ASTER TRYNG TO ORDER 0.00 BASE QTY, SKIPPING') logging.info('ASTER TRYNG TO ORDER 0.00 BASE QTY, SKIPPING')
if place_order: 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 = { post_order = {
"url": "/fapi/v3/order", "url": "/fapi/v3/order",
"method": "POST", "method": "POST",
@@ -650,7 +658,7 @@ async def run_algo():
'price': price, '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: if order_resp.get('orderId', None) is not None:
order_resp['original_price'] = price order_resp['original_price'] = price
order_resp['order_status'] = order_resp['status'] 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 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 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 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: else:
post_only = True 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 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(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: 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})')
@@ -703,13 +711,13 @@ async def run_algo():
open_order_px = 0 open_order_px = 0
place_order = True place_order = True
if place_order: 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: if round(open_order_px - float(price), 2) == 0.00:
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") 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, market_name=symbol,
amount_of_synthetic=qty, amount_of_synthetic=qty,
price=price, price=price,
@@ -780,16 +788,14 @@ async def main():
VAL_KEY = valkey.Valkey(host='localhost', port=6379, db=0, decode_responses=True) VAL_KEY = valkey.Valkey(host='localhost', port=6379, db=0, decode_responses=True)
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', mode='r', encoding='utf-8') as file:
ALGO_CONFIG = json.load(file) ALGO_CONFIG = json.load(file)
ALGO_CONFIG = structs.Algo_Config(**ALGO_CONFIG) 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) 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: async with engine.connect() as CON:
### ASTER SETUP ### ### ASTER SETUP ###

View File

@@ -3,22 +3,23 @@ from dotenv import load_dotenv
import os import os
import time import time
import threading import threading
import urllib from urllib import parse
from eth_account.messages import encode_typed_data from eth_account.messages import encode_typed_data
from eth_account import Account from eth_account import Account
from eth_account.datastructures import SignedMessage
load_dotenv() load_dotenv()
user = os.getenv("RABBY_WALLET") USER: str = os.getenv(key="RABBY_WALLET") # ty:ignore[invalid-assignment]
signer = os.getenv("ASTER_API_WALLET_ADDRESS") SIGNER: str = os.getenv(key="ASTER_API_WALLET_ADDRESS") # ty:ignore[invalid-assignment]
private_key = os.getenv("ASTER_API_PRIVATE_KEY") PRIVATE_KEY: str = os.getenv(key="ASTER_API_PRIVATE_KEY") # ty:ignore[invalid-assignment]
_last_ms = 0 _last_ms = 0
_i = 0 _i = 0
async def post_authenticated_url(req: dict) -> dict: async def post_authenticated_url(req: dict) -> list | dict:
typed_data = { typed_data: dict = {
"types": { "types": {
"EIP712Domain": [ "EIP712Domain": [
{"name": "name", "type": "string"}, {"name": "name", "type": "string"},
@@ -27,7 +28,7 @@ async def post_authenticated_url(req: dict) -> dict:
{"name": "verifyingContract", "type": "address"} {"name": "verifyingContract", "type": "address"}
], ],
"Message": [ "Message": [
{ "name": "msg", "type": "string" } { "name": "msg", "type": "string" },
] ]
}, },
"primaryType": "Message", "primaryType": "Message",
@@ -41,12 +42,12 @@ async def post_authenticated_url(req: dict) -> dict:
"msg": "$msg" "msg": "$msg"
} }
} }
headers = {
headers: dict[str, str] = {
'Content-Type': 'application/x-www-form-urlencoded', 'Content-Type': 'application/x-www-form-urlencoded',
'User-Agent': 'PythonApp/1.0' 'User-Agent': 'PythonApp/1.0'
} }
host = 'https://fapi.asterdex.com' host: str = 'https://fapi.asterdex.com'
def get_nonce(): def get_nonce():
_nonce_lock = threading.Lock() _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) 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() my_dict = req['params'].copy()
url = host + req['url'] url = host + req['url']
method = req['method'] method = req['method']
my_dict['nonce'] = str(get_nonce()) my_dict['nonce'] = str(object=get_nonce())
my_dict['user'] = user my_dict['user'] = USER
my_dict['signer'] = signer my_dict['signer'] = SIGNER
param = urllib.parse.urlencode(my_dict) param: str = parse.urlencode(query=my_dict)
typed_data['message']['msg'] = param 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() full_url: str = url + '?' + param + '&signature=' + signed.signature.hex()
# print(full_url)
if method == 'GET': 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) # print(res.status_code, res.text)
return res.json() return res.json()
elif method == 'POST': elif method == 'POST':
res = requests.post(full_url, headers=headers) res: requests.Response = requests.post(url=full_url, headers=headers)
# print(res.status_code, res.text)
return res.json() return res.json()
elif method == 'PUT': elif method == 'PUT':
res = requests.put(full_url, headers=headers) res: requests.Response = requests.put(url=full_url, headers=headers)
# print(res.status_code, res.text)
return res.json() return res.json()
elif method == 'DELETE': elif method == 'DELETE':
res = requests.delete(full_url, headers=headers) res: requests.Response = requests.delete(url=full_url, headers=headers)
# print(res.status_code, res.text)
return res.json() return res.json()
return await send_by_url(req=req) 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): class Algo_Config_Overrides(BaseModel):
Allow_Ordering_Aster: bool Allow_Ordering_Aster: bool
Allow_Ordering_Extend: bool Allow_Ordering_Extend: bool
Allow_Symbol_Change: bool
Flatten_Open_Positions: bool Flatten_Open_Positions: bool
Flip_Side_For_Testing: bool Flip_Side_For_Testing: bool
# @dataclass(kw_only=True) # @dataclass(kw_only=True)
class Algo_Config_Config(BaseModel): class Algo_Config_Config(BaseModel):
Loop_Sleep_Sec: int Loop_Sleep_Sec: int
@@ -51,8 +53,8 @@ class Valkey_Stream:
none_fill: Any = None none_fill: Any = None
async def update(self): async def update(self):
r = self.client.get(self.channel) r: str = self.client.get(name=self.channel) # ty:ignore[invalid-assignment]
self.data = json.loads(r) if r is not None else self.none_fill self.data = json.loads(s=r) if r is not None else self.none_fill
@dataclass(kw_only=True) @dataclass(kw_only=True)
@@ -162,40 +164,40 @@ class Perpetual_Exchange:
rh_asset: str rh_asset: str
symbol_asset_separator: str = '' symbol_asset_separator: str = ''
async def update(self): # async def update(self):
await self.Collateral_Updates.update() # await self.Collateral_Updates.update()
await self.Order_Updates.update() # await self.Order_Updates.update()
await self.Position_Updates.update() # await self.Position_Updates.update()
await self.Funding_Rate.update() # await self.Funding_Rate.update()
def __post_init__(self) -> None: def __post_init__(self) -> None:
self.symbol = f'{self.lh_asset.upper()}{self.symbol_asset_separator}{self.rh_asset.upper()}' self.symbol = f'{self.lh_asset.upper()}{self.symbol_asset_separator}{self.rh_asset.upper()}'
@dataclass(kw_only=True) # @dataclass(kw_only=True)
class Aster(Perpetual_Exchange): # class Aster(Perpetual_Exchange):
name: str = 'Aster' # name: str = 'Aster'
lh_asset: str = 'ETH' # lh_asset: str = 'ETH'
rh_asset: str = 'USDT' # rh_asset: str = 'USDT'
def __post_init__(self): # def __post_init__(self):
super().__post_init__() # super().__post_init__()
self.Order_Updates = Order_Updates(Valkey=Valkey_Stream(channel = 'fr_aster_user_balances', none_fills = [])) # 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.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.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)) # self.Funding_Rate - Funding_Rate(Valkey=Valkey_Stream(channel = 'fund_rate_aster', none_fills = None))
@dataclass(kw_only=True) # @dataclass(kw_only=True)
class Extend(Perpetual_Exchange): # class Extend(Perpetual_Exchange):
name: str = 'Extended' # name: str = 'Extended'
lh_asset: str = 'ETH' # lh_asset: str = 'ETH'
rh_asset: str = 'USD' # rh_asset: str = 'USD'
symbol_asset_separator: str = '-' # symbol_asset_separator: str = '-'
def __post_init__(self): # def __post_init__(self):
super().__post_init__() # super().__post_init__()
self.Order_Updates = Order_Updates(Valkey=Valkey_Stream(channel = 'fr_aster_user_balances', none_fills = [])) # 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.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.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)) # 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() 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): for index, item in enumerate(list_of_dicts):
if item.get(id) == new_dict.get(id): if item.get(id) == new_dict.get(id):
if seq_check_field is not None: if seq_check_field is not None:
@@ -26,3 +26,18 @@ def send_tg_alert(msg: str):
response = requests.post(url, json={'text': str(str(msg)[:250])}, timeout=10) 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 import traceback
from datetime import datetime from datetime import datetime
from typing import AsyncContextManager from typing import AsyncContextManager
import time
import numpy as np import numpy as np
import pandas as pd import pandas as pd
import requests.packages.urllib3.util.connection as urllib3_cn # type: ignore import requests.packages.urllib3.util.connection as urllib3_cn # type: ignore
@@ -26,32 +26,76 @@ urllib3_cn.allowed_gai_family = allowed_gai_family
### Database ### ### Database ###
USE_DB: bool = True USE_DB: bool = True
USE_VK: bool = True USE_VK: bool = True
CON: AsyncContextManager
VAL_KEY: valkey.Valkey
VK_FUND_RATE = 'fund_rate_aster' VK_FUND_RATE = 'fund_rate_aster'
VK_TICKER = 'fut_ticker_aster' VK_TICKER = 'fut_ticker_aster'
VK_LAST_TRADE = 'fut_last_trade_aster' VK_LAST_TRADE = 'fut_last_trade_aster'
CON: AsyncContextManager | None = None
VAL_KEY = None
### Logging ### ### Logging ###
load_dotenv() 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 ### ### CONSTANTS ###
SYMBOL: str = 'ETHUSDT' SYMBOL: str = 'ETHUSDT'
STREAM_MARKPRICE: str = f'{SYMBOL.lower()}@markPrice@1s' STREAM_MARKPRICE: str = f'{SYMBOL.lower()}@markPrice@1s'
STREAM_BOOKTICKER: str = f'{SYMBOL.lower()}@bookTicker' STREAM_BOOKTICKER: str = f'{SYMBOL.lower()}@bookTicker'
STREAM_TRADES: str = f'{SYMBOL.lower()}@aggTrade' STREAM_TRADES: str = f'{SYMBOL.lower()}@aggTrade'
### Globals ### ### 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 ### ### Websocket ###
async def ws_stream(): async def ws_stream():
async for websocket in websockets.connect(WSS_URL): global SYMBOL
logging.info(f"Connected to {WSS_URL}") 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: try:
async for message in websocket: 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) ts_arrival = round(datetime.now().timestamp()*1000)
if isinstance(message, str): if isinstance(message, str):
try: try:
@@ -132,8 +176,8 @@ async def main():
if USE_VK: if USE_VK:
VAL_KEY = valkey.Valkey(host='localhost', port=6379, db=0) VAL_KEY = valkey.Valkey(host='localhost', port=6379, db=0)
else: else:
VAL_KEY = None
logging.warning("VALKEY NOT BEING USED, NO DATA WILL BE PUBLISHED") logging.warning("VALKEY NOT BEING USED, NO DATA WILL BE PUBLISHED")
raise NotImplementedError('Cannot run without VK')
if USE_DB: if USE_DB:
engine = create_async_engine('mysql+asyncmy://root:pwd@localhost/fund_rate') 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 aster_db.create_fr_aster_mkt_trades(CON=CON)
await ws_stream() await ws_stream()
else: else:
CON = None
logging.warning("DATABASE NOT BEING USED, NO DATA WILL BE RECORDED") logging.warning("DATABASE NOT BEING USED, NO DATA WILL BE RECORDED")
await ws_stream() raise NotImplementedError('Cannot run without DB')
if __name__ == '__main__': if __name__ == '__main__':

View File

@@ -3,16 +3,15 @@ import json
import logging import logging
import socket import socket
import traceback import traceback
from datetime import datetime, timezone from datetime import datetime
from typing import AsyncContextManager from typing import AsyncContextManager
import math
import numpy as np # import numpy as np
import pandas as pd # import pandas as pd
import requests.packages.urllib3.util.connection as urllib3_cn # type: ignore import requests.packages.urllib3.util.connection as urllib3_cn # type: ignore
from sqlalchemy import text # from sqlalchemy import text
import websockets import websockets
from sqlalchemy.ext.asyncio import create_async_engine # from sqlalchemy.ext.asyncio import create_async_engine
import valkey import valkey
import os import os
from dotenv import load_dotenv from dotenv import load_dotenv
@@ -25,31 +24,30 @@ urllib3_cn.allowed_gai_family = allowed_gai_family
### Database ### ### Database ###
USE_DB: bool = False USE_DB: bool = False
USE_VK: bool = True VK_CHANNEL = 'fund_rate_aster_all'
VK_FUND_RATE_ALL = 'fund_rate_aster_all'
CON: AsyncContextManager | None = None CON: AsyncContextManager
VAL_KEY = None VAL_KEY: valkey.Valkey
### Logging ### ### Logging ###
load_dotenv() 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 ### ### Globals ###
WSS_URL = "wss://fstream.asterdex.com/ws/!markPrice@arr" WSS_URL: str = "wss://fstream.asterdex.com/ws/!markPrice@arr"
### Websocket ### ### Websocket ###
async def ws_stream(): async def ws_stream():
async for websocket in websockets.connect(WSS_URL): async for websocket in websockets.connect(WSS_URL):
logging.info(f"Connected to {WSS_URL}") logging.info(msg=f"Connected to {WSS_URL}")
try: try:
async for message in websocket: async for message in websocket:
if isinstance(message, str): if isinstance(message, str):
try: try:
data = json.loads(message) data: dict = json.loads(message)
if data: 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)}') # print(f'VK_SAVED: {len(data)}')
continue continue
else: else:
@@ -61,39 +59,35 @@ async def ws_stream():
else: else:
raise ValueError(f'Type: {type(data)} not expected: {message}') raise ValueError(f'Type: {type(data)} not expected: {message}')
except websockets.ConnectionClosed as e: except websockets.ConnectionClosed as e:
logging.error(f'Connection closed: {e}') logging.error(msg=f'Connection closed: {e}')
logging.error(traceback.format_exc()) logging.error(msg=traceback.format_exc())
continue utils.send_tg_alert(msg=f'WS: {VK_CHANNEL} - Failure: {e}')
except Exception as e: except Exception as e:
logging.error(f'Connection closed: {e}') logging.error(msg=f'Connection closed: {e}')
logging.error(traceback.format_exc()) logging.error(msg=traceback.format_exc())
utils.send_tg_alert(msg=f'WS: {VK_CHANNEL} - Failure: {e}')
### Startup ### ### Startup ###
async def main(): async def main():
global VAL_KEY global VAL_KEY
global CON global CON
if USE_VK:
VAL_KEY = valkey.Valkey(host='localhost', port=6379, db=0) 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: if USE_DB:
engine = create_async_engine('mysql+asyncmy://root:pwd@localhost/fund_rate') raise NotImplementedError('DB not implemented for this ws.')
async with engine.connect() as CON: # engine = create_async_engine('mysql+asyncmy://root:pwd@localhost/fund_rate')
# await create_rtds_btcusd_table(CON=CON) # async with engine.connect() as CON:
await ws_stream() # await ws_stream()
else: else:
CON = None
logging.warning("DATABASE NOT BEING USED, NO DATA WILL BE RECORDED") logging.warning("DATABASE NOT BEING USED, NO DATA WILL BE RECORDED")
await ws_stream() await ws_stream()
if __name__ == '__main__': 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( logging.basicConfig(
force=True, force=True,
@@ -102,9 +96,9 @@ if __name__ == '__main__':
format='%(asctime)s - %(levelname)s - %(message)s', format='%(asctime)s - %(levelname)s - %(message)s',
filemode='w' filemode='w'
) )
logging.info(f"STARTED: {START_TIME}") logging.info(msg=f"STARTED: {START_TIME}")
try: try:
asyncio.run(main()) asyncio.run(main())
except KeyboardInterrupt: except KeyboardInterrupt:
logging.info("Stream stopped") logging.info(msg="Stream stopped")

View File

@@ -1,20 +1,19 @@
import asyncio import asyncio
import json import json
import logging import logging
import os
import socket import socket
import traceback import traceback
from datetime import datetime from datetime import datetime
from typing import AsyncContextManager from typing import AsyncContextManager
import numpy as np
import pandas as pd
import requests.packages.urllib3.util.connection as urllib3_cn # type: ignore 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 valkey
import os
import websockets
from dotenv import load_dotenv from dotenv import load_dotenv
from sqlalchemy.ext.asyncio import create_async_engine
import modules.aster_auth as aster_auth import modules.aster_auth as aster_auth
import modules.aster_db as aster_db import modules.aster_db as aster_db
import modules.db as db import modules.db as db
@@ -28,84 +27,84 @@ urllib3_cn.allowed_gai_family = allowed_gai_family
### Database ### ### Database ###
USE_DB: bool = True USE_DB: bool = True
USE_VK: bool = True USE_VK: bool = True
VK_ORDERS_TRADES = 'fr_aster_user_orders' CON: AsyncContextManager
VK_MARGIN_CALLS = 'fr_aster_user_margin_calls' VAL_KEY: valkey.Valkey
VK_BALANCES = 'fr_aster_user_balances' VK_ORDERS_TRADES: str = 'fr_aster_user_orders'
VK_POSITIONS = 'fr_aster_user_positions' VK_MARGIN_CALLS: str = 'fr_aster_user_margin_calls'
CON: AsyncContextManager | None = None VK_BALANCES: str = 'fr_aster_user_balances'
VAL_KEY = None VK_POSITIONS: str = 'fr_aster_user_positions'
### Logging ### ### Logging ###
load_dotenv() 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 ### ### CONSTANTS ###
WSS_URL = "wss://fstream.asterdex.com/ws/" WSS_URL: str = "wss://fstream.asterdex.com/ws/"
LOCAL_RECENT_UPDATES_LOOKBACK_SEC = 30 LOCAL_RECENT_UPDATES_LOOKBACK_SEC: int = 30
### Globals ### ### Globals ###
LISTEN_KEY: str | None = None Listen_Key: str
LISTEN_KEY_LAST_UPDATE_TS_S: int = 0 Listen_Key_Last_Update_TS_S: int = 0
LISTEN_KEY_PUT_INTERVAL_SEC = 1800 Listen_Key_Put_Interval_Sec: int = 1800
LOCAL_RECENT_ORDERS: list = [] Local_Recent_Orders: list[dict] = []
LOCAL_RECENT_MARGIN_CALLS: list = [] Local_Recent_Margin_Calls: list[dict] = []
LOCAL_RECENT_BALANCES: list = [] Local_Recent_Balances: list[dict] = []
LOCAL_RECENT_POSITIONS: list = [] Local_Recent_Positions: list[dict] = []
async def get_new_listen_key() -> str: 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", "url": "/fapi/v3/listenKey",
"method": "POST", "method": "POST",
"params": {} "params": {}
} }
r = await aster_auth.post_authenticated_url(listen_key_request) r: dict = await aster_auth.post_authenticated_url(listen_key_request) # ty:ignore[invalid-assignment]
listen_key = r.get('listenKey', None) listen_key: str = r.get('listenKey', '')
print(f'LISTEN KEY: {listen_key}') print(f'LISTEN KEY: {listen_key}')
if listen_key is not None: if listen_key:
LISTEN_KEY_LAST_UPDATE_TS_S = round(datetime.now().timestamp()) Listen_Key_Last_Update_TS_S = round(number=datetime.now().timestamp())
return listen_key return listen_key
else: 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(): async def listen_key_interval():
global LISTEN_KEY global Listen_Key
while True: while True:
await asyncio.sleep(LISTEN_KEY_PUT_INTERVAL_SEC) await asyncio.sleep(delay=Listen_Key_Put_Interval_Sec)
LISTEN_KEY = await get_new_listen_key() Listen_Key = await get_new_listen_key()
### Websocket ### ### Websocket ###
async def ws_stream(): async def ws_stream():
global LISTEN_KEY global Listen_Key
global LOCAL_RECENT_ORDERS global Local_Recent_Orders
global LOCAL_RECENT_MARGIN_CALLS global Local_Recent_Margin_Calls
global LOCAL_RECENT_BALANCES global Local_Recent_Balances
global LOCAL_RECENT_POSITIONS 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): async for websocket in websockets.connect(uri=WSS_URL+Listen_Key, ping_interval=5):
logging.info(f"Connected to {WSS_URL}") logging.info(msg=f"Connected to {WSS_URL}")
asyncio.create_task(listen_key_interval()) asyncio.create_task(coro=listen_key_interval())
try: try:
async for message in websocket: 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): if isinstance(message, str):
try: try:
data = json.loads(message) data: dict = json.loads(s=message)
channel = data.get('e', None) channel: str = data.get('e', '')
if channel is not None: if channel:
LOOKBACK_MIN_TS_MS = ts_arrival - (LOCAL_RECENT_UPDATES_LOOKBACK_SEC*1000) lookback_min_ts_ms: int = ts_arrival - (LOCAL_RECENT_UPDATES_LOOKBACK_SEC*1000)
match channel: match channel:
case 'ORDER_TRADE_UPDATE': case 'ORDER_TRADE_UPDATE':
# logging.info(f'ORDER_TRADE_UPDATE: {data}') # logging.info(f'ORDER_TRADE_UPDATE: {data}')
new_order_update = { new_order_update: dict = {
'timestamp_arrival': ts_arrival, 'timestamp_arrival': ts_arrival,
'timestamp_msg': data['E'], 'timestamp_msg': data['E'],
'timestamp_transaction': data['T'], '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 '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 '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 = 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 = [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_OBJ: str = json.dumps(obj=Local_Recent_Orders)
VAL_KEY.set(VK_ORDERS_TRADES, VAL_KEY_OBJ) 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) await db.insert_df_to_mysql(table_name='fr_aster_user_order_trade', params=new_order_update, CON=CON)
continue continue
@@ -153,7 +152,7 @@ async def ws_stream():
# logging.info(f'MARGIN_CALL: {data}') # logging.info(f'MARGIN_CALL: {data}')
list_for_df = [] list_for_df = []
for p in list(data['p']): for p in list(data['p']):
margin_call_update = { margin_call_update: dict = {
'timestamp_arrival': ts_arrival, 'timestamp_arrival': ts_arrival,
'timestamp_msg': data['E'], 'timestamp_msg': data['E'],
'cross_wallet_balance': float(data.get('cw', 0)), '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 'maint_margin_required': float(p["mm"]), # :"1.614445" // Maintenance Margin Required
} }
list_for_df.append(margin_call_update) 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 = 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 = [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_OBJ: str = json.dumps(obj=Local_Recent_Margin_Calls)
VAL_KEY.set(VK_MARGIN_CALLS, VAL_KEY_OBJ) 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) await db.insert_df_to_mysql(table_name='fr_aster_user_margin', params=list_for_df, CON=CON)
continue continue
@@ -183,7 +182,7 @@ async def ws_stream():
### Balance Updates ### ### Balance Updates ###
if len(list(data['a']['B'])) > 0: if len(list(data['a']['B'])) > 0:
for b in list(data['a']['B']): for b in list(data['a']['B']):
balance_update = { balance_update: dict = {
'timestamp_arrival': ts_arrival, 'timestamp_arrival': ts_arrival,
'timestamp_msg': data['E'], 'timestamp_msg': data['E'],
'timestamp_transaction': data['T'], 'timestamp_transaction': data['T'],
@@ -196,13 +195,13 @@ async def ws_stream():
'balance_change_excl_pnl_comms': float(b['bc']), 'balance_change_excl_pnl_comms': float(b['bc']),
} }
list_for_df_bal.append(balance_update) 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 = 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] 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)) VAL_KEY.set(name=VK_BALANCES, value=json.dumps(obj=Local_Recent_Balances))
### Position Updates ### ### Position Updates ###
if len(list(data['a']['P'])) > 0: if len(list(data['a']['P'])) > 0:
for p in list(data['a']['P']): for p in list(data['a']['P']):
position_update = { position_update: dict = {
'timestamp_arrival': ts_arrival, 'timestamp_arrival': ts_arrival,
'timestamp_msg': data['E'], 'timestamp_msg': data['E'],
'timestamp_transaction': data['T'], 'timestamp_transaction': data['T'],
@@ -219,33 +218,34 @@ async def ws_stream():
'position_side': p['ps'], 'position_side': p['ps'],
} }
list_for_df_pos.append(position_update) 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 = 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] 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)) VAL_KEY.set(name=VK_POSITIONS, value=json.dumps(obj=Local_Recent_Positions))
if list_for_df_bal: 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) 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: 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) await db.insert_df_to_mysql(table_name='fr_aster_user_account_pos', params=list_for_df_pos, CON=CON)
continue continue
case 'listenKeyExpired': case 'listenKeyExpired':
raise('Listen Key Has Expired; Failed to Update Properly. Restarting.') raise ValueError('Listen Key Has Expired; Failed to Update Properly. Restarting.')
case _: case _:
logging.warning(f'UNMATCHED OTHER MSG: {data}') logging.warning(msg=f'UNMATCHED OTHER MSG: {data}')
else: else:
logging.info(f'Initial or unexpected data struct, skipping: {data}') logging.info(msg=f'Initial or unexpected data struct, skipping: {data}')
continue continue
except (json.JSONDecodeError, ValueError): 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 continue
else: else:
raise ValueError(f'Type: {type(data)} not expected: {message}') raise ValueError(f'Type: {type(data)} not expected: {message}')
except websockets.ConnectionClosed as e: except websockets.ConnectionClosed as e:
logging.error(f'Connection closed: {e}') logging.error(msg=f'Connection closed: {e}')
logging.error(traceback.format_exc()) logging.error(msg=traceback.format_exc())
continue utils.send_tg_alert(msg=f'WS_Aster_User - Failure: {e}')
except Exception as e: except Exception as e:
logging.error(f'Connection closed: {e}') logging.error(msg=f'Connection closed: {e}')
logging.error(traceback.format_exc()) logging.error(msg=traceback.format_exc())
utils.send_tg_alert(msg=f'WS_Aster_User - Failure: {e}')
async def main(): async def main():
@@ -255,8 +255,8 @@ async def main():
if USE_VK: if USE_VK:
VAL_KEY = valkey.Valkey(host='localhost', port=6379, db=0) VAL_KEY = valkey.Valkey(host='localhost', port=6379, db=0)
else: else:
VAL_KEY = None
logging.warning("VALKEY NOT BEING USED, NO DATA WILL BE PUBLISHED") logging.warning("VALKEY NOT BEING USED, NO DATA WILL BE PUBLISHED")
raise NotImplementedError('Cannot run without Valkey')
if USE_DB: if USE_DB:
engine = create_async_engine('mysql+asyncmy://root:pwd@localhost/fund_rate') 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 aster_db.create_fr_aster_user_account_pos(CON=CON)
await ws_stream() await ws_stream()
else: else:
CON = None
logging.warning("DATABASE NOT BEING USED, NO DATA WILL BE RECORDED") logging.warning("DATABASE NOT BEING USED, NO DATA WILL BE RECORDED")
await ws_stream() raise NotImplementedError('Cannot run without DB')
if __name__ == '__main__': 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( logging.basicConfig(
force=True, force=True,
@@ -284,9 +283,9 @@ if __name__ == '__main__':
format='%(asctime)s - %(levelname)s - %(message)s', format='%(asctime)s - %(levelname)s - %(message)s',
filemode='w' filemode='w'
) )
logging.info(f"STARTED: {START_TIME}") logging.info(msg=f"STARTED: {START_TIME}")
try: try:
asyncio.run(main()) asyncio.run(main())
except KeyboardInterrupt: 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 USE_VK: bool = True
VK_FUND_RATE = 'fund_rate_extended' VK_FUND_RATE = 'fund_rate_extended'
CON: AsyncContextManager | None = None CON: AsyncContextManager
VAL_KEY = None VAL_KEY: valkey.Valkey
### Logging ### ### Logging ###
load_dotenv() 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 ### ### CONSTANTS ###
WS_SYMBOL: str = 'ETH-USD' SYMBOL: str = 'ETH-USD'
FUNDING_RATE_INTERVAL_MIN = 60
### Globals ### ### Globals ###
WSS_URL = f"wss://api.starknet.extended.exchange/stream.extended.exchange/v1/funding/{WS_SYMBOL}" ALLOW_SYMBOL_CHG: bool = False
# HIST_TRADES = np.empty((0, 3))
# HIST_TRADES_LOOKBACK_SEC = 6
def time_round_down(dt, interval_mins=5) -> int: # returns timestamp in seconds def time_round_down(dt, interval_mins=5) -> int: # returns timestamp in seconds
interval_secs = interval_mins * 60 interval_secs = interval_mins * 60
@@ -53,80 +48,30 @@ def time_round_down(dt, interval_mins=5) -> int: # returns timestamp in seconds
return rounded_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 ### ### Websocket ###
async def ws_stream(): async def ws_stream():
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): async for websocket in websockets.connect(WSS_URL):
if CHANGE_SYMBOL:
break
logging.info(f"Connected to {WSS_URL}") logging.info(f"Connected to {WSS_URL}")
try: try:
async for message in websocket: 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) ts_arrival = round(datetime.now().timestamp()*1000)
if isinstance(message, str): if isinstance(message, str):
try: try:
@@ -169,16 +114,16 @@ async def main():
if USE_VK: if USE_VK:
VAL_KEY = valkey.Valkey(host='localhost', port=6379, db=0) VAL_KEY = valkey.Valkey(host='localhost', port=6379, db=0)
else: else:
VAL_KEY = None
logging.warning("VALKEY NOT BEING USED, NO DATA WILL BE PUBLISHED") logging.warning("VALKEY NOT BEING USED, NO DATA WILL BE PUBLISHED")
raise NotImplementedError('Cannot run without VK')
if USE_DB: if USE_DB:
engine = create_async_engine('mysql+asyncmy://root:pwd@localhost/fund_rate') raise NotImplementedError('DB not implemented')
async with engine.connect() as CON: # engine = create_async_engine('mysql+asyncmy://root:pwd@localhost/fund_rate')
# await create_rtds_btcusd_table(CON=CON) # async with engine.connect() as CON:
await ws_stream() # # await create_rtds_btcusd_table(CON=CON)
# await ws_stream()
else: else:
CON = None
logging.warning("DATABASE NOT BEING USED, NO DATA WILL BE RECORDED") logging.warning("DATABASE NOT BEING USED, NO DATA WILL BE RECORDED")
await ws_stream() await ws_stream()

View File

@@ -27,25 +27,43 @@ USE_DB: bool = False
USE_VK: bool = True USE_VK: bool = True
VK_TICKER = 'fut_ticker_extended' VK_TICKER = 'fut_ticker_extended'
CON: AsyncContextManager | None = None CON: AsyncContextManager
VAL_KEY = None VAL_KEY: valkey.Valkey
### Logging ### ### Logging ###
load_dotenv() 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 ### ### CONSTANTS ###
WS_SYMBOL: str = 'ETH-USD' SYMBOL: str = 'ETH-USD'
### Globals ### ### Globals ###
WSS_URL = f"wss://api.starknet.extended.exchange/stream.extended.exchange/v1/orderbooks/{WS_SYMBOL}?depth=1" ALLOW_SYMBOL_CHG: bool = False
### Websocket ### ### Websocket ###
async def ws_stream(): async def ws_stream():
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): async for websocket in websockets.connect(WSS_URL):
if CHANGE_SYMBOL:
break
logging.info(f"Connected to {WSS_URL}") logging.info(f"Connected to {WSS_URL}")
try: try:
async for message in websocket: 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) ts_arrival = round(datetime.now().timestamp()*1000)
if isinstance(message, str): if isinstance(message, str):
try: try:
@@ -88,16 +106,16 @@ async def main():
if USE_VK: if USE_VK:
VAL_KEY = valkey.Valkey(host='localhost', port=6379, db=0) VAL_KEY = valkey.Valkey(host='localhost', port=6379, db=0)
else: else:
VAL_KEY = None
logging.warning("VALKEY NOT BEING USED, NO DATA WILL BE PUBLISHED") logging.warning("VALKEY NOT BEING USED, NO DATA WILL BE PUBLISHED")
raise NotImplementedError('Cannot run without VK')
if USE_DB: if USE_DB:
engine = create_async_engine('mysql+asyncmy://root:pwd@localhost/fund_rate') raise NotImplementedError('DB not implemented')
async with engine.connect() as CON: # engine = create_async_engine('mysql+asyncmy://root:pwd@localhost/fund_rate')
# await create_rtds_btcusd_table(CON=CON) # async with engine.connect() as CON:
await ws_stream() # # await create_rtds_btcusd_table(CON=CON)
# await ws_stream()
else: else:
CON = None
logging.warning("DATABASE NOT BEING USED, NO DATA WILL BE RECORDED") logging.warning("DATABASE NOT BEING USED, NO DATA WILL BE RECORDED")
await ws_stream() await ws_stream()

View File

@@ -29,30 +29,50 @@ USE_DB: bool = True
USE_VK: bool = True USE_VK: bool = True
VK_LAST_TRADE = 'fut_last_trade_extended' VK_LAST_TRADE = 'fut_last_trade_extended'
CON: AsyncContextManager | None = None CON: AsyncContextManager
VAL_KEY = None VAL_KEY: valkey.Valkey
### Logging ### ### Logging ###
load_dotenv() 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 ### ### CONSTANTS ###
WS_SYMBOL: str = 'ETH-USD' SYMBOL: str = 'ETH-USD'
### Globals ### ### Globals ###
WSS_URL = f"wss://api.starknet.extended.exchange/stream.extended.exchange/v1/publicTrades/{WS_SYMBOL}" ALLOW_SYMBOL_CHG: bool = False
### Websocket ### ### Websocket ###
async def ws_stream(): async def ws_stream():
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): async for websocket in websockets.connect(WSS_URL):
if CHANGE_SYMBOL:
break
logging.info(f"Connected to {WSS_URL}") logging.info(f"Connected to {WSS_URL}")
try: try:
async for message in websocket: 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) ts_arrival = round(datetime.now().timestamp()*1000)
if isinstance(message, str): if isinstance(message, str):
try: try:
data = json.loads(message) data = json.loads(message)
if data.get('data', None) is not None: if data.get('data', None) is not None:
# print(data)
if data['seq'] == 1: # Skip first msg that has historical trades if data['seq'] == 1: # Skip first msg that has historical trades
continue continue
list_for_df = [] list_for_df = []
@@ -100,8 +120,8 @@ async def main():
if USE_VK: if USE_VK:
VAL_KEY = valkey.Valkey(host='localhost', port=6379, db=0) VAL_KEY = valkey.Valkey(host='localhost', port=6379, db=0)
else: else:
VAL_KEY = None
logging.warning("VALKEY NOT BEING USED, NO DATA WILL BE PUBLISHED") logging.warning("VALKEY NOT BEING USED, NO DATA WILL BE PUBLISHED")
raise NotImplementedError('Cannot run without VK')
if USE_DB: if USE_DB:
engine = create_async_engine('mysql+asyncmy://root:pwd@localhost/fund_rate') 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 extended_db.create_fr_extended_mkt_trades(CON=CON)
await ws_stream() await ws_stream()
else: else:
CON = None
logging.warning("DATABASE NOT BEING USED, NO DATA WILL BE RECORDED") logging.warning("DATABASE NOT BEING USED, NO DATA WILL BE RECORDED")
await ws_stream() raise NotImplementedError('Cannot run without DB')
if __name__ == '__main__': if __name__ == '__main__':

View File

@@ -32,16 +32,16 @@ VK_ORDERS = 'fr_extended_user_orders'
VK_TRADES = 'fr_extended_user_trades' VK_TRADES = 'fr_extended_user_trades'
VK_BALANCES = 'fr_extended_user_balances' VK_BALANCES = 'fr_extended_user_balances'
VK_POSITIONS = 'fr_extended_user_positions' VK_POSITIONS = 'fr_extended_user_positions'
CON: AsyncContextManager | None = None CON: AsyncContextManager
VAL_KEY = None VAL_KEY: valkey.Valkey
### Logging ### ### Logging ###
load_dotenv() 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 ### ### CONSTANTS ###
WSS_URL = "wss://api.starknet.extended.exchange/stream.extended.exchange/v1/account" 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 LOCAL_RECENT_UPDATES_LOOKBACK_SEC = 30
### Globals ### ### Globals ###
@@ -226,8 +226,8 @@ async def main():
if USE_VK: if USE_VK:
VAL_KEY = valkey.Valkey(host='localhost', port=6379, db=0) VAL_KEY = valkey.Valkey(host='localhost', port=6379, db=0)
else: else:
VAL_KEY = None
logging.warning("VALKEY NOT BEING USED, NO DATA WILL BE PUBLISHED") logging.warning("VALKEY NOT BEING USED, NO DATA WILL BE PUBLISHED")
raise NotImplementedError('Cannot run without VK')
if USE_DB: if USE_DB:
engine = create_async_engine('mysql+asyncmy://root:pwd@localhost/fund_rate') 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 extended_db.create_fr_extended_user_trade(CON=CON)
await ws_stream() await ws_stream()
else: else:
CON = None
logging.warning("DATABASE NOT BEING USED, NO DATA WILL BE RECORDED") 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__': if __name__ == '__main__':