added extend symbol change for ws
This commit is contained in:
259
algo.ipynb
259
algo.ipynb
@@ -2,7 +2,7 @@
|
||||
"cells": [
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 22,
|
||||
"execution_count": 4,
|
||||
"id": "d1eed397",
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
@@ -11,6 +11,7 @@
|
||||
"import json\n",
|
||||
"from dataclasses import dataclass, asdict\n",
|
||||
"import valkey\n",
|
||||
"import modules.utils as utils\n",
|
||||
"\n",
|
||||
"with open('algo_config.json', 'r', encoding='utf-8') as file:\n",
|
||||
" ALGO_CONFIG = json.load(file)\n",
|
||||
@@ -19,7 +20,7 @@
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 23,
|
||||
"execution_count": 5,
|
||||
"id": "c6151613",
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
@@ -29,7 +30,7 @@
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"execution_count": 38,
|
||||
"id": "d83c61e5",
|
||||
"metadata": {},
|
||||
"outputs": [
|
||||
@@ -39,30 +40,31 @@
|
||||
"1"
|
||||
]
|
||||
},
|
||||
"execution_count": 4,
|
||||
"execution_count": 38,
|
||||
"metadata": {},
|
||||
"output_type": "execute_result"
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"config_update = {\n",
|
||||
" # 'Config': {\n",
|
||||
" # 'Loop_Sleep_Sec': 0.00,\n",
|
||||
" # 'Min_Time_To_Funding_Minutes': 60,\n",
|
||||
" # 'Min_Fund_Rate_Pct_To_Trade': 0.001,\n",
|
||||
" # 'Price_Worsener_Extend': 0.0,\n",
|
||||
" # 'Price_Worsener_Aster': 0.0,\n",
|
||||
" # 'Switch_To_Taker_Seconds': 1,\n",
|
||||
" # },\n",
|
||||
" 'Config': {\n",
|
||||
" # 'Loop_Sleep_Sec': 0.00,\n",
|
||||
" # 'Min_Time_To_Funding_Minutes': 60,\n",
|
||||
" # 'Min_Fund_Rate_Pct_To_Trade': 0.0002,\n",
|
||||
" # 'Price_Worsener_Extend': 0.0,\n",
|
||||
" # 'Price_Worsener_Aster': 0.0,\n",
|
||||
" # 'Switch_To_Taker_Seconds': 1,\n",
|
||||
" },\n",
|
||||
" 'Logging': {\n",
|
||||
" 'Log_Summary_Each_Loop': False,\n",
|
||||
" 'Print_Summary_Each_Loop': True,\n",
|
||||
" # 'Log_Summary_Each_Loop': False,\n",
|
||||
" 'Print_Summary_Each_Loop': False,\n",
|
||||
" },\n",
|
||||
" # 'Overrides': {\n",
|
||||
" # 'Allow_Ordering_Aster': True,\n",
|
||||
" # 'Allow_Ordering_Extend': True,\n",
|
||||
" # 'Flip_Side_For_Testing': False,\n",
|
||||
" # 'Flatten_Open_Positions': False,\n",
|
||||
" # # 'Allow_Ordering_Aster': True,\n",
|
||||
" # # 'Allow_Ordering_Extend': True,\n",
|
||||
" # # 'Allow_Symbol_Change': False,\n",
|
||||
" # # 'Flip_Side_For_Testing': False,\n",
|
||||
" # # 'Flatten_Open_Positions': False,\n",
|
||||
" # },\n",
|
||||
"}\n",
|
||||
"VAL_KEY.publish('fr_orchestrator_input', json.dumps(config_update))"
|
||||
@@ -70,29 +72,30 @@
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 93,
|
||||
"id": "45fae761",
|
||||
"execution_count": 52,
|
||||
"id": "f5260342",
|
||||
"metadata": {},
|
||||
"outputs": [
|
||||
{
|
||||
"data": {
|
||||
"text/plain": [
|
||||
"5.0"
|
||||
"1"
|
||||
]
|
||||
},
|
||||
"execution_count": 93,
|
||||
"execution_count": 52,
|
||||
"metadata": {},
|
||||
"output_type": "execute_result"
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"json.loads(VAL_KEY.get('fr_orchestrator_output'), object_hook=lambda d: structs.Algo_Config(**d)).Loop_Sleep_Sec"
|
||||
"order = {'order_id':'test_1'}\n",
|
||||
"VAL_KEY.publish('fr_engine_orders_input', json.dumps(order))"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"id": "98c500cc",
|
||||
"id": "940586bb",
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": []
|
||||
@@ -100,7 +103,15 @@
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"id": "f2cf3325",
|
||||
"id": "cd600e0e",
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": []
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"id": "db52edf9",
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": []
|
||||
@@ -112,92 +123,6 @@
|
||||
"outputs": [],
|
||||
"source": []
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 31,
|
||||
"id": "a0df43de",
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"pos = json.loads(VAL_KEY.get('fr_aster_user_positions'))"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 32,
|
||||
"id": "ca526c8a",
|
||||
"metadata": {},
|
||||
"outputs": [
|
||||
{
|
||||
"data": {
|
||||
"text/plain": [
|
||||
"[{'timestamp_arrival': 1777303258987,\n",
|
||||
" 'timestamp_msg': 1777303258979,\n",
|
||||
" 'timestamp_transaction': 1777303258950,\n",
|
||||
" 'event_reason_type': 'ORDER',\n",
|
||||
" 'symbol': 'ETHUSDT',\n",
|
||||
" 'position_amount': 0.226,\n",
|
||||
" 'entry_price': 2284.28,\n",
|
||||
" 'accumulated_realized_pre_fees': 8.24392002,\n",
|
||||
" 'unrealized_pnl': 0.0,\n",
|
||||
" 'margin_type': 'cross',\n",
|
||||
" 'isolated_wallet': 0.0,\n",
|
||||
" 'position_side': 'BOTH'}]"
|
||||
]
|
||||
},
|
||||
"execution_count": 32,
|
||||
"metadata": {},
|
||||
"output_type": "execute_result"
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"pos"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 33,
|
||||
"id": "f788b6df",
|
||||
"metadata": {},
|
||||
"outputs": [
|
||||
{
|
||||
"data": {
|
||||
"text/plain": [
|
||||
"Timestamp('2026-04-27 15:20:58.987000')"
|
||||
]
|
||||
},
|
||||
"execution_count": 33,
|
||||
"metadata": {},
|
||||
"output_type": "execute_result"
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"import pandas as pd\n",
|
||||
"pd.to_datetime(1777303258987, unit='ms')"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 34,
|
||||
"id": "855f980b",
|
||||
"metadata": {},
|
||||
"outputs": [
|
||||
{
|
||||
"data": {
|
||||
"text/plain": [
|
||||
"Timestamp('2026-04-27 15:20:58.979000')"
|
||||
]
|
||||
},
|
||||
"execution_count": 34,
|
||||
"metadata": {},
|
||||
"output_type": "execute_result"
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"import pandas as pd\n",
|
||||
"pd.to_datetime(1777303258979, unit='ms')"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 35,
|
||||
@@ -219,6 +144,118 @@
|
||||
"pd.to_datetime(1777303258950, unit='ms')"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 57,
|
||||
"id": "5f7535df",
|
||||
"metadata": {},
|
||||
"outputs": [
|
||||
{
|
||||
"data": {
|
||||
"text/plain": [
|
||||
"'{\"ASTER\": {\"mult\": 150, \"lh_asset\": \"ETH\", \"rh_asset\": \"USD\", \"symbol_asset_separator\": \"\"}, \"EXTEND\": {\"mult\": 50, \"lh_asset\": \"ETH\", \"rh_asset\": \"USD\", \"symbol_asset_separator\": \"-\"}}'"
|
||||
]
|
||||
},
|
||||
"execution_count": 57,
|
||||
"metadata": {},
|
||||
"output_type": "execute_result"
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"VAL_KEY.get('fr_algo_working_symbol')"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 51,
|
||||
"id": "3acaa8cd",
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"ASTER = structs.Perpetual_Exchange(\n",
|
||||
" mult = 150,\n",
|
||||
" lh_asset = 'ETH',\n",
|
||||
" rh_asset = 'USD',\n",
|
||||
" symbol_asset_separator = '',\n",
|
||||
")\n",
|
||||
"EXTEND = structs.Perpetual_Exchange(\n",
|
||||
" mult = 50,\n",
|
||||
" lh_asset = 'ETH',\n",
|
||||
" rh_asset = 'USD',\n",
|
||||
" symbol_asset_separator = '-',\n",
|
||||
")"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 52,
|
||||
"id": "b417adad",
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"best_symbol_by_exchange: dict = json.loads(s=VAL_KEY.get(name='fr_algo_working_symbol')) # ty:ignore[invalid-argument-type]\n",
|
||||
"best_symbol_by_exchange_aster = structs.Perpetual_Exchange(**asdict(ASTER))\n",
|
||||
"best_symbol_by_exchange_extend = structs.Perpetual_Exchange(**asdict(EXTEND))"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 53,
|
||||
"id": "fa5a8e85",
|
||||
"metadata": {},
|
||||
"outputs": [
|
||||
{
|
||||
"data": {
|
||||
"text/plain": [
|
||||
"Perpetual_Exchange(mult=50, lh_asset='ETH', rh_asset='USD', symbol_asset_separator='-')"
|
||||
]
|
||||
},
|
||||
"execution_count": 53,
|
||||
"metadata": {},
|
||||
"output_type": "execute_result"
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"best_symbol_by_exchange_extend"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 54,
|
||||
"id": "fb81441a",
|
||||
"metadata": {},
|
||||
"outputs": [
|
||||
{
|
||||
"data": {
|
||||
"text/plain": [
|
||||
"True"
|
||||
]
|
||||
},
|
||||
"execution_count": 54,
|
||||
"metadata": {},
|
||||
"output_type": "execute_result"
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"\n",
|
||||
"VAL_KEY.set('fr_algo_working_symbol', json.dumps({'ASTER': asdict(best_symbol_by_exchange_aster), 'EXTEND': asdict(best_symbol_by_exchange_extend)}))"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"id": "09571e38",
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": []
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": []
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"Updated_Timestamp": 1777478176165,
|
||||
"Updated_Timestamp": 1777496051056,
|
||||
"Config": {
|
||||
"Loop_Sleep_Sec": 0.0,
|
||||
"Max_Order_Over_Notional_Ratio": 1.05,
|
||||
@@ -13,11 +13,12 @@
|
||||
},
|
||||
"Logging": {
|
||||
"Log_Summary_Each_Loop": false,
|
||||
"Print_Summary_Each_Loop": true
|
||||
"Print_Summary_Each_Loop": false
|
||||
},
|
||||
"Overrides": {
|
||||
"Allow_Ordering_Aster": true,
|
||||
"Allow_Ordering_Extend": true,
|
||||
"Allow_Symbol_Change": false,
|
||||
"Flatten_Open_Positions": false,
|
||||
"Flip_Side_For_Testing": false
|
||||
}
|
||||
|
||||
@@ -4,11 +4,10 @@ import logging
|
||||
import os
|
||||
import traceback
|
||||
from datetime import datetime
|
||||
from typing import AsyncContextManager
|
||||
|
||||
import valkey
|
||||
from dotenv import load_dotenv
|
||||
# from sqlalchemy.ext.asyncio import create_async_engine
|
||||
import modules.utils as utils
|
||||
|
||||
'''
|
||||
TO DO:
|
||||
@@ -16,75 +15,62 @@ TO DO:
|
||||
'''
|
||||
|
||||
### Database ###
|
||||
CON: AsyncContextManager | None = None
|
||||
VAL_KEY = None
|
||||
VK_IN = 'fr_orchestrator_input'
|
||||
VK_OUT = 'fr_orchestrator_output'
|
||||
VK_IN: str = 'fr_orchestrator_input'
|
||||
VK_OUT: str = 'fr_orchestrator_output'
|
||||
|
||||
# CONFIG_FILEPATH: str = '/algo_local_drive/algo_config.json'
|
||||
CONFIG_FILEPATH: str = 'algo_config.json'
|
||||
|
||||
### Logging ###
|
||||
load_dotenv()
|
||||
LOG_FILEPATH: str = os.getenv("LOGS_PATH") + '/Fund_Rate_Algo_Orchestrator.log'
|
||||
LOG_FILEPATH: str = f'{os.getenv("LOGS_PATH")}/Fund_Rate_Algo_Orchestrator.log'
|
||||
|
||||
ALGO_CONFIG: None | dict
|
||||
|
||||
async def orchestrator() -> None:
|
||||
global ALGO_CONFIG
|
||||
async def main() -> None:
|
||||
VAL_KEY: valkey.Valkey = valkey.Valkey(host='localhost', port=6379, db=0, decode_responses=True)
|
||||
|
||||
try:
|
||||
VK_PUBSUB = VAL_KEY.pubsub()
|
||||
VK_PUBSUB: valkey.client.PubSub = VAL_KEY.pubsub()
|
||||
VK_PUBSUB.subscribe(VK_IN)
|
||||
|
||||
logging.info(f"Subscribed to '{VK_IN}'. Waiting for messages...")
|
||||
logging.info(msg=f"Subscribed to '{VK_IN}'. Waiting for messages...")
|
||||
|
||||
for message in VK_PUBSUB.listen():
|
||||
if message['type'] == 'message':
|
||||
timestamp = round(datetime.now().timestamp()*1000)
|
||||
data = json.loads(message['data'])
|
||||
# channel = message['channel']
|
||||
timestamp: int = round(number=datetime.now().timestamp()*1000)
|
||||
|
||||
with open('/algo_local_drive/algo_config.json', 'r', encoding='utf-8') as f:
|
||||
# ALGO_CONFIG = json.load(f, object_hook=lambda d: Algo_Config(**d))
|
||||
ALGO_CONFIG = json.load(f)
|
||||
ALGO_CONFIG['Updated_Timestamp'] = timestamp
|
||||
|
||||
for k, v in data.items():
|
||||
if ALGO_CONFIG.get(k, None) is not None:
|
||||
ALGO_CONFIG[k] = v
|
||||
# Receive Update Msg from PubSub
|
||||
data: dict = json.loads(s=message['data'])
|
||||
|
||||
VAL_KEY.set(VK_OUT, json.dumps(ALGO_CONFIG))
|
||||
with open('/algo_local_drive/algo_config.json', 'w', encoding='utf-8') as f:
|
||||
json.dump(ALGO_CONFIG, f, indent=4)
|
||||
logging.info(f"Algo Config Updated @ {timestamp}; {data}")
|
||||
# Load Config File
|
||||
with open(file=CONFIG_FILEPATH, mode='r', encoding='utf-8') as f:
|
||||
Algo_Config: dict = json.load(fp=f)
|
||||
Algo_Config['Updated_Timestamp'] = timestamp
|
||||
|
||||
|
||||
# Update Config w Update Data
|
||||
Algo_Config: dict = utils.rec_set_dict(orig_dict=Algo_Config, new_dict=data)
|
||||
# Set VK KV w Updated Config
|
||||
VAL_KEY.set(name=VK_OUT, value=json.dumps(obj=Algo_Config))
|
||||
|
||||
# Save Updated Config to File
|
||||
with open(file=CONFIG_FILEPATH, mode='w', encoding='utf-8') as f:
|
||||
json.dump(obj=Algo_Config, fp=f, indent=4)
|
||||
|
||||
logging.info(msg=f"Algo Config Updated @ {timestamp}; {data}")
|
||||
|
||||
except valkey.exceptions.ConnectionError as e:
|
||||
logging.info(f"Could not connect to Valkey. Please check the publish server is up; {e}")
|
||||
logging.info(msg=f"Could not connect to Valkey. Please check the publish server is up; {e}")
|
||||
except KeyboardInterrupt:
|
||||
logging.info('ORCHESTRATOR SHUTTING DOWN...')
|
||||
logging.info(msg='ORCHESTRATOR SHUTTING DOWN...')
|
||||
except Exception as e:
|
||||
logging.error(traceback.format_exc())
|
||||
logging.critical(f'*** ORCHESTRATOR CRASHED: {e}')
|
||||
logging.error(msg=traceback.format_exc())
|
||||
logging.critical(msg=f'*** ORCHESTRATOR CRASHED: {e}')
|
||||
|
||||
### MAIN STARTUP ###
|
||||
async def main() -> None:
|
||||
global VAL_KEY
|
||||
global CON
|
||||
global ALGO_CONFIG
|
||||
|
||||
VAL_KEY = valkey.Valkey(host='localhost', port=6379, db=0, decode_responses=True)
|
||||
# engine = create_async_engine('mysql+asyncmy://root:pwd@localhost/fund_rate')
|
||||
|
||||
with open('/algo_local_drive/algo_config.json', 'r', encoding='utf-8') as f:
|
||||
# ALGO_CONFIG = json.load(f, object_hook=lambda d: Algo_Config(**d))
|
||||
ALGO_CONFIG = json.load(f)
|
||||
ALGO_CONFIG['Updated_Timestamp'] = round(datetime.now().timestamp()*1000)
|
||||
|
||||
# async with engine.connect() as CON:
|
||||
await orchestrator()
|
||||
|
||||
if __name__ == '__main__':
|
||||
START_TIME = round(datetime.now().timestamp()*1000)
|
||||
START_TIME: int = round(number=datetime.now().timestamp()*1000)
|
||||
|
||||
logging.info(f'Log FilePath: {LOG_FILEPATH}')
|
||||
logging.info(msg=f'Log FilePath: {LOG_FILEPATH}')
|
||||
|
||||
logging.basicConfig(
|
||||
force=True,
|
||||
@@ -93,6 +79,6 @@ if __name__ == '__main__':
|
||||
format='%(asctime)s - %(levelname)s - %(message)s',
|
||||
filemode='w'
|
||||
)
|
||||
logging.info(f"STARTED: {START_TIME}")
|
||||
logging.info(msg=f"STARTED: {START_TIME}")
|
||||
|
||||
asyncio.run(main())
|
||||
26480
aster.ipynb
26480
aster.ipynb
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -4,53 +4,27 @@ import logging
|
||||
import os
|
||||
import time
|
||||
import traceback
|
||||
from dataclasses import dataclass, field
|
||||
from dataclasses import asdict
|
||||
from datetime import datetime
|
||||
from typing import AsyncContextManager
|
||||
|
||||
import modules.structs as structs
|
||||
import pandas as pd
|
||||
import requests
|
||||
import valkey
|
||||
from dotenv import load_dotenv
|
||||
|
||||
# from sqlalchemy.ext.asyncio import create_async_engine
|
||||
|
||||
### Structs ###
|
||||
@dataclass(kw_only=False)
|
||||
class Asset_Leverage:
|
||||
exchange: str
|
||||
lh_asset: str
|
||||
rh_asset: str
|
||||
max_leverage: int
|
||||
max_notional: float
|
||||
# max_leverage_notional: list = field(default_factory=list)
|
||||
import modules.manual_leverage as leverage
|
||||
|
||||
### MANUAL LEVERAGE DATA ###
|
||||
LEVERAGE_BY_EXCH: list[Asset_Leverage] = [
|
||||
Asset_Leverage('ASTER', 'BTC' , 'USDT', 150, 300_000), Asset_Leverage('EXTEND', 'BTC' , 'USD', 50, 4_000_000),
|
||||
Asset_Leverage('ASTER', 'ETH' , 'USDT', 150, 300_000), Asset_Leverage('EXTEND', 'ETH' , 'USD', 50, 4_000_000),
|
||||
Asset_Leverage('ASTER', 'LIT' , 'USDT', 50 , 2_500 ), Asset_Leverage('EXTEND', 'LIT' , 'USD', 25, 400_000 ),
|
||||
Asset_Leverage('ASTER', 'CHIP' , 'USDT', 50 , 5_000 ), Asset_Leverage('EXTEND', 'CHIP' , 'USD', 5 , 100_000 ),
|
||||
Asset_Leverage('ASTER', 'XAG' , 'USDT', 100, 50_000 ), Asset_Leverage('EXTEND', 'XAG' , 'USD', 10, 1_000_000),
|
||||
Asset_Leverage('ASTER', '4' , 'USDT', 50 , 5_000 ), Asset_Leverage('EXTEND', '4' , 'USD', 5 , 100_000 ),
|
||||
Asset_Leverage('ASTER', 'XPT' , 'USDT', 3 , 30_000 ), Asset_Leverage('EXTEND', 'XPT' , 'USD', 5 , 1_000_000),
|
||||
Asset_Leverage('ASTER', 'XMR' , 'USDT', 50 , 10_000 ), Asset_Leverage('EXTEND', 'XMR' , 'USD', 25, 400_000 ),
|
||||
Asset_Leverage('ASTER', 'WLFI' , 'USDT', 25 , 104_869), Asset_Leverage('EXTEND', 'WLFI' , 'USD', 10, 250_000 ),
|
||||
Asset_Leverage('ASTER', 'TRUMP', 'USDT', 50 , 5_567 ), Asset_Leverage('EXTEND', 'TRUMP', 'USD', 25, 400_000 ),
|
||||
Asset_Leverage('ASTER', 'INIT' , 'USDT', 50 , 5_000 ), Asset_Leverage('EXTEND', 'INIT' , 'USD', 5 , 100_000 ),
|
||||
Asset_Leverage('ASTER', 'ZORA' , 'USDT', 5 , 100_000), Asset_Leverage('EXTEND', 'ZORA' , 'USD', 5 , 100_000 ),
|
||||
Asset_Leverage('ASTER', 'ZEC' , 'USDT', 75 , 6_250 ), Asset_Leverage('EXTEND', 'ZEC' , 'USD', 10, 250_000 ),
|
||||
]
|
||||
df_leverage_by_exch = pd.DataFrame(LEVERAGE_BY_EXCH)
|
||||
df_leverage_by_exch = pd.DataFrame(data=leverage.LEVERAGE_BY_EXCH)
|
||||
|
||||
### Database ###
|
||||
# CON: AsyncContextManager | None = None
|
||||
VAL_KEY = None
|
||||
VK_OUT = 'fr_engine_best_fund_rate_output'
|
||||
VAL_KEY: valkey.Valkey
|
||||
VK_OUT: str = 'fr_engine_best_fund_rate_output'
|
||||
|
||||
### Logging ###
|
||||
load_dotenv()
|
||||
LOG_FILEPATH: str = os.getenv("LOGS_PATH") + '/Fund_Rate_Engine_BFR.log'
|
||||
LOG_FILEPATH: str = f'{os.getenv(key="LOGS_PATH")}/Fund_Rate_Engine_BFR.log'
|
||||
|
||||
### CONSTANTS ###
|
||||
LOOP_SLEEP_SEC: int = 5
|
||||
@@ -65,9 +39,9 @@ Mkt_Volume_Last_Refresh_TS_ms: int
|
||||
def get_extended_markets_info() -> pd.DataFrame:
|
||||
global Mkt_Info_Last_Refresh_TS_ms
|
||||
|
||||
r = json.loads(requests.get('https://api.starknet.extended.exchange/api/v1/info/markets').text)
|
||||
r: dict = json.loads(s=requests.get(url='https://api.starknet.extended.exchange/api/v1/info/markets').text)
|
||||
|
||||
df = pd.DataFrame(r['data'])
|
||||
df: pd.DataFrame = pd.DataFrame(data=r['data'])
|
||||
df['funding_rate'] = df['marketStats'].apply(lambda x: x.get('fundingRate',{}))
|
||||
df['funding_rate_ts'] = df['marketStats'].apply(lambda x: x.get('nextFundingRate',{}))
|
||||
df['min_order_size'] = df['tradingConfig'].apply(lambda x: x.get('minOrderSize',{}))
|
||||
@@ -81,9 +55,9 @@ def get_extended_markets_info() -> pd.DataFrame:
|
||||
return df
|
||||
|
||||
def load_aster_current_fr() -> pd.DataFrame:
|
||||
df = pd.DataFrame(json.loads(VAL_KEY.get('fund_rate_aster_all')))
|
||||
df = pd.DataFrame(data=json.loads(s=VAL_KEY.get(name='fund_rate_aster_all'))) # ty:ignore[invalid-argument-type]
|
||||
|
||||
df = df[['s','E','r','T']].rename({'s':'symbol','E':'funding_rate_updated_ts_ms','r':'funding_rate','T':'next_funding_ts'}, axis=1)
|
||||
df: pd.DataFrame = df[['s','E','r','T']].rename({'s':'symbol','E':'funding_rate_updated_ts_ms','r':'funding_rate','T':'next_funding_ts'}, axis=1)
|
||||
df['funding_rate_updated_dt'] = pd.to_datetime(df['funding_rate_updated_ts_ms'], unit='ms')
|
||||
df['funding_rate'] = df['funding_rate'].astype(float)
|
||||
df['time_delta_to_next_funding'] = pd.to_datetime(df['next_funding_ts'], unit='ms') - pd.Timestamp.now()
|
||||
@@ -91,17 +65,17 @@ def load_aster_current_fr() -> pd.DataFrame:
|
||||
return df
|
||||
|
||||
def load_extend_current_fr(df_mkt_stats: pd.DataFrame) -> pd.DataFrame:
|
||||
df = pd.DataFrame(json.loads(VAL_KEY.get('fund_rate_extended_all')))
|
||||
df = pd.DataFrame(data=json.loads(s=VAL_KEY.get(name='fund_rate_extended_all'))) # ty:ignore[invalid-argument-type]
|
||||
|
||||
df = df[['symbol','funding_rate_updated_ts_ms','funding_rate']]
|
||||
df: pd.DataFrame = df[['symbol','funding_rate_updated_ts_ms','funding_rate']]
|
||||
df['funding_rate_updated_dt'] = pd.to_datetime(df['funding_rate_updated_ts_ms'], unit='ms')
|
||||
df['funding_rate'] = df['funding_rate'].astype(float)
|
||||
|
||||
df = df.merge(df_mkt_stats[['name','assetName','status', 'funding_rate_ts','max_leverage']].rename({'name':'symbol','funding_rate_ts':'next_funding_ts'}, axis=1), on='symbol', how='left')
|
||||
df = df.loc[df['status']=='ACTIVE',:]
|
||||
df: pd.DataFrame = df.merge(df_mkt_stats[['name','assetName','status', 'funding_rate_ts']].rename({'name':'symbol','funding_rate_ts':'next_funding_ts'}, axis=1), on='symbol', how='left')
|
||||
df: pd.DataFrame = df.loc[df['status']=='ACTIVE',:]
|
||||
df['USDT_Symbol'] = df['assetName'] + 'USDT'
|
||||
|
||||
df['time_delta_to_next_funding'] = pd.to_datetime(df['next_funding_ts'], unit='ms') - pd.Timestamp.now()
|
||||
df['time_delta_to_next_funding'] = pd.to_datetime(arg=df['next_funding_ts'], unit='ms') - pd.Timestamp.now()
|
||||
|
||||
return df
|
||||
|
||||
@@ -124,25 +98,40 @@ async def loop() -> None:
|
||||
df_comb_fr['net_funding_rate_abs'] = df_comb_fr['net_funding_rate'].abs()
|
||||
|
||||
### NET MULT ###
|
||||
df_comb_fr = df_comb_fr.merge(df_leverage_by_exch.loc[df_leverage_by_exch['exchange']=='EXTEND'], left_on='assetName', right_on='lh_asset').merge(df_leverage_by_exch.loc[df_leverage_by_exch['exchange']=='ASTER'], left_on='assetName', right_on='lh_asset', suffixes=('_ext', '_ast'))
|
||||
df_comb_fr = df_comb_fr.merge(right=df_leverage_by_exch.loc[df_leverage_by_exch['exchange']=='EXTEND'], left_on='assetName', right_on='lh_asset').merge(df_leverage_by_exch.loc[df_leverage_by_exch['exchange']=='ASTER'], left_on='assetName', right_on='lh_asset', suffixes=('_ext', '_ast'))
|
||||
df_comb_fr['net_mult'] = 1 / ( ( 0.5 / df_comb_fr['max_leverage_ext'] ) + ( 0.5 / df_comb_fr['max_leverage_ast'] ) )
|
||||
df_comb_fr['net_mult'] = df_comb_fr['net_mult'].round(2)
|
||||
df_comb_fr['net_mult_x_net_fr_abs'] = df_comb_fr['net_funding_rate_abs'] * df_comb_fr['net_mult']
|
||||
|
||||
df_best_fr_rate = df_comb_fr[['symbol_ext','symbol_ast','net_mult_x_net_fr_abs','net_funding_rate_abs','net_funding_rate','next_funding_at_same_time']].sort_values(by='net_mult_x_net_fr_abs', ascending=False).reset_index(drop=True)
|
||||
best_next_funding_pair = {'symbol_aster':df_best_fr_rate['symbol_ast'][0],'symbol_extended':df_best_fr_rate['symbol_ext'][0]}
|
||||
df_best_fr_rate: pd.DataFrame = df_comb_fr[['symbol_ext','symbol_ast','max_leverage_ext','max_leverage_ast','lh_asset_ext','lh_asset_ast','rh_asset_ext','rh_asset_ast','net_mult_x_net_fr_abs','net_funding_rate_abs','net_funding_rate','next_funding_at_same_time']].sort_values(by='net_mult_x_net_fr_abs', ascending=False).reset_index(drop=True)
|
||||
|
||||
ASTER = structs.Perpetual_Exchange(
|
||||
mult = int(df_best_fr_rate['max_leverage_ast'][0]),
|
||||
lh_asset = df_best_fr_rate['lh_asset_ast'][0],
|
||||
rh_asset = df_best_fr_rate['rh_asset_ast'][0],
|
||||
symbol_asset_separator = '',
|
||||
)
|
||||
|
||||
EXTEND = structs.Perpetual_Exchange(
|
||||
mult = int(df_best_fr_rate['max_leverage_ext'][0]),
|
||||
lh_asset = df_best_fr_rate['lh_asset_ext'][0],
|
||||
rh_asset = df_best_fr_rate['rh_asset_ext'][0],
|
||||
symbol_asset_separator = '-',
|
||||
)
|
||||
|
||||
best_next_funding_pair: dict[str, dict] = {'ASTER': asdict(obj=ASTER), 'EXTEND': asdict(obj=EXTEND)}
|
||||
|
||||
VAL_KEY.set(VK_OUT, json.dumps(best_next_funding_pair))
|
||||
print(best_next_funding_pair)
|
||||
VAL_KEY.set(name=VK_OUT, value=json.dumps(obj=best_next_funding_pair))
|
||||
# print(best_next_funding_pair)
|
||||
time.sleep(LOOP_SLEEP_SEC)
|
||||
continue
|
||||
except valkey.exceptions.ConnectionError as e:
|
||||
logging.info(f"Could not connect to Valkey. Please check the publish server is up; {e}")
|
||||
except KeyboardInterrupt:
|
||||
logging.info('ORCHESTRATOR SHUTTING DOWN...')
|
||||
logging.info('SHUTTING DOWN...')
|
||||
except Exception as e:
|
||||
logging.error(traceback.format_exc())
|
||||
logging.critical(f'*** ORCHESTRATOR CRASHED: {e}')
|
||||
logging.critical(f'*** CRASHED: {e}')
|
||||
|
||||
|
||||
### STARTUP ###
|
||||
@@ -156,9 +145,9 @@ async def main() -> None:
|
||||
await loop()
|
||||
|
||||
if __name__ == '__main__':
|
||||
START_TIME = round(datetime.now().timestamp()*1000)
|
||||
START_TIME = round(number=datetime.now().timestamp()*1000)
|
||||
|
||||
logging.info(f'Log FilePath: {LOG_FILEPATH}')
|
||||
logging.info(msg=f'Log FilePath: {LOG_FILEPATH}')
|
||||
|
||||
logging.basicConfig(
|
||||
force=True,
|
||||
@@ -167,6 +156,6 @@ if __name__ == '__main__':
|
||||
format='%(asctime)s - %(levelname)s - %(message)s',
|
||||
filemode='w'
|
||||
)
|
||||
logging.info(f"STARTED: {START_TIME}")
|
||||
logging.info(msg=f"STARTED: {START_TIME}")
|
||||
|
||||
asyncio.run(main())
|
||||
119
engine_orders.py
Normal file
119
engine_orders.py
Normal 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())
|
||||
@@ -2,7 +2,7 @@
|
||||
"cells": [
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 9,
|
||||
"execution_count": 2,
|
||||
"id": "6c70a8c3",
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
@@ -27,7 +27,7 @@
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 12,
|
||||
"execution_count": 3,
|
||||
"id": "ff971ca9",
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
@@ -49,19 +49,10 @@
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 13,
|
||||
"execution_count": 4,
|
||||
"id": "fc2c6d2b",
|
||||
"metadata": {},
|
||||
"outputs": [
|
||||
{
|
||||
"name": "stderr",
|
||||
"output_type": "stream",
|
||||
"text": [
|
||||
"Unclosed client session\n",
|
||||
"client_session: <aiohttp.client.ClientSession object at 0x7b2dc26ba850>\n"
|
||||
]
|
||||
}
|
||||
],
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"client, trading_client = await extend_auth.create_auth_account_and_trading_client()"
|
||||
]
|
||||
@@ -146,7 +137,7 @@
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 6,
|
||||
"execution_count": 5,
|
||||
"id": "8dd8aa73",
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
@@ -156,23 +147,44 @@
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 8,
|
||||
"execution_count": 7,
|
||||
"id": "50bb753e",
|
||||
"metadata": {},
|
||||
"outputs": [
|
||||
{
|
||||
"data": {
|
||||
"text/plain": [
|
||||
"x10.utils.http.WrappedApiResponse[List[PositionModel]]"
|
||||
]
|
||||
},
|
||||
"execution_count": 7,
|
||||
"metadata": {},
|
||||
"output_type": "execute_result"
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"type(d)"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 10,
|
||||
"id": "ade14392",
|
||||
"metadata": {},
|
||||
"outputs": [
|
||||
{
|
||||
"data": {
|
||||
"text/plain": [
|
||||
"{'status': 'OK', 'data': [], 'error': None, 'pagination': None}"
|
||||
"list"
|
||||
]
|
||||
},
|
||||
"execution_count": 8,
|
||||
"execution_count": 10,
|
||||
"metadata": {},
|
||||
"output_type": "execute_result"
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"dict(d)"
|
||||
"type(dict(d).get('data'))"
|
||||
]
|
||||
},
|
||||
{
|
||||
|
||||
136
main.py
136
main.py
@@ -1,3 +1,5 @@
|
||||
from x10.utils.http import WrappedApiResponse
|
||||
from x10.perpetual.trading_client.trading_client import PerpetualTradingClient
|
||||
import asyncio
|
||||
import json
|
||||
import logging
|
||||
@@ -8,7 +10,7 @@ import traceback
|
||||
from datetime import datetime, timezone
|
||||
from decimal import ROUND_DOWN, Decimal
|
||||
from typing import AsyncContextManager
|
||||
|
||||
from dataclasses import dataclass, asdict
|
||||
from typing import Any
|
||||
import numpy as np
|
||||
import pandas as pd
|
||||
@@ -19,24 +21,24 @@ import valkey
|
||||
from dotenv import load_dotenv
|
||||
from sqlalchemy import text
|
||||
from sqlalchemy.ext.asyncio import create_async_engine
|
||||
from x10.models.order import OrderSide
|
||||
from x10.models.order import OrderSide, PlacedOrderModel
|
||||
|
||||
import modules.utils as utils
|
||||
import modules.aster_auth as aster_auth
|
||||
import modules.extended_auth as extend_auth
|
||||
import modules.structs as structs
|
||||
|
||||
### Database ###
|
||||
EXTEND_CLIENT = None
|
||||
CON: AsyncContextManager | None = None
|
||||
VAL_KEY = None
|
||||
### Clients ###
|
||||
EXTEND_CLIENT: PerpetualTradingClient
|
||||
CON: AsyncContextManager
|
||||
VAL_KEY: valkey.Valkey
|
||||
|
||||
### Logging ###
|
||||
load_dotenv()
|
||||
LOG_FILEPATH: str = os.getenv("LOGS_PATH") + '/Fund_Rate_Algo.log'
|
||||
LOG_FILEPATH: str = f'{os.getenv(key="LOGS_PATH")}/Fund_Rate_Algo.log'
|
||||
|
||||
### Algo Config ###
|
||||
ALGO_CONFIG: structs.Algo_Config | None = None
|
||||
ALGO_CONFIG: structs.Algo_Config
|
||||
MIN_TIME_TO_FUNDING: int
|
||||
|
||||
### EXCHANGES ###
|
||||
@@ -56,7 +58,6 @@ EXTEND = structs.Perpetual_Exchange(
|
||||
### GLOBALS ###
|
||||
Last_Aster_Fill_Time_Ts: float = 0.00
|
||||
Just_Rejected_Or_Expired: bool = False
|
||||
Best_Symbol_by_Exchange: dict = {}
|
||||
|
||||
# ASTER_MULT = 150
|
||||
# EXTEND_MULT = 50
|
||||
@@ -122,7 +123,7 @@ async def get_aster_collateral():
|
||||
r = await aster_auth.post_authenticated_url(fut_acct_balances)
|
||||
ASTER_AVAIL_COLLATERAL = float([d for d in r if d.get('asset')==ASTER.rh_asset][0].get('availableBalance'))
|
||||
|
||||
async def get_aster_notional_position(resp: dict | None = None):
|
||||
async def get_aster_notional_position(resp: list | None = None):
|
||||
global ASTER_NOTIONAL_OBJ
|
||||
global ASTER_NOTIONAL_POSITION
|
||||
global ASTER_UNREALIZED_PNL
|
||||
@@ -131,14 +132,14 @@ async def get_aster_notional_position(resp: dict | None = None):
|
||||
previous_notional_obj = ASTER_NOTIONAL_OBJ
|
||||
|
||||
if not resp:
|
||||
fut_acct_positionRisk = {
|
||||
fut_acct_positionRisk: dict = {
|
||||
"url": "/fapi/v3/positionRisk",
|
||||
"method": "GET",
|
||||
"params": {
|
||||
'symbol': ASTER.symbol,
|
||||
}
|
||||
}
|
||||
resp = await aster_auth.post_authenticated_url(fut_acct_positionRisk)
|
||||
resp: list = await aster_auth.post_authenticated_url(req=fut_acct_positionRisk) # ty:ignore[invalid-assignment]
|
||||
d = [x for x in resp if x.get('symbol', None) == ASTER.symbol][0]
|
||||
d['timestamp_arrival'] = round(datetime.now().timestamp()*1000)
|
||||
else:
|
||||
@@ -168,8 +169,8 @@ async def get_aster_notional_position(resp: dict | None = None):
|
||||
if abs(ASTER_NOTIONAL_POSITION) > ALGO_CONFIG.Config.Max_Target_Notional*ALGO_CONFIG.Config.Max_Order_Over_Notional_Ratio:
|
||||
logging.info(f'BAD NOTIONAL - ASTER CHANGE: {previous_notional_position} -> {ASTER_NOTIONAL_POSITION}; UR PNL: {ASTER_UNREALIZED_PNL}; MULT: {ASTER.mult}; d: {d}; resp: {resp}')
|
||||
await kill_algo()
|
||||
# if ASTER_NOTIONAL_POSITION != previous_notional_position:
|
||||
logging.info(f'ASTER NOTIONAL CHANGE: {previous_notional_position:.2f} -> {ASTER_NOTIONAL_POSITION:.2f}; UR PNL: {ASTER_UNREALIZED_PNL:.2f}; MULT: {ASTER.mult:.0f}; resp: {bool(resp)}')
|
||||
if ASTER_NOTIONAL_POSITION != previous_notional_position:
|
||||
logging.info(f'ASTER NOTIONAL CHANGE: {previous_notional_position:.2f} -> {ASTER_NOTIONAL_POSITION:.2f}; UR PNL: {ASTER_UNREALIZED_PNL:.2f}; MULT: {ASTER.mult:.0f}; resp: {bool(resp)}')
|
||||
|
||||
async def get_extend_collateral():
|
||||
global EXTEND_AVAIL_COLLATERAL
|
||||
@@ -177,7 +178,7 @@ async def get_extend_collateral():
|
||||
get_bals = dict(dict(await EXTEND_CLIENT.account.get_balance()).get('data', {}))
|
||||
EXTEND_AVAIL_COLLATERAL = get_bals.get('available_for_trade', 0) if get_bals.get('collateral_name', None)==EXTEND.rh_asset else 0
|
||||
|
||||
async def get_extend_notional(resp: dict | None = None):
|
||||
async def get_extend_notional(resp: list | None = None):
|
||||
global EXTEND_NOTIONAL_OBJ
|
||||
global EXTEND_NOTIONAL_POSITION
|
||||
global EXTEND_UNREALIZED_PNL
|
||||
@@ -238,15 +239,15 @@ async def get_extend_notional(resp: dict | None = None):
|
||||
async def get_aster_exch_info():
|
||||
global ASTER_MIN_ORDER_QTY
|
||||
|
||||
fut_acct_exchangeInfo = {
|
||||
fut_acct_exchangeInfo: dict = {
|
||||
"url": "/fapi/v3/exchangeInfo",
|
||||
"method": "GET",
|
||||
"params": {}
|
||||
}
|
||||
r = await aster_auth.post_authenticated_url(fut_acct_exchangeInfo)
|
||||
s = r['symbols']
|
||||
d = [d for d in s if d.get('symbol', None) == 'ETHUSDT'][0]
|
||||
f = [f for f in d['filters'] if f.get('filterType', None) == 'LOT_SIZE'][0]
|
||||
r: dict = await aster_auth.post_authenticated_url(fut_acct_exchangeInfo) # ty:ignore[invalid-assignment]
|
||||
s: list = r['symbols']
|
||||
d: dict = [d for d in s if d.get('symbol', None) == 'ETHUSDT'][0]
|
||||
f: dict = [f for f in d['filters'] if f.get('filterType', None) == 'LOT_SIZE'][0]
|
||||
ASTER_MIN_ORDER_QTY = float(f['minQty'])
|
||||
|
||||
async def get_extend_exch_info():
|
||||
@@ -290,7 +291,7 @@ async def run_algo():
|
||||
global EXTEND_OPEN_ORDERS
|
||||
global Last_Aster_Fill_Time_Ts
|
||||
global Just_Rejected_Or_Expired
|
||||
global Best_Symbol_by_Exchange
|
||||
# global Best_Symbol_by_Exchange
|
||||
|
||||
try:
|
||||
while True:
|
||||
@@ -298,20 +299,22 @@ async def run_algo():
|
||||
# print('__________Start___________')
|
||||
### ALGO CONIFG ###
|
||||
|
||||
ALGO_CONFIG = json.loads(VAL_KEY.get('fr_orchestrator_output'))
|
||||
ALGO_CONFIG = json.loads(VAL_KEY.get('fr_orchestrator_output')) # ty:ignore[invalid-argument-type]
|
||||
ALGO_CONFIG = structs.Algo_Config(**ALGO_CONFIG)
|
||||
ALGO_CONFIG.Config.Max_Target_Notional = float(min([ASTER.mult, EXTEND.mult]) * ALGO_CONFIG.Config.Target_Open_Cash_Position)
|
||||
|
||||
MIN_TIME_TO_FUNDING = ALGO_CONFIG.Config.Min_Time_To_Funding_Minutes * 60 * 1000
|
||||
|
||||
### Load Data from Feedhandlers ###
|
||||
Best_Symbol_by_Exchange = json.loads(VAL_KEY.get('fr_engine_best_fund_rate_output'))
|
||||
best_symbol_by_exchange: dict = json.loads(s=VAL_KEY.get(name='fr_engine_best_fund_rate_output')) # ty:ignore[invalid-argument-type]
|
||||
best_symbol_by_exchange_aster = structs.Perpetual_Exchange(**best_symbol_by_exchange['ASTER'])
|
||||
best_symbol_by_exchange_extend = structs.Perpetual_Exchange(**best_symbol_by_exchange['EXTEND'])
|
||||
|
||||
ASTER_FUND_RATE_DICT = json.loads(VAL_KEY.get('fund_rate_aster'))
|
||||
EXTENDED_FUND_RATE_DICT = json.loads(VAL_KEY.get('fund_rate_extended'))
|
||||
ASTER_FUND_RATE_DICT = json.loads(VAL_KEY.get('fund_rate_aster')) # ty:ignore[invalid-argument-type]
|
||||
EXTENDED_FUND_RATE_DICT = json.loads(VAL_KEY.get('fund_rate_extended')) # ty:ignore[invalid-argument-type]
|
||||
|
||||
ASTER_FUND_RATE = float(ASTER_FUND_RATE_DICT.get('funding_rate', 0))
|
||||
EXTEND_FUND_RATE = float(EXTENDED_FUND_RATE_DICT.get('funding_rate', 0))
|
||||
ASTER_FUND_RATE: float = float(ASTER_FUND_RATE_DICT.get('funding_rate', 0))
|
||||
EXTEND_FUND_RATE: float = float(EXTENDED_FUND_RATE_DICT.get('funding_rate', 0))
|
||||
|
||||
if ALGO_CONFIG.Overrides.Flip_Side_For_Testing:
|
||||
ASTER_FUND_RATE = ASTER_FUND_RATE * -1
|
||||
@@ -321,26 +324,26 @@ async def run_algo():
|
||||
EXTEND_FUND_RATE_TIME = float(EXTENDED_FUND_RATE_DICT.get('next_funding_time_ts_ms', 0))
|
||||
EXTEND_FUND_RATE_TIME = max([EXTEND_FUND_RATE_TIME, 0])
|
||||
|
||||
ASTER_TICKER_DICT = json.loads(VAL_KEY.get('fut_ticker_aster'))
|
||||
EXTENDED_TICKER_DICT = json.loads(VAL_KEY.get('fut_ticker_extended'))
|
||||
ASTER_TICKER_DICT = json.loads(VAL_KEY.get('fut_ticker_aster')) # ty:ignore[invalid-argument-type]
|
||||
EXTENDED_TICKER_DICT = json.loads(VAL_KEY.get('fut_ticker_extended')) # ty:ignore[invalid-argument-type]
|
||||
|
||||
### Manage Local Collateral Using Updates from WS ###
|
||||
ASTER_WS_COLLATERAL_UPDATES = VAL_KEY.get('fr_aster_user_positions')
|
||||
ASTER_WS_COLLATERAL_UPDATES = json.loads(ASTER_WS_COLLATERAL_UPDATES) if ASTER_WS_COLLATERAL_UPDATES is not None else []
|
||||
ASTER_WS_COLLATERAL_UPDATES = json.loads(ASTER_WS_COLLATERAL_UPDATES) if ASTER_WS_COLLATERAL_UPDATES is not None else [] # ty:ignore[invalid-argument-type]
|
||||
EXTEND_WS_COLLATERAL_UPDATES = VAL_KEY.get('fr_extended_user_positions')
|
||||
EXTEND_WS_COLLATERAL_UPDATES = json.loads(EXTEND_WS_COLLATERAL_UPDATES) if EXTEND_WS_COLLATERAL_UPDATES is not None else []
|
||||
EXTEND_WS_COLLATERAL_UPDATES = json.loads(EXTEND_WS_COLLATERAL_UPDATES) if EXTEND_WS_COLLATERAL_UPDATES is not None else [] # ty:ignore[invalid-argument-type]
|
||||
|
||||
### Manage Local Notionals Using Updates from WS ###
|
||||
ASTER_WS_POS_UPDATES = VAL_KEY.get('fr_aster_user_positions')
|
||||
ASTER_WS_POS_UPDATES = json.loads(ASTER_WS_POS_UPDATES) if ASTER_WS_POS_UPDATES is not None else []
|
||||
EXTEND_WS_POS_UPDATES = VAL_KEY.get('fr_extended_user_positions')
|
||||
EXTEND_WS_POS_UPDATES = json.loads(EXTEND_WS_POS_UPDATES) if EXTEND_WS_POS_UPDATES is not None else []
|
||||
ASTER_WS_POS_UPDATES: Any = VAL_KEY.get(name='fr_aster_user_positions')
|
||||
ASTER_WS_POS_UPDATES: list = json.loads(s=ASTER_WS_POS_UPDATES) if ASTER_WS_POS_UPDATES is not None else []
|
||||
EXTEND_WS_POS_UPDATES: Any = VAL_KEY.get('fr_extended_user_positions')
|
||||
EXTEND_WS_POS_UPDATES: list = json.loads(EXTEND_WS_POS_UPDATES) if EXTEND_WS_POS_UPDATES is not None else []
|
||||
|
||||
### Manage Local Orders Using Updates from WS ###
|
||||
ASTER_WS_ORDER_UPDATES = VAL_KEY.get('fr_aster_user_orders')
|
||||
ASTER_WS_ORDER_UPDATES = json.loads(ASTER_WS_ORDER_UPDATES) if ASTER_WS_ORDER_UPDATES is not None else []
|
||||
EXTEND_WS_ORDER_UPDATES = VAL_KEY.get('fr_extended_user_orders')
|
||||
EXTEND_WS_ORDER_UPDATES = json.loads(EXTEND_WS_ORDER_UPDATES) if EXTEND_WS_ORDER_UPDATES is not None else []
|
||||
ASTER_WS_ORDER_UPDATES: Any = VAL_KEY.get('fr_aster_user_orders')
|
||||
ASTER_WS_ORDER_UPDATES: list = json.loads(ASTER_WS_ORDER_UPDATES) if ASTER_WS_ORDER_UPDATES is not None else []
|
||||
EXTEND_WS_ORDER_UPDATES: Any = VAL_KEY.get('fr_extended_user_orders')
|
||||
EXTEND_WS_ORDER_UPDATES: list = json.loads(EXTEND_WS_ORDER_UPDATES) if EXTEND_WS_ORDER_UPDATES is not None else []
|
||||
|
||||
# CHECK NO MORE THAN 1 OPEN ORDER ON EITHER EXCHANGE #
|
||||
if len(ASTER_OPEN_ORDERS) > 1 or len(EXTEND_OPEN_ORDERS) > 1:
|
||||
@@ -409,9 +412,9 @@ async def run_algo():
|
||||
order_update = [dict(ou) for ou in EXTEND_WS_ORDER_UPDATES if dict(ou).get('order_id', None) == order_id]
|
||||
|
||||
if len(order_update) > 0:
|
||||
order_update = order_update[0]
|
||||
order_update_status = order_update.get('status')
|
||||
order_status_changed = order_orig_status.upper() != order_update_status.upper()
|
||||
order_update: dict = order_update[0]
|
||||
order_update_status: str = order_update['status']
|
||||
order_status_changed: bool = order_orig_status.upper() != order_update_status.upper()
|
||||
|
||||
if order_status_changed:
|
||||
logging.info(f'EXTEND ORDER ({order_id}): {order_orig_status} -> {order_update_status}')
|
||||
@@ -436,14 +439,18 @@ async def run_algo():
|
||||
logging.critical(f'EXTEND ORDER STATUS CHG TO UNEXPECTED VALUE, KILLING... ({order_id}): {order_orig_status} -> {order_update_status}')
|
||||
|
||||
|
||||
# if Best_Symbol_by_Exchange['symbol_aster'] != ASTER.symbol:
|
||||
# if abs( ASTER_NOTIONAL_POSITION ) > 0 or abs( EXTEND_NOTIONAL_POSITION ) > 0:
|
||||
# if (best_symbol_by_exchange_aster.symbol != ASTER.symbol) or (best_symbol_by_exchange_extend.symbol != EXTEND.symbol):
|
||||
# if abs( ASTER_NOTIONAL_POSITION ) > 0.00 or abs( EXTEND_NOTIONAL_POSITION ) > 0.00:
|
||||
# print('Symbol switch - Flattening Positions')
|
||||
# ALGO_CONFIG.Overrides.Flatten_Open_Positions = True
|
||||
# else:
|
||||
# print(f'Balances Flattened - Updating to Trade New Symbol: ASTER.symbol -> {Best_Symbol_by_Exchange['symbol_aster']}')
|
||||
# # ASTER.symbol = Best_Symbol_by_Exchange['symbol_aster']
|
||||
# # EXTEND.symbol = Best_Symbol_by_Exchange['symbol_extended']
|
||||
# print('Balances Flattened - Updating to Trade New Symbols:')
|
||||
# print(f' ASTER.symbol -> {best_symbol_by_exchange_aster.symbol}')
|
||||
# print(f' EXTEND.symbol -> {best_symbol_by_exchange_extend.symbol}')
|
||||
# ALGO_CONFIG.Overrides.Flatten_Open_Positions = False
|
||||
# ASTER = best_symbol_by_exchange_aster
|
||||
# EXTEND = best_symbol_by_exchange_extend
|
||||
# VAL_KEY.set(name='fr_algo_working_symbol', value=json.dumps(obj={'ASTER': asdict(obj=ASTER), 'EXTEND': asdict(obj=EXTEND)}))
|
||||
|
||||
|
||||
|
||||
@@ -542,7 +549,7 @@ async def run_algo():
|
||||
Currently_Hedged = Hedge_Ratio < 1.00
|
||||
|
||||
def print_summary(use_logging: bool = False):
|
||||
OUT: print | logging.info = logging.info if use_logging else print
|
||||
OUT: Any = logging.info if use_logging else print
|
||||
|
||||
OUT(f'''
|
||||
LOOP SLEEP (SEC): {ALGO_CONFIG.Config.Loop_Sleep_Sec}
|
||||
@@ -566,6 +573,7 @@ async def run_algo():
|
||||
FEES : TAKER: {0.0002:.2%}; Expected Alpha w Taker = {Expected_Alpha_Net_FR-0.0002:.6f} [w/o FR: {Expected_Alpha_w_Taker:.6f}]
|
||||
HEDGE: {Hedge_Ratio:.2f}% <= {1:.2f}%: {Currently_Hedged} [{EXTEND_NOTIONAL_POSITION:.2f} / {ASTER_NOTIONAL_POSITION:.2f}]
|
||||
|
||||
MKT : Aster: {ASTER.symbol} (best: {best_symbol_by_exchange_aster.symbol}) | Extend: {ASTER.symbol} (best: {best_symbol_by_exchange_extend.symbol})
|
||||
|
||||
--- ASTER OPEN ORDERS ---
|
||||
{ASTER_OPEN_ORDERS}
|
||||
@@ -583,8 +591,8 @@ async def run_algo():
|
||||
|
||||
|
||||
### ROUTES ###
|
||||
# MIN_EXPECTED_ALPHA_TO_TRADE = 0.0001
|
||||
MIN_EXPECTED_ALPHA_TO_TRADE = -0.000001
|
||||
MIN_EXPECTED_ALPHA_TO_TRADE = 0.0001
|
||||
# MIN_EXPECTED_ALPHA_TO_TRADE = -0.000001
|
||||
# ALPHA RATIO CHECK
|
||||
if not( ( Expected_Alpha_Net_FR_w_Taker > MIN_EXPECTED_ALPHA_TO_TRADE ) or ( ASTER_OPEN_ORDERS or EXTEND_OPEN_ORDERS or Just_Rejected_Or_Expired or ALGO_CONFIG.Overrides.Flatten_Open_Positions) ):
|
||||
if ALGO_CONFIG.Logging.Print_Summary_Each_Loop:
|
||||
@@ -614,7 +622,7 @@ async def run_algo():
|
||||
logging.info('ASTER OPEN ORDER NO PX CHG; SKIPPING')
|
||||
place_order = False
|
||||
else:
|
||||
cancel_order = {
|
||||
cancel_order: dict = {
|
||||
"url": "/fapi/v3/order",
|
||||
"method": "DELETE",
|
||||
"params": {
|
||||
@@ -622,7 +630,7 @@ async def run_algo():
|
||||
'orderId': open_order_id,
|
||||
}
|
||||
}
|
||||
cr = await aster_auth.post_authenticated_url(cancel_order)
|
||||
cr: dict = await aster_auth.post_authenticated_url(cancel_order) # ty:ignore[invalid-assignment]
|
||||
if cr.get('status', None) == 'CANCELED':
|
||||
ASTER_OPEN_ORDERS.pop(0)
|
||||
place_order = True
|
||||
@@ -637,7 +645,7 @@ async def run_algo():
|
||||
logging.info('ASTER TRYNG TO ORDER 0.00 BASE QTY, SKIPPING')
|
||||
|
||||
if place_order:
|
||||
price = Decimal(str(price)).quantize(Decimal(str(0.01)), rounding=ROUND_DOWN)
|
||||
price: Decimal = Decimal(str(price)).quantize(Decimal(str(0.01)), rounding=ROUND_DOWN)
|
||||
post_order = {
|
||||
"url": "/fapi/v3/order",
|
||||
"method": "POST",
|
||||
@@ -650,7 +658,7 @@ async def run_algo():
|
||||
'price': price,
|
||||
}
|
||||
}
|
||||
order_resp = await aster_auth.post_authenticated_url(post_order)
|
||||
order_resp: dict = await aster_auth.post_authenticated_url(post_order) # ty:ignore[invalid-assignment]
|
||||
if order_resp.get('orderId', None) is not None:
|
||||
order_resp['original_price'] = price
|
||||
order_resp['order_status'] = order_resp['status']
|
||||
@@ -672,14 +680,14 @@ async def run_algo():
|
||||
Time_Since_Last_Aster_Fill_ms = ( datetime.now().timestamp()*1000 ) - Last_Aster_Fill_Time_Ts
|
||||
if Time_Since_Last_Aster_Fill_ms > ( 1000 * ALGO_CONFIG.Config.Switch_To_Taker_Seconds ): # Change to allow taker orders if its been more than x seconds
|
||||
post_only = False
|
||||
price = EXTEND_TOB_PX - ALGO_CONFIG.Config.Price_Worsener_Extend if side == 'BUY' else EXTEND_TOB_PX + ALGO_CONFIG.Config.Price_Worsener_Extend
|
||||
price: Decimal = Decimal(value=str(EXTEND_TOB_PX - ALGO_CONFIG.Config.Price_Worsener_Extend if side == 'BUY' else EXTEND_TOB_PX + ALGO_CONFIG.Config.Price_Worsener_Extend)).quantize(Decimal(str(0.1)), rounding=ROUND_DOWN)
|
||||
else:
|
||||
post_only = True
|
||||
price = EXTEND_TOB_PX
|
||||
price: Decimal = Decimal(value=str(EXTEND_TOB_PX)).quantize(Decimal(str(0.1)), rounding=ROUND_DOWN)
|
||||
|
||||
symbol = EXTEND.symbol
|
||||
side = OrderSide.BUY if EXTEND_TGT_TAIL_BASE_QTY > 0.00 else OrderSide.SELL
|
||||
qty = Decimal(str(abs(EXTEND_TGT_TAIL_BASE_QTY)))
|
||||
qty = Decimal(value=str(abs(EXTEND_TGT_TAIL_BASE_QTY)))
|
||||
|
||||
if abs( ( float(EXTEND_TGT_TAIL_BASE_QTY)*float(price) ) + EXTEND_NOTIONAL_POSITION ) > ALGO_CONFIG.Config.Max_Target_Notional*ALGO_CONFIG.Config.Max_Order_Over_Notional_Ratio:
|
||||
logging.info(f'TRYING TO ORDER OVER MAX NOTIOANL - EXTEND: {EXTEND_NOTIONAL_POSITION:.2f} + {float(EXTEND_TGT_TAIL_BASE_QTY)*float(price):.2f} (qty: {float(EXTEND_TGT_TAIL_BASE_QTY):.2f}; px: {float(price):.2f})')
|
||||
@@ -703,13 +711,13 @@ async def run_algo():
|
||||
open_order_px = 0
|
||||
place_order = True
|
||||
if place_order:
|
||||
price = Decimal(str(price)).quantize(Decimal(str(0.1)), rounding=ROUND_DOWN)
|
||||
price: Decimal = Decimal(str(price)).quantize(Decimal(str(0.1)), rounding=ROUND_DOWN)
|
||||
if round(open_order_px - float(price), 2) == 0.00:
|
||||
logging.info('EXTEND OPEN ORDER NO PX CHG; SKIPPING')
|
||||
else:
|
||||
try:
|
||||
taker_fee = taker_fee=Decimal("0.00000") if post_only else Decimal("0.00025")
|
||||
order_resp = await EXTEND_CLIENT.place_order(
|
||||
order_resp: WrappedApiResponse[PlacedOrderModel] = await EXTEND_CLIENT.place_order(
|
||||
market_name=symbol,
|
||||
amount_of_synthetic=qty,
|
||||
price=price,
|
||||
@@ -780,16 +788,14 @@ async def main():
|
||||
VAL_KEY = valkey.Valkey(host='localhost', port=6379, db=0, decode_responses=True)
|
||||
engine = create_async_engine('mysql+asyncmy://root:pwd@localhost/fund_rate')
|
||||
|
||||
with open('algo_config.json', 'r', encoding='utf-8') as file:
|
||||
with open('algo_config.json', mode='r', encoding='utf-8') as file:
|
||||
ALGO_CONFIG = json.load(file)
|
||||
ALGO_CONFIG = structs.Algo_Config(**ALGO_CONFIG)
|
||||
|
||||
# with open('algo_config.json', 'r', encoding='utf-8') as file:
|
||||
# ALGO_CONFIG = json.load(file, object_hook=lambda d: structs.Algo_Config(**d))
|
||||
|
||||
|
||||
ALGO_CONFIG.Config.Max_Target_Notional = float(min([ASTER.mult, EXTEND.mult]) * ALGO_CONFIG.Config.Target_Open_Cash_Position)
|
||||
|
||||
VAL_KEY.set('fr_orchestrator_output', json.dumps(ALGO_CONFIG.model_dump()))
|
||||
VAL_KEY.set(name='fr_orchestrator_output', value=json.dumps(obj=ALGO_CONFIG.model_dump()))
|
||||
VAL_KEY.set(name='fr_algo_working_symbol', value=json.dumps(obj={'ASTER': asdict(obj=ASTER), 'EXTEND': asdict(obj=EXTEND)}))
|
||||
|
||||
async with engine.connect() as CON:
|
||||
### ASTER SETUP ###
|
||||
|
||||
@@ -3,50 +3,51 @@ from dotenv import load_dotenv
|
||||
import os
|
||||
import time
|
||||
import threading
|
||||
import urllib
|
||||
from urllib import parse
|
||||
|
||||
from eth_account.messages import encode_typed_data
|
||||
from eth_account import Account
|
||||
from eth_account.datastructures import SignedMessage
|
||||
|
||||
load_dotenv()
|
||||
|
||||
user = os.getenv("RABBY_WALLET")
|
||||
signer = os.getenv("ASTER_API_WALLET_ADDRESS")
|
||||
private_key = os.getenv("ASTER_API_PRIVATE_KEY")
|
||||
USER: str = os.getenv(key="RABBY_WALLET") # ty:ignore[invalid-assignment]
|
||||
SIGNER: str = os.getenv(key="ASTER_API_WALLET_ADDRESS") # ty:ignore[invalid-assignment]
|
||||
PRIVATE_KEY: str = os.getenv(key="ASTER_API_PRIVATE_KEY") # ty:ignore[invalid-assignment]
|
||||
|
||||
_last_ms = 0
|
||||
_i = 0
|
||||
|
||||
async def post_authenticated_url(req: dict) -> dict:
|
||||
typed_data = {
|
||||
"types": {
|
||||
"EIP712Domain": [
|
||||
{"name": "name", "type": "string"},
|
||||
{"name": "version", "type": "string"},
|
||||
{"name": "chainId", "type": "uint256"},
|
||||
{"name": "verifyingContract", "type": "address"}
|
||||
],
|
||||
"Message": [
|
||||
{ "name": "msg", "type": "string" }
|
||||
]
|
||||
},
|
||||
"primaryType": "Message",
|
||||
"domain": {
|
||||
"name": "AsterSignTransaction",
|
||||
"version": "1",
|
||||
"chainId": 1666,
|
||||
"verifyingContract": "0x0000000000000000000000000000000000000000"
|
||||
},
|
||||
"message": {
|
||||
"msg": "$msg"
|
||||
async def post_authenticated_url(req: dict) -> list | dict:
|
||||
typed_data: dict = {
|
||||
"types": {
|
||||
"EIP712Domain": [
|
||||
{"name": "name", "type": "string"},
|
||||
{"name": "version", "type": "string"},
|
||||
{"name": "chainId", "type": "uint256"},
|
||||
{"name": "verifyingContract", "type": "address"}
|
||||
],
|
||||
"Message": [
|
||||
{ "name": "msg", "type": "string" },
|
||||
]
|
||||
},
|
||||
"primaryType": "Message",
|
||||
"domain": {
|
||||
"name": "AsterSignTransaction",
|
||||
"version": "1",
|
||||
"chainId": 1666,
|
||||
"verifyingContract": "0x0000000000000000000000000000000000000000"
|
||||
},
|
||||
"message": {
|
||||
"msg": "$msg"
|
||||
}
|
||||
}
|
||||
}
|
||||
headers = {
|
||||
|
||||
headers: dict[str, str] = {
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
'User-Agent': 'PythonApp/1.0'
|
||||
}
|
||||
host = 'https://fapi.asterdex.com'
|
||||
|
||||
host: str = 'https://fapi.asterdex.com'
|
||||
|
||||
def get_nonce():
|
||||
_nonce_lock = threading.Lock()
|
||||
@@ -71,38 +72,34 @@ async def post_authenticated_url(req: dict) -> dict:
|
||||
)
|
||||
return Account.sign_message(message, private_key=private_key)
|
||||
|
||||
async def send_by_url(req):
|
||||
async def send_by_url(req) -> list | dict: # ty:ignore[invalid-return-type]
|
||||
my_dict = req['params'].copy()
|
||||
url = host + req['url']
|
||||
method = req['method']
|
||||
|
||||
my_dict['nonce'] = str(get_nonce())
|
||||
my_dict['user'] = user
|
||||
my_dict['signer'] = signer
|
||||
my_dict['nonce'] = str(object=get_nonce())
|
||||
my_dict['user'] = USER
|
||||
my_dict['signer'] = SIGNER
|
||||
|
||||
param = urllib.parse.urlencode(my_dict)
|
||||
param: str = parse.urlencode(query=my_dict)
|
||||
|
||||
typed_data['message']['msg'] = param
|
||||
signed = sign_typed_data(typed_data, private_key)
|
||||
signed: SignedMessage = sign_typed_data(data=typed_data, private_key=PRIVATE_KEY)
|
||||
|
||||
full_url = url + '?' + param + '&signature=' + signed.signature.hex()
|
||||
# print(full_url)
|
||||
full_url: str = url + '?' + param + '&signature=' + signed.signature.hex()
|
||||
|
||||
if method == 'GET':
|
||||
res = requests.get(full_url, headers=headers)
|
||||
res: requests.Response = requests.get(url=full_url, headers=headers)
|
||||
# print(res.status_code, res.text)
|
||||
return res.json()
|
||||
elif method == 'POST':
|
||||
res = requests.post(full_url, headers=headers)
|
||||
# print(res.status_code, res.text)
|
||||
res: requests.Response = requests.post(url=full_url, headers=headers)
|
||||
return res.json()
|
||||
elif method == 'PUT':
|
||||
res = requests.put(full_url, headers=headers)
|
||||
# print(res.status_code, res.text)
|
||||
res: requests.Response = requests.put(url=full_url, headers=headers)
|
||||
return res.json()
|
||||
elif method == 'DELETE':
|
||||
res = requests.delete(full_url, headers=headers)
|
||||
# print(res.status_code, res.text)
|
||||
res: requests.Response = requests.delete(url=full_url, headers=headers)
|
||||
return res.json()
|
||||
|
||||
return await send_by_url(req=req)
|
||||
|
||||
38
modules/manual_leverage.py
Normal file
38
modules/manual_leverage.py
Normal 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 ),
|
||||
]
|
||||
@@ -9,9 +9,11 @@ from pydantic import BaseModel
|
||||
class Algo_Config_Overrides(BaseModel):
|
||||
Allow_Ordering_Aster: bool
|
||||
Allow_Ordering_Extend: bool
|
||||
Allow_Symbol_Change: bool
|
||||
Flatten_Open_Positions: bool
|
||||
Flip_Side_For_Testing: bool
|
||||
|
||||
|
||||
# @dataclass(kw_only=True)
|
||||
class Algo_Config_Config(BaseModel):
|
||||
Loop_Sleep_Sec: int
|
||||
@@ -51,8 +53,8 @@ class Valkey_Stream:
|
||||
none_fill: Any = None
|
||||
|
||||
async def update(self):
|
||||
r = self.client.get(self.channel)
|
||||
self.data = json.loads(r) if r is not None else self.none_fill
|
||||
r: str = self.client.get(name=self.channel) # ty:ignore[invalid-assignment]
|
||||
self.data = json.loads(s=r) if r is not None else self.none_fill
|
||||
|
||||
|
||||
@dataclass(kw_only=True)
|
||||
@@ -162,40 +164,40 @@ class Perpetual_Exchange:
|
||||
rh_asset: str
|
||||
symbol_asset_separator: str = ''
|
||||
|
||||
async def update(self):
|
||||
await self.Collateral_Updates.update()
|
||||
await self.Order_Updates.update()
|
||||
await self.Position_Updates.update()
|
||||
await self.Funding_Rate.update()
|
||||
# async def update(self):
|
||||
# await self.Collateral_Updates.update()
|
||||
# await self.Order_Updates.update()
|
||||
# await self.Position_Updates.update()
|
||||
# await self.Funding_Rate.update()
|
||||
|
||||
def __post_init__(self) -> None:
|
||||
self.symbol = f'{self.lh_asset.upper()}{self.symbol_asset_separator}{self.rh_asset.upper()}'
|
||||
|
||||
|
||||
@dataclass(kw_only=True)
|
||||
class Aster(Perpetual_Exchange):
|
||||
name: str = 'Aster'
|
||||
lh_asset: str = 'ETH'
|
||||
rh_asset: str = 'USDT'
|
||||
# @dataclass(kw_only=True)
|
||||
# class Aster(Perpetual_Exchange):
|
||||
# name: str = 'Aster'
|
||||
# lh_asset: str = 'ETH'
|
||||
# rh_asset: str = 'USDT'
|
||||
|
||||
def __post_init__(self):
|
||||
super().__post_init__()
|
||||
self.Order_Updates = Order_Updates(Valkey=Valkey_Stream(channel = 'fr_aster_user_balances', none_fills = []))
|
||||
self.Collateral_Updates = Collateral(Valkey=Valkey_Stream(channel = 'fr_aster_user_orders', none_fills = []))
|
||||
self.Position_Updates = Open_Positions(Valkey=Valkey_Stream(channel = 'fr_aster_user_positions', none_fills = []))
|
||||
self.Funding_Rate - Funding_Rate(Valkey=Valkey_Stream(channel = 'fund_rate_aster', none_fills = None))
|
||||
# def __post_init__(self):
|
||||
# super().__post_init__()
|
||||
# self.Order_Updates = Order_Updates(Valkey=Valkey_Stream(channel = 'fr_aster_user_balances', none_fills = []))
|
||||
# self.Collateral_Updates = Collateral(Valkey=Valkey_Stream(channel = 'fr_aster_user_orders', none_fills = []))
|
||||
# self.Position_Updates = Open_Positions(Valkey=Valkey_Stream(channel = 'fr_aster_user_positions', none_fills = []))
|
||||
# self.Funding_Rate - Funding_Rate(Valkey=Valkey_Stream(channel = 'fund_rate_aster', none_fills = None))
|
||||
|
||||
|
||||
@dataclass(kw_only=True)
|
||||
class Extend(Perpetual_Exchange):
|
||||
name: str = 'Extended'
|
||||
lh_asset: str = 'ETH'
|
||||
rh_asset: str = 'USD'
|
||||
symbol_asset_separator: str = '-'
|
||||
# @dataclass(kw_only=True)
|
||||
# class Extend(Perpetual_Exchange):
|
||||
# name: str = 'Extended'
|
||||
# lh_asset: str = 'ETH'
|
||||
# rh_asset: str = 'USD'
|
||||
# symbol_asset_separator: str = '-'
|
||||
|
||||
def __post_init__(self):
|
||||
super().__post_init__()
|
||||
self.Order_Updates = Order_Updates(Valkey=Valkey_Stream(channel = 'fr_aster_user_balances', none_fills = []))
|
||||
self.Collateral_Updates = Collateral(Valkey=Valkey_Stream(channel = 'fr_aster_user_orders', none_fills = []))
|
||||
self.Position_Updates = Open_Positions(Valkey=Valkey_Stream(channel = 'fr_aster_user_positions', none_fills = []))
|
||||
self.Funding_Rate - Funding_Rate(Valkey=Valkey_Stream(channel = 'fund_rate_aster', none_fills = None))
|
||||
# def __post_init__(self):
|
||||
# super().__post_init__()
|
||||
# self.Order_Updates = Order_Updates(Valkey=Valkey_Stream(channel = 'fr_aster_user_balances', none_fills = []))
|
||||
# self.Collateral_Updates = Collateral(Valkey=Valkey_Stream(channel = 'fr_aster_user_orders', none_fills = []))
|
||||
# self.Position_Updates = Open_Positions(Valkey=Valkey_Stream(channel = 'fr_aster_user_positions', none_fills = []))
|
||||
# self.Funding_Rate - Funding_Rate(Valkey=Valkey_Stream(channel = 'fund_rate_aster', none_fills = None))
|
||||
|
||||
@@ -5,7 +5,7 @@ import os
|
||||
|
||||
load_dotenv()
|
||||
|
||||
def upsert_list_of_dicts_by_id(list_of_dicts, new_dict, id='id', seq_check_field: str | None = None):
|
||||
def upsert_list_of_dicts_by_id(list_of_dicts, new_dict, id='id', seq_check_field: str | None = None) -> list[dict]:
|
||||
for index, item in enumerate(list_of_dicts):
|
||||
if item.get(id) == new_dict.get(id):
|
||||
if seq_check_field is not None:
|
||||
@@ -25,4 +25,19 @@ def send_tg_alert(msg: str):
|
||||
url = f'https://api.telegram.org/bot{token}/sendMessage?chat_id={chat_id}'
|
||||
response = requests.post(url, json={'text': str(str(msg)[:250])}, timeout=10)
|
||||
|
||||
return response.json()
|
||||
return response.json()
|
||||
|
||||
def rec_set_dict(orig_dict, new_dict, allow_new_fields: bool = False) -> dict:
|
||||
for k, v in new_dict.items():
|
||||
if isinstance(v, dict):
|
||||
rec_set_dict(orig_dict=orig_dict[k], new_dict=v)
|
||||
else:
|
||||
if allow_new_fields:
|
||||
orig_dict[k] = v
|
||||
else:
|
||||
if orig_dict.get(k, None) is not None:
|
||||
orig_dict[k] = v
|
||||
else:
|
||||
logging.warning(msg=f'rec_set_dict: encountered nonexistent key: "{k}"; skipping')
|
||||
|
||||
return orig_dict
|
||||
65
ws_aster.py
65
ws_aster.py
@@ -5,7 +5,7 @@ import socket
|
||||
import traceback
|
||||
from datetime import datetime
|
||||
from typing import AsyncContextManager
|
||||
|
||||
import time
|
||||
import numpy as np
|
||||
import pandas as pd
|
||||
import requests.packages.urllib3.util.connection as urllib3_cn # type: ignore
|
||||
@@ -26,35 +26,79 @@ urllib3_cn.allowed_gai_family = allowed_gai_family
|
||||
### Database ###
|
||||
USE_DB: bool = True
|
||||
USE_VK: bool = True
|
||||
CON: AsyncContextManager
|
||||
VAL_KEY: valkey.Valkey
|
||||
VK_FUND_RATE = 'fund_rate_aster'
|
||||
VK_TICKER = 'fut_ticker_aster'
|
||||
VK_LAST_TRADE = 'fut_last_trade_aster'
|
||||
CON: AsyncContextManager | None = None
|
||||
VAL_KEY = None
|
||||
|
||||
### Logging ###
|
||||
load_dotenv()
|
||||
LOG_FILEPATH: str = os.getenv("LOGS_PATH") + '/Fund_Rate_Aster.log'
|
||||
LOG_FILEPATH: str = f'{os.getenv(key="LOGS_PATH")}/Fund_Rate_Aster.log'
|
||||
|
||||
### CONSTANTS ###
|
||||
SYMBOL: str = 'ETHUSDT'
|
||||
|
||||
STREAM_MARKPRICE: str = f'{SYMBOL.lower()}@markPrice@1s'
|
||||
STREAM_BOOKTICKER: str = f'{SYMBOL.lower()}@bookTicker'
|
||||
STREAM_TRADES: str = f'{SYMBOL.lower()}@aggTrade'
|
||||
|
||||
### Globals ###
|
||||
WSS_URL = f"wss://fstream.asterdex.com/stream?streams={STREAM_MARKPRICE}/{STREAM_BOOKTICKER}/{STREAM_TRADES}"
|
||||
WSS_URL: str = f"wss://fstream.asterdex.com/stream?streams={STREAM_MARKPRICE}/{STREAM_BOOKTICKER}/{STREAM_TRADES}"
|
||||
ALLOW_SYMBOL_CHG: bool = False
|
||||
|
||||
### Funcs ###
|
||||
async def subscribe_streams(websocket, streams: list[str]) -> None:
|
||||
logging.info(f'Trying to sub: {streams}')
|
||||
msg = {
|
||||
"method": "SUBSCRIBE",
|
||||
"params": streams,
|
||||
"id": int(round(number=datetime.now().timestamp()*1000))
|
||||
}
|
||||
await websocket.send(json.dumps(obj=msg))
|
||||
logging.info(f'Success sub: {streams}')
|
||||
|
||||
|
||||
async def unsubscribe_streams(websocket, streams: list[str]) -> None:
|
||||
logging.info(f'Trying to unsub: {streams}')
|
||||
msg = {
|
||||
"method": "UNSUBSCRIBE",
|
||||
"params": streams,
|
||||
"id": int(round(number=datetime.now().timestamp()*1000))
|
||||
}
|
||||
await websocket.send(json.dumps(obj=msg))
|
||||
logging.info(f'Success unsub: {streams}')
|
||||
|
||||
|
||||
### Websocket ###
|
||||
async def ws_stream():
|
||||
async for websocket in websockets.connect(WSS_URL):
|
||||
logging.info(f"Connected to {WSS_URL}")
|
||||
global SYMBOL
|
||||
global STREAM_MARKPRICE
|
||||
global STREAM_BOOKTICKER
|
||||
global STREAM_TRADES
|
||||
|
||||
async for websocket in websockets.connect(WSS_URL, ping_interval=5):
|
||||
logging.info(msg=f"Connected to {WSS_URL}")
|
||||
try:
|
||||
async for message in websocket:
|
||||
### Update Symbol if Algo Outputs Change ###
|
||||
if ALLOW_SYMBOL_CHG:
|
||||
best_symbol_by_exchange: dict = json.loads(s=VAL_KEY.get(name='fr_algo_working_symbol')) # ty:ignore[invalid-argument-type]
|
||||
best_symbol: str = f'{best_symbol_by_exchange['ASTER']['lh_asset']}{best_symbol_by_exchange['ASTER']['rh_asset']}'
|
||||
if best_symbol != SYMBOL:
|
||||
logging.info(f'Symbol Change: {SYMBOL} -> {best_symbol}')
|
||||
SYMBOL = best_symbol
|
||||
await unsubscribe_streams(websocket = websocket, streams=[STREAM_MARKPRICE,STREAM_BOOKTICKER,STREAM_TRADES])
|
||||
|
||||
STREAM_MARKPRICE = f'{SYMBOL.lower()}@markPrice@1s'
|
||||
STREAM_BOOKTICKER = f'{SYMBOL.lower()}@bookTicker'
|
||||
STREAM_TRADES = f'{SYMBOL.lower()}@aggTrade'
|
||||
await subscribe_streams(websocket = websocket, streams=[STREAM_MARKPRICE,STREAM_BOOKTICKER,STREAM_TRADES])
|
||||
continue
|
||||
|
||||
ts_arrival = round(datetime.now().timestamp()*1000)
|
||||
if isinstance(message, str):
|
||||
try:
|
||||
try:
|
||||
data = json.loads(message)
|
||||
channel = data.get('stream', None)
|
||||
if channel is not None:
|
||||
@@ -132,8 +176,8 @@ async def main():
|
||||
if USE_VK:
|
||||
VAL_KEY = valkey.Valkey(host='localhost', port=6379, db=0)
|
||||
else:
|
||||
VAL_KEY = None
|
||||
logging.warning("VALKEY NOT BEING USED, NO DATA WILL BE PUBLISHED")
|
||||
raise NotImplementedError('Cannot run without VK')
|
||||
|
||||
if USE_DB:
|
||||
engine = create_async_engine('mysql+asyncmy://root:pwd@localhost/fund_rate')
|
||||
@@ -141,9 +185,8 @@ async def main():
|
||||
await aster_db.create_fr_aster_mkt_trades(CON=CON)
|
||||
await ws_stream()
|
||||
else:
|
||||
CON = None
|
||||
logging.warning("DATABASE NOT BEING USED, NO DATA WILL BE RECORDED")
|
||||
await ws_stream()
|
||||
raise NotImplementedError('Cannot run without DB')
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
|
||||
@@ -3,16 +3,15 @@ import json
|
||||
import logging
|
||||
import socket
|
||||
import traceback
|
||||
from datetime import datetime, timezone
|
||||
from datetime import datetime
|
||||
from typing import AsyncContextManager
|
||||
import math
|
||||
|
||||
import numpy as np
|
||||
import pandas as pd
|
||||
# import numpy as np
|
||||
# import pandas as pd
|
||||
import requests.packages.urllib3.util.connection as urllib3_cn # type: ignore
|
||||
from sqlalchemy import text
|
||||
# from sqlalchemy import text
|
||||
import websockets
|
||||
from sqlalchemy.ext.asyncio import create_async_engine
|
||||
# from sqlalchemy.ext.asyncio import create_async_engine
|
||||
import valkey
|
||||
import os
|
||||
from dotenv import load_dotenv
|
||||
@@ -25,31 +24,30 @@ urllib3_cn.allowed_gai_family = allowed_gai_family
|
||||
|
||||
### Database ###
|
||||
USE_DB: bool = False
|
||||
USE_VK: bool = True
|
||||
VK_FUND_RATE_ALL = 'fund_rate_aster_all'
|
||||
VK_CHANNEL = 'fund_rate_aster_all'
|
||||
|
||||
CON: AsyncContextManager | None = None
|
||||
VAL_KEY = None
|
||||
CON: AsyncContextManager
|
||||
VAL_KEY: valkey.Valkey
|
||||
|
||||
### Logging ###
|
||||
load_dotenv()
|
||||
LOG_FILEPATH: str = os.getenv("LOGS_PATH") + '/Fund_Rate_Aster_FR_ALL.log'
|
||||
LOG_FILEPATH: str = f'{os.getenv(key="LOGS_PATH")}/Fund_Rate_Aster_FR_ALL.log'
|
||||
|
||||
### Globals ###
|
||||
WSS_URL = "wss://fstream.asterdex.com/ws/!markPrice@arr"
|
||||
WSS_URL: str = "wss://fstream.asterdex.com/ws/!markPrice@arr"
|
||||
|
||||
|
||||
### Websocket ###
|
||||
async def ws_stream():
|
||||
async for websocket in websockets.connect(WSS_URL):
|
||||
logging.info(f"Connected to {WSS_URL}")
|
||||
logging.info(msg=f"Connected to {WSS_URL}")
|
||||
try:
|
||||
async for message in websocket:
|
||||
if isinstance(message, str):
|
||||
try:
|
||||
data = json.loads(message)
|
||||
data: dict = json.loads(message)
|
||||
if data:
|
||||
VAL_KEY.set(VK_FUND_RATE_ALL, json.dumps(data))
|
||||
VAL_KEY.set(name=VK_CHANNEL, value=json.dumps(obj=data))
|
||||
# print(f'VK_SAVED: {len(data)}')
|
||||
continue
|
||||
else:
|
||||
@@ -61,39 +59,35 @@ async def ws_stream():
|
||||
else:
|
||||
raise ValueError(f'Type: {type(data)} not expected: {message}')
|
||||
except websockets.ConnectionClosed as e:
|
||||
logging.error(f'Connection closed: {e}')
|
||||
logging.error(traceback.format_exc())
|
||||
continue
|
||||
logging.error(msg=f'Connection closed: {e}')
|
||||
logging.error(msg=traceback.format_exc())
|
||||
utils.send_tg_alert(msg=f'WS: {VK_CHANNEL} - Failure: {e}')
|
||||
except Exception as e:
|
||||
logging.error(f'Connection closed: {e}')
|
||||
logging.error(traceback.format_exc())
|
||||
logging.error(msg=f'Connection closed: {e}')
|
||||
logging.error(msg=traceback.format_exc())
|
||||
utils.send_tg_alert(msg=f'WS: {VK_CHANNEL} - Failure: {e}')
|
||||
|
||||
### Startup ###
|
||||
async def main():
|
||||
global VAL_KEY
|
||||
global CON
|
||||
|
||||
if USE_VK:
|
||||
VAL_KEY = valkey.Valkey(host='localhost', port=6379, db=0)
|
||||
else:
|
||||
VAL_KEY = None
|
||||
logging.warning("VALKEY NOT BEING USED, NO DATA WILL BE PUBLISHED")
|
||||
|
||||
VAL_KEY = valkey.Valkey(host='localhost', port=6379, db=0)
|
||||
|
||||
if USE_DB:
|
||||
engine = create_async_engine('mysql+asyncmy://root:pwd@localhost/fund_rate')
|
||||
async with engine.connect() as CON:
|
||||
# await create_rtds_btcusd_table(CON=CON)
|
||||
await ws_stream()
|
||||
raise NotImplementedError('DB not implemented for this ws.')
|
||||
# engine = create_async_engine('mysql+asyncmy://root:pwd@localhost/fund_rate')
|
||||
# async with engine.connect() as CON:
|
||||
# await ws_stream()
|
||||
else:
|
||||
CON = None
|
||||
logging.warning("DATABASE NOT BEING USED, NO DATA WILL BE RECORDED")
|
||||
await ws_stream()
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
START_TIME = round(datetime.now().timestamp()*1000)
|
||||
START_TIME: int = round(number=datetime.now().timestamp()*1000)
|
||||
|
||||
logging.info(f'Log FilePath: {LOG_FILEPATH}')
|
||||
logging.info(msg=f'Log FilePath: {LOG_FILEPATH}')
|
||||
|
||||
logging.basicConfig(
|
||||
force=True,
|
||||
@@ -102,9 +96,9 @@ if __name__ == '__main__':
|
||||
format='%(asctime)s - %(levelname)s - %(message)s',
|
||||
filemode='w'
|
||||
)
|
||||
logging.info(f"STARTED: {START_TIME}")
|
||||
logging.info(msg=f"STARTED: {START_TIME}")
|
||||
|
||||
try:
|
||||
asyncio.run(main())
|
||||
except KeyboardInterrupt:
|
||||
logging.info("Stream stopped")
|
||||
logging.info(msg="Stream stopped")
|
||||
159
ws_aster_user.py
159
ws_aster_user.py
@@ -1,20 +1,19 @@
|
||||
import asyncio
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
import socket
|
||||
import traceback
|
||||
from datetime import datetime
|
||||
from typing import AsyncContextManager
|
||||
|
||||
import numpy as np
|
||||
import pandas as pd
|
||||
import requests.packages.urllib3.util.connection as urllib3_cn # type: ignore
|
||||
from sqlalchemy import text
|
||||
import websockets
|
||||
from sqlalchemy.ext.asyncio import create_async_engine
|
||||
import valkey
|
||||
import os
|
||||
|
||||
import websockets
|
||||
from dotenv import load_dotenv
|
||||
from sqlalchemy.ext.asyncio import create_async_engine
|
||||
|
||||
import modules.aster_auth as aster_auth
|
||||
import modules.aster_db as aster_db
|
||||
import modules.db as db
|
||||
@@ -28,84 +27,84 @@ urllib3_cn.allowed_gai_family = allowed_gai_family
|
||||
### Database ###
|
||||
USE_DB: bool = True
|
||||
USE_VK: bool = True
|
||||
VK_ORDERS_TRADES = 'fr_aster_user_orders'
|
||||
VK_MARGIN_CALLS = 'fr_aster_user_margin_calls'
|
||||
VK_BALANCES = 'fr_aster_user_balances'
|
||||
VK_POSITIONS = 'fr_aster_user_positions'
|
||||
CON: AsyncContextManager | None = None
|
||||
VAL_KEY = None
|
||||
CON: AsyncContextManager
|
||||
VAL_KEY: valkey.Valkey
|
||||
VK_ORDERS_TRADES: str = 'fr_aster_user_orders'
|
||||
VK_MARGIN_CALLS: str = 'fr_aster_user_margin_calls'
|
||||
VK_BALANCES: str = 'fr_aster_user_balances'
|
||||
VK_POSITIONS: str = 'fr_aster_user_positions'
|
||||
|
||||
### Logging ###
|
||||
load_dotenv()
|
||||
LOG_FILEPATH: str = os.getenv("LOGS_PATH") + '/Fund_Rate_Aster_User.log'
|
||||
LOG_FILEPATH: str = f'{os.getenv(key="LOGS_PATH")}/Fund_Rate_Aster_User.log'
|
||||
|
||||
### CONSTANTS ###
|
||||
WSS_URL = "wss://fstream.asterdex.com/ws/"
|
||||
LOCAL_RECENT_UPDATES_LOOKBACK_SEC = 30
|
||||
WSS_URL: str = "wss://fstream.asterdex.com/ws/"
|
||||
LOCAL_RECENT_UPDATES_LOOKBACK_SEC: int = 30
|
||||
|
||||
### Globals ###
|
||||
LISTEN_KEY: str | None = None
|
||||
LISTEN_KEY_LAST_UPDATE_TS_S: int = 0
|
||||
LISTEN_KEY_PUT_INTERVAL_SEC = 1800
|
||||
Listen_Key: str
|
||||
Listen_Key_Last_Update_TS_S: int = 0
|
||||
Listen_Key_Put_Interval_Sec: int = 1800
|
||||
|
||||
LOCAL_RECENT_ORDERS: list = []
|
||||
LOCAL_RECENT_MARGIN_CALLS: list = []
|
||||
LOCAL_RECENT_BALANCES: list = []
|
||||
LOCAL_RECENT_POSITIONS: list = []
|
||||
Local_Recent_Orders: list[dict] = []
|
||||
Local_Recent_Margin_Calls: list[dict] = []
|
||||
Local_Recent_Balances: list[dict] = []
|
||||
Local_Recent_Positions: list[dict] = []
|
||||
|
||||
|
||||
async def get_new_listen_key() -> str:
|
||||
global LISTEN_KEY_LAST_UPDATE_TS_S
|
||||
global Listen_Key_Last_Update_TS_S
|
||||
|
||||
listen_key_request = {
|
||||
listen_key_request: dict = {
|
||||
"url": "/fapi/v3/listenKey",
|
||||
"method": "POST",
|
||||
"params": {}
|
||||
}
|
||||
r = await aster_auth.post_authenticated_url(listen_key_request)
|
||||
listen_key = r.get('listenKey', None)
|
||||
r: dict = await aster_auth.post_authenticated_url(listen_key_request) # ty:ignore[invalid-assignment]
|
||||
listen_key: str = r.get('listenKey', '')
|
||||
print(f'LISTEN KEY: {listen_key}')
|
||||
if listen_key is not None:
|
||||
LISTEN_KEY_LAST_UPDATE_TS_S = round(datetime.now().timestamp())
|
||||
if listen_key:
|
||||
Listen_Key_Last_Update_TS_S = round(number=datetime.now().timestamp())
|
||||
return listen_key
|
||||
else:
|
||||
raise ValueError(f'Listen Key is None; Failed to Update. response: {r}')
|
||||
raise ValueError(f'Listen Key is empty; Failed to Update. response: {r}')
|
||||
|
||||
async def listen_key_interval():
|
||||
global LISTEN_KEY
|
||||
global Listen_Key
|
||||
|
||||
while True:
|
||||
await asyncio.sleep(LISTEN_KEY_PUT_INTERVAL_SEC)
|
||||
LISTEN_KEY = await get_new_listen_key()
|
||||
await asyncio.sleep(delay=Listen_Key_Put_Interval_Sec)
|
||||
Listen_Key = await get_new_listen_key()
|
||||
|
||||
### Websocket ###
|
||||
async def ws_stream():
|
||||
global LISTEN_KEY
|
||||
global LOCAL_RECENT_ORDERS
|
||||
global LOCAL_RECENT_MARGIN_CALLS
|
||||
global LOCAL_RECENT_BALANCES
|
||||
global LOCAL_RECENT_POSITIONS
|
||||
global Listen_Key
|
||||
global Local_Recent_Orders
|
||||
global Local_Recent_Margin_Calls
|
||||
global Local_Recent_Balances
|
||||
global Local_Recent_Positions
|
||||
|
||||
LISTEN_KEY = await get_new_listen_key()
|
||||
Listen_Key = await get_new_listen_key()
|
||||
|
||||
async for websocket in websockets.connect(WSS_URL+LISTEN_KEY):
|
||||
logging.info(f"Connected to {WSS_URL}")
|
||||
asyncio.create_task(listen_key_interval())
|
||||
async for websocket in websockets.connect(uri=WSS_URL+Listen_Key, ping_interval=5):
|
||||
logging.info(msg=f"Connected to {WSS_URL}")
|
||||
asyncio.create_task(coro=listen_key_interval())
|
||||
try:
|
||||
async for message in websocket:
|
||||
ts_arrival = round(datetime.now().timestamp()*1000)
|
||||
ts_arrival: int = round(number=datetime.now().timestamp()*1000)
|
||||
|
||||
if isinstance(message, str):
|
||||
try:
|
||||
data = json.loads(message)
|
||||
channel = data.get('e', None)
|
||||
if channel is not None:
|
||||
LOOKBACK_MIN_TS_MS = ts_arrival - (LOCAL_RECENT_UPDATES_LOOKBACK_SEC*1000)
|
||||
data: dict = json.loads(s=message)
|
||||
channel: str = data.get('e', '')
|
||||
if channel:
|
||||
lookback_min_ts_ms: int = ts_arrival - (LOCAL_RECENT_UPDATES_LOOKBACK_SEC*1000)
|
||||
|
||||
match channel:
|
||||
case 'ORDER_TRADE_UPDATE':
|
||||
# logging.info(f'ORDER_TRADE_UPDATE: {data}')
|
||||
new_order_update = {
|
||||
new_order_update: dict = {
|
||||
'timestamp_arrival': ts_arrival,
|
||||
'timestamp_msg': data['E'],
|
||||
'timestamp_transaction': data['T'],
|
||||
@@ -141,11 +140,11 @@ async def ws_stream():
|
||||
'callback_rate': float(data['o'].get("cr", 0)), # :"5.0", // Callback Rate, only puhed with TRAILING_STOP_MARKET order
|
||||
'realized_profit': float(data['o']["rp"]), # :"0" // Realized Profit of the trade
|
||||
}
|
||||
LOCAL_RECENT_ORDERS = utils.upsert_list_of_dicts_by_id(LOCAL_RECENT_ORDERS, new_order_update, id='order_id', seq_check_field='timestamp_msg')
|
||||
LOCAL_RECENT_ORDERS = [t for t in LOCAL_RECENT_ORDERS if t.get('timestamp_arrival', 0) >= LOOKBACK_MIN_TS_MS]
|
||||
Local_Recent_Orders = utils.upsert_list_of_dicts_by_id(Local_Recent_Orders, new_order_update, id='order_id', seq_check_field='timestamp_msg')
|
||||
Local_Recent_Orders = [t for t in Local_Recent_Orders if t.get('timestamp_arrival', 0) >= lookback_min_ts_ms]
|
||||
|
||||
VAL_KEY_OBJ = json.dumps(LOCAL_RECENT_ORDERS)
|
||||
VAL_KEY.set(VK_ORDERS_TRADES, VAL_KEY_OBJ)
|
||||
VAL_KEY_OBJ: str = json.dumps(obj=Local_Recent_Orders)
|
||||
VAL_KEY.set(name=VK_ORDERS_TRADES, value=VAL_KEY_OBJ)
|
||||
|
||||
await db.insert_df_to_mysql(table_name='fr_aster_user_order_trade', params=new_order_update, CON=CON)
|
||||
continue
|
||||
@@ -153,7 +152,7 @@ async def ws_stream():
|
||||
# logging.info(f'MARGIN_CALL: {data}')
|
||||
list_for_df = []
|
||||
for p in list(data['p']):
|
||||
margin_call_update = {
|
||||
margin_call_update: dict = {
|
||||
'timestamp_arrival': ts_arrival,
|
||||
'timestamp_msg': data['E'],
|
||||
'cross_wallet_balance': float(data.get('cw', 0)),
|
||||
@@ -168,11 +167,11 @@ async def ws_stream():
|
||||
'maint_margin_required': float(p["mm"]), # :"1.614445" // Maintenance Margin Required
|
||||
}
|
||||
list_for_df.append(margin_call_update)
|
||||
LOCAL_RECENT_MARGIN_CALLS = utils.upsert_list_of_dicts_by_id(LOCAL_RECENT_MARGIN_CALLS, margin_call_update, id='symbol', seq_check_field='timestamp_msg')
|
||||
LOCAL_RECENT_MARGIN_CALLS = [t for t in LOCAL_RECENT_MARGIN_CALLS if t.get('timestamp_arrival', 0) >= LOOKBACK_MIN_TS_MS]
|
||||
Local_Recent_Margin_Calls = utils.upsert_list_of_dicts_by_id(Local_Recent_Margin_Calls, margin_call_update, id='symbol', seq_check_field='timestamp_msg')
|
||||
Local_Recent_Margin_Calls = [t for t in Local_Recent_Margin_Calls if t.get('timestamp_arrival', 0) >= lookback_min_ts_ms]
|
||||
|
||||
VAL_KEY_OBJ = json.dumps(LOCAL_RECENT_MARGIN_CALLS)
|
||||
VAL_KEY.set(VK_MARGIN_CALLS, VAL_KEY_OBJ)
|
||||
VAL_KEY_OBJ: str = json.dumps(obj=Local_Recent_Margin_Calls)
|
||||
VAL_KEY.set(name=VK_MARGIN_CALLS, value=VAL_KEY_OBJ)
|
||||
|
||||
await db.insert_df_to_mysql(table_name='fr_aster_user_margin', params=list_for_df, CON=CON)
|
||||
continue
|
||||
@@ -183,7 +182,7 @@ async def ws_stream():
|
||||
### Balance Updates ###
|
||||
if len(list(data['a']['B'])) > 0:
|
||||
for b in list(data['a']['B']):
|
||||
balance_update = {
|
||||
balance_update: dict = {
|
||||
'timestamp_arrival': ts_arrival,
|
||||
'timestamp_msg': data['E'],
|
||||
'timestamp_transaction': data['T'],
|
||||
@@ -196,13 +195,13 @@ async def ws_stream():
|
||||
'balance_change_excl_pnl_comms': float(b['bc']),
|
||||
}
|
||||
list_for_df_bal.append(balance_update)
|
||||
LOCAL_RECENT_BALANCES = utils.upsert_list_of_dicts_by_id(LOCAL_RECENT_BALANCES, balance_update, id='asset', seq_check_field='timestamp_msg')
|
||||
LOCAL_RECENT_BALANCES = [t for t in LOCAL_RECENT_BALANCES if t.get('timestamp_arrival', 0) >= LOOKBACK_MIN_TS_MS]
|
||||
VAL_KEY.set(VK_BALANCES, json.dumps(LOCAL_RECENT_BALANCES))
|
||||
Local_Recent_Balances = utils.upsert_list_of_dicts_by_id(Local_Recent_Balances, balance_update, id='asset', seq_check_field='timestamp_msg')
|
||||
Local_Recent_Balances = [t for t in Local_Recent_Balances if t.get('timestamp_arrival', 0) >= lookback_min_ts_ms]
|
||||
VAL_KEY.set(name=VK_BALANCES, value=json.dumps(obj=Local_Recent_Balances))
|
||||
### Position Updates ###
|
||||
if len(list(data['a']['P'])) > 0:
|
||||
for p in list(data['a']['P']):
|
||||
position_update = {
|
||||
position_update: dict = {
|
||||
'timestamp_arrival': ts_arrival,
|
||||
'timestamp_msg': data['E'],
|
||||
'timestamp_transaction': data['T'],
|
||||
@@ -219,33 +218,34 @@ async def ws_stream():
|
||||
'position_side': p['ps'],
|
||||
}
|
||||
list_for_df_pos.append(position_update)
|
||||
LOCAL_RECENT_POSITIONS = utils.upsert_list_of_dicts_by_id(LOCAL_RECENT_POSITIONS, position_update, id='symbol', seq_check_field='timestamp_msg')
|
||||
LOCAL_RECENT_POSITIONS = [t for t in LOCAL_RECENT_POSITIONS if t.get('timestamp_arrival', 0) >= LOOKBACK_MIN_TS_MS]
|
||||
VAL_KEY.set(VK_POSITIONS, json.dumps(LOCAL_RECENT_POSITIONS))
|
||||
Local_Recent_Positions = utils.upsert_list_of_dicts_by_id(Local_Recent_Positions, position_update, id='symbol', seq_check_field='timestamp_msg')
|
||||
Local_Recent_Positions = [t for t in Local_Recent_Positions if t.get('timestamp_arrival', 0) >= lookback_min_ts_ms]
|
||||
VAL_KEY.set(name=VK_POSITIONS, value=json.dumps(obj=Local_Recent_Positions))
|
||||
if list_for_df_bal:
|
||||
await db.insert_df_to_mysql(table_name='fr_aster_user_account_bal', params=list_for_df_bal, CON=CON)
|
||||
if list_for_df_pos:
|
||||
await db.insert_df_to_mysql(table_name='fr_aster_user_account_pos', params=list_for_df_pos, CON=CON)
|
||||
continue
|
||||
case 'listenKeyExpired':
|
||||
raise('Listen Key Has Expired; Failed to Update Properly. Restarting.')
|
||||
raise ValueError('Listen Key Has Expired; Failed to Update Properly. Restarting.')
|
||||
case _:
|
||||
logging.warning(f'UNMATCHED OTHER MSG: {data}')
|
||||
logging.warning(msg=f'UNMATCHED OTHER MSG: {data}')
|
||||
else:
|
||||
logging.info(f'Initial or unexpected data struct, skipping: {data}')
|
||||
logging.info(msg=f'Initial or unexpected data struct, skipping: {data}')
|
||||
continue
|
||||
except (json.JSONDecodeError, ValueError):
|
||||
logging.warning(f'Message not in JSON format, skipping: {message}')
|
||||
logging.warning(msg=f'Message not in JSON format, skipping: {message}')
|
||||
continue
|
||||
else:
|
||||
raise ValueError(f'Type: {type(data)} not expected: {message}')
|
||||
except websockets.ConnectionClosed as e:
|
||||
logging.error(f'Connection closed: {e}')
|
||||
logging.error(traceback.format_exc())
|
||||
continue
|
||||
logging.error(msg=f'Connection closed: {e}')
|
||||
logging.error(msg=traceback.format_exc())
|
||||
utils.send_tg_alert(msg=f'WS_Aster_User - Failure: {e}')
|
||||
except Exception as e:
|
||||
logging.error(f'Connection closed: {e}')
|
||||
logging.error(traceback.format_exc())
|
||||
logging.error(msg=f'Connection closed: {e}')
|
||||
logging.error(msg=traceback.format_exc())
|
||||
utils.send_tg_alert(msg=f'WS_Aster_User - Failure: {e}')
|
||||
|
||||
|
||||
async def main():
|
||||
@@ -255,8 +255,8 @@ async def main():
|
||||
if USE_VK:
|
||||
VAL_KEY = valkey.Valkey(host='localhost', port=6379, db=0)
|
||||
else:
|
||||
VAL_KEY = None
|
||||
logging.warning("VALKEY NOT BEING USED, NO DATA WILL BE PUBLISHED")
|
||||
raise NotImplementedError('Cannot run without Valkey')
|
||||
|
||||
if USE_DB:
|
||||
engine = create_async_engine('mysql+asyncmy://root:pwd@localhost/fund_rate')
|
||||
@@ -267,15 +267,14 @@ async def main():
|
||||
await aster_db.create_fr_aster_user_account_pos(CON=CON)
|
||||
await ws_stream()
|
||||
else:
|
||||
CON = None
|
||||
logging.warning("DATABASE NOT BEING USED, NO DATA WILL BE RECORDED")
|
||||
await ws_stream()
|
||||
raise NotImplementedError('Cannot run without DB')
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
START_TIME = round(datetime.now().timestamp()*1000)
|
||||
START_TIME: int = round(number=datetime.now().timestamp()*1000)
|
||||
|
||||
logging.info(f'Log FilePath: {LOG_FILEPATH}')
|
||||
logging.info(msg=f'Log FilePath: {LOG_FILEPATH}')
|
||||
|
||||
logging.basicConfig(
|
||||
force=True,
|
||||
@@ -284,9 +283,9 @@ if __name__ == '__main__':
|
||||
format='%(asctime)s - %(levelname)s - %(message)s',
|
||||
filemode='w'
|
||||
)
|
||||
logging.info(f"STARTED: {START_TIME}")
|
||||
logging.info(msg=f"STARTED: {START_TIME}")
|
||||
|
||||
try:
|
||||
asyncio.run(main())
|
||||
except KeyboardInterrupt:
|
||||
logging.info("Stream stopped")
|
||||
logging.info(msg="Stream stopped")
|
||||
@@ -28,23 +28,18 @@ USE_DB: bool = False
|
||||
USE_VK: bool = True
|
||||
VK_FUND_RATE = 'fund_rate_extended'
|
||||
|
||||
CON: AsyncContextManager | None = None
|
||||
VAL_KEY = None
|
||||
CON: AsyncContextManager
|
||||
VAL_KEY: valkey.Valkey
|
||||
|
||||
### Logging ###
|
||||
load_dotenv()
|
||||
LOG_FILEPATH: str = os.getenv("LOGS_PATH") + '/Fund_Rate_Extended_FR.log'
|
||||
LOG_FILEPATH: str = f'{os.getenv("LOGS_PATH")}/Fund_Rate_Extended_FR.log'
|
||||
|
||||
### CONSTANTS ###
|
||||
WS_SYMBOL: str = 'ETH-USD'
|
||||
FUNDING_RATE_INTERVAL_MIN = 60
|
||||
SYMBOL: str = 'ETH-USD'
|
||||
|
||||
### Globals ###
|
||||
WSS_URL = f"wss://api.starknet.extended.exchange/stream.extended.exchange/v1/funding/{WS_SYMBOL}"
|
||||
|
||||
|
||||
# HIST_TRADES = np.empty((0, 3))
|
||||
# HIST_TRADES_LOOKBACK_SEC = 6
|
||||
ALLOW_SYMBOL_CHG: bool = False
|
||||
|
||||
def time_round_down(dt, interval_mins=5) -> int: # returns timestamp in seconds
|
||||
interval_secs = interval_mins * 60
|
||||
@@ -53,113 +48,63 @@ def time_round_down(dt, interval_mins=5) -> int: # returns timestamp in seconds
|
||||
|
||||
return rounded_seconds
|
||||
|
||||
|
||||
# ### Database Funcs ###
|
||||
# async def create_rtds_btcusd_table(
|
||||
# CON: AsyncContextManager,
|
||||
# engine: str = 'mysql', # mysql | duckdb
|
||||
# ) -> None:
|
||||
# if CON is None:
|
||||
# logging.info("NO DB CONNECTION, SKIPPING Create Statements")
|
||||
# else:
|
||||
# if engine == 'mysql':
|
||||
# logging.info('Creating Table if Does Not Exist: binance_btcusd_trades')
|
||||
# await CON.execute(text("""
|
||||
# CREATE TABLE IF NOT EXISTS binance_btcusd_trades (
|
||||
# timestamp_arrival BIGINT,
|
||||
# timestamp_msg BIGINT,
|
||||
# timestamp_value BIGINT,
|
||||
# value DOUBLE,
|
||||
# qty DOUBLE
|
||||
# );
|
||||
# """))
|
||||
# await CON.commit()
|
||||
# else:
|
||||
# raise ValueError('Only MySQL engine is implemented')
|
||||
|
||||
# async def insert_rtds_btcusd_table(
|
||||
# timestamp_arrival: int,
|
||||
# timestamp_msg: int,
|
||||
# timestamp_value: int,
|
||||
# value: float,
|
||||
# qty: float,
|
||||
# CON: AsyncContextManager,
|
||||
# engine: str = 'mysql', # mysql | duckdb
|
||||
# ) -> None:
|
||||
# params={
|
||||
# 'timestamp_arrival': timestamp_arrival,
|
||||
# 'timestamp_msg': timestamp_msg,
|
||||
# 'timestamp_value': timestamp_value,
|
||||
# 'value': value,
|
||||
# 'qty': qty,
|
||||
# }
|
||||
# if CON is None:
|
||||
# logging.info("NO DB CONNECTION, SKIPPING Insert Statements")
|
||||
# else:
|
||||
# if engine == 'mysql':
|
||||
# await CON.execute(text("""
|
||||
# INSERT INTO binance_btcusd_trades
|
||||
# (
|
||||
# timestamp_arrival,
|
||||
# timestamp_msg,
|
||||
# timestamp_value,
|
||||
# value,
|
||||
# qty
|
||||
# )
|
||||
# VALUES
|
||||
# (
|
||||
# :timestamp_arrival,
|
||||
# :timestamp_msg,
|
||||
# :timestamp_value,
|
||||
# :value,
|
||||
# :qty
|
||||
# )
|
||||
# """),
|
||||
# parameters=params
|
||||
# )
|
||||
# await CON.commit()
|
||||
# else:
|
||||
# raise ValueError('Only MySQL engine is implemented')
|
||||
|
||||
### Websocket ###
|
||||
async def ws_stream():
|
||||
async for websocket in websockets.connect(WSS_URL):
|
||||
logging.info(f"Connected to {WSS_URL}")
|
||||
try:
|
||||
async for message in websocket:
|
||||
ts_arrival = round(datetime.now().timestamp()*1000)
|
||||
if isinstance(message, str):
|
||||
try:
|
||||
data = json.loads(message)
|
||||
if data.get('data', None) is not None:
|
||||
# print(f'FR: {data}')
|
||||
fr_next_update_ts = (time_round_down(dt=datetime.now(timezone.utc), interval_mins=60)+(60*60))*1000
|
||||
VAL_KEY_OBJ = json.dumps({
|
||||
'sequence_id': data['seq'],
|
||||
'timestamp_arrival': ts_arrival,
|
||||
'timestamp_msg': data['ts'],
|
||||
'symbol': data['data']['m'],
|
||||
'funding_rate': float(data['data']['f']),
|
||||
'funding_rate_updated_ts_ms': data['data']['T'],
|
||||
'next_funding_time_ts_ms': fr_next_update_ts,
|
||||
})
|
||||
VAL_KEY.set(VK_FUND_RATE, VAL_KEY_OBJ)
|
||||
global SYMBOL
|
||||
|
||||
while True:
|
||||
CHANGE_SYMBOL = False
|
||||
WSS_URL = f"wss://api.starknet.extended.exchange/stream.extended.exchange/v1/funding/{SYMBOL}"
|
||||
async for websocket in websockets.connect(WSS_URL):
|
||||
if CHANGE_SYMBOL:
|
||||
break
|
||||
logging.info(f"Connected to {WSS_URL}")
|
||||
try:
|
||||
async for message in websocket:
|
||||
### Update Symbol if Algo Outputs Change ###
|
||||
if ALLOW_SYMBOL_CHG:
|
||||
best_symbol_by_exchange: dict = json.loads(s=VAL_KEY.get(name='fr_algo_working_symbol')) # ty:ignore[invalid-argument-type]
|
||||
best_symbol: str = f'{best_symbol_by_exchange['EXTEND']['lh_asset']}-{best_symbol_by_exchange['EXTEND']['rh_asset']}'
|
||||
if best_symbol != SYMBOL:
|
||||
logging.info(f'Symbol Change: {SYMBOL} -> {best_symbol}')
|
||||
SYMBOL = best_symbol
|
||||
CHANGE_SYMBOL = True
|
||||
await websocket.close()
|
||||
break
|
||||
|
||||
ts_arrival = round(datetime.now().timestamp()*1000)
|
||||
if isinstance(message, str):
|
||||
try:
|
||||
data = json.loads(message)
|
||||
if data.get('data', None) is not None:
|
||||
# print(f'FR: {data}')
|
||||
fr_next_update_ts = (time_round_down(dt=datetime.now(timezone.utc), interval_mins=60)+(60*60))*1000
|
||||
VAL_KEY_OBJ = json.dumps({
|
||||
'sequence_id': data['seq'],
|
||||
'timestamp_arrival': ts_arrival,
|
||||
'timestamp_msg': data['ts'],
|
||||
'symbol': data['data']['m'],
|
||||
'funding_rate': float(data['data']['f']),
|
||||
'funding_rate_updated_ts_ms': data['data']['T'],
|
||||
'next_funding_time_ts_ms': fr_next_update_ts,
|
||||
})
|
||||
VAL_KEY.set(VK_FUND_RATE, VAL_KEY_OBJ)
|
||||
continue
|
||||
else:
|
||||
logging.info(f'Initial or unexpected data struct, skipping: {data}')
|
||||
continue
|
||||
except (json.JSONDecodeError, ValueError):
|
||||
logging.warning(f'Message not in JSON format, skipping: {message}')
|
||||
continue
|
||||
else:
|
||||
logging.info(f'Initial or unexpected data struct, skipping: {data}')
|
||||
continue
|
||||
except (json.JSONDecodeError, ValueError):
|
||||
logging.warning(f'Message not in JSON format, skipping: {message}')
|
||||
continue
|
||||
else:
|
||||
raise ValueError(f'Type: {type(data)} not expected: {message}')
|
||||
except websockets.ConnectionClosed as e:
|
||||
logging.error(f'Connection closed: {e}')
|
||||
logging.error(traceback.format_exc())
|
||||
continue
|
||||
except Exception as e:
|
||||
logging.error(f'Connection closed: {e}')
|
||||
logging.error(traceback.format_exc())
|
||||
else:
|
||||
raise ValueError(f'Type: {type(data)} not expected: {message}')
|
||||
except websockets.ConnectionClosed as e:
|
||||
logging.error(f'Connection closed: {e}')
|
||||
logging.error(traceback.format_exc())
|
||||
continue
|
||||
except Exception as e:
|
||||
logging.error(f'Connection closed: {e}')
|
||||
logging.error(traceback.format_exc())
|
||||
|
||||
|
||||
async def main():
|
||||
@@ -169,16 +114,16 @@ async def main():
|
||||
if USE_VK:
|
||||
VAL_KEY = valkey.Valkey(host='localhost', port=6379, db=0)
|
||||
else:
|
||||
VAL_KEY = None
|
||||
logging.warning("VALKEY NOT BEING USED, NO DATA WILL BE PUBLISHED")
|
||||
raise NotImplementedError('Cannot run without VK')
|
||||
|
||||
if USE_DB:
|
||||
engine = create_async_engine('mysql+asyncmy://root:pwd@localhost/fund_rate')
|
||||
async with engine.connect() as CON:
|
||||
# await create_rtds_btcusd_table(CON=CON)
|
||||
await ws_stream()
|
||||
raise NotImplementedError('DB not implemented')
|
||||
# engine = create_async_engine('mysql+asyncmy://root:pwd@localhost/fund_rate')
|
||||
# async with engine.connect() as CON:
|
||||
# # await create_rtds_btcusd_table(CON=CON)
|
||||
# await ws_stream()
|
||||
else:
|
||||
CON = None
|
||||
logging.warning("DATABASE NOT BEING USED, NO DATA WILL BE RECORDED")
|
||||
await ws_stream()
|
||||
|
||||
|
||||
@@ -27,58 +27,76 @@ USE_DB: bool = False
|
||||
USE_VK: bool = True
|
||||
|
||||
VK_TICKER = 'fut_ticker_extended'
|
||||
CON: AsyncContextManager | None = None
|
||||
VAL_KEY = None
|
||||
CON: AsyncContextManager
|
||||
VAL_KEY: valkey.Valkey
|
||||
|
||||
### Logging ###
|
||||
load_dotenv()
|
||||
LOG_FILEPATH: str = os.getenv("LOGS_PATH") + '/Fund_Rate_Extended_OB.log'
|
||||
LOG_FILEPATH: str = f'{os.getenv("LOGS_PATH")}/Fund_Rate_Extended_OB.log'
|
||||
|
||||
### CONSTANTS ###
|
||||
WS_SYMBOL: str = 'ETH-USD'
|
||||
SYMBOL: str = 'ETH-USD'
|
||||
|
||||
### Globals ###
|
||||
WSS_URL = f"wss://api.starknet.extended.exchange/stream.extended.exchange/v1/orderbooks/{WS_SYMBOL}?depth=1"
|
||||
ALLOW_SYMBOL_CHG: bool = False
|
||||
|
||||
### Websocket ###
|
||||
async def ws_stream():
|
||||
async for websocket in websockets.connect(WSS_URL):
|
||||
logging.info(f"Connected to {WSS_URL}")
|
||||
try:
|
||||
async for message in websocket:
|
||||
ts_arrival = round(datetime.now().timestamp()*1000)
|
||||
if isinstance(message, str):
|
||||
try:
|
||||
data = json.loads(message)
|
||||
if data.get('type', None) is not None:
|
||||
# print(f'OB: {data}')
|
||||
VAL_KEY_OBJ = json.dumps({
|
||||
'sequence_id': data['seq'],
|
||||
'timestamp_arrival': ts_arrival,
|
||||
'timestamp_msg': data['ts'],
|
||||
'symbol': data['data']['m'],
|
||||
'best_bid_px': float(data['data']['b'][0]['p']),
|
||||
'best_bid_qty': float(data['data']['b'][0]['q']),
|
||||
'best_ask_px': float(data['data']['a'][0]['p']),
|
||||
'best_ask_qty': float(data['data']['a'][0]['q']),
|
||||
})
|
||||
VAL_KEY.set(VK_TICKER, VAL_KEY_OBJ)
|
||||
global SYMBOL
|
||||
|
||||
while True:
|
||||
CHANGE_SYMBOL = False
|
||||
WSS_URL = f"wss://api.starknet.extended.exchange/stream.extended.exchange/v1/orderbooks/{SYMBOL}?depth=1"
|
||||
async for websocket in websockets.connect(WSS_URL):
|
||||
if CHANGE_SYMBOL:
|
||||
break
|
||||
logging.info(f"Connected to {WSS_URL}")
|
||||
try:
|
||||
async for message in websocket:
|
||||
### Update Symbol if Algo Outputs Change ###
|
||||
if ALLOW_SYMBOL_CHG:
|
||||
best_symbol_by_exchange: dict = json.loads(s=VAL_KEY.get(name='fr_algo_working_symbol')) # ty:ignore[invalid-argument-type]
|
||||
best_symbol: str = f'{best_symbol_by_exchange['EXTEND']['lh_asset']}-{best_symbol_by_exchange['EXTEND']['rh_asset']}'
|
||||
if best_symbol != SYMBOL:
|
||||
logging.info(f'Symbol Change: {SYMBOL} -> {best_symbol}')
|
||||
SYMBOL = best_symbol
|
||||
CHANGE_SYMBOL = True
|
||||
await websocket.close()
|
||||
break
|
||||
|
||||
ts_arrival = round(datetime.now().timestamp()*1000)
|
||||
if isinstance(message, str):
|
||||
try:
|
||||
data = json.loads(message)
|
||||
if data.get('type', None) is not None:
|
||||
# print(f'OB: {data}')
|
||||
VAL_KEY_OBJ = json.dumps({
|
||||
'sequence_id': data['seq'],
|
||||
'timestamp_arrival': ts_arrival,
|
||||
'timestamp_msg': data['ts'],
|
||||
'symbol': data['data']['m'],
|
||||
'best_bid_px': float(data['data']['b'][0]['p']),
|
||||
'best_bid_qty': float(data['data']['b'][0]['q']),
|
||||
'best_ask_px': float(data['data']['a'][0]['p']),
|
||||
'best_ask_qty': float(data['data']['a'][0]['q']),
|
||||
})
|
||||
VAL_KEY.set(VK_TICKER, VAL_KEY_OBJ)
|
||||
continue
|
||||
else:
|
||||
logging.info(f'Initial or unexpected data struct, skipping: {data}')
|
||||
continue
|
||||
except (json.JSONDecodeError, ValueError):
|
||||
logging.warning(f'Message not in JSON format, skipping: {message}')
|
||||
continue
|
||||
else:
|
||||
logging.info(f'Initial or unexpected data struct, skipping: {data}')
|
||||
continue
|
||||
except (json.JSONDecodeError, ValueError):
|
||||
logging.warning(f'Message not in JSON format, skipping: {message}')
|
||||
continue
|
||||
else:
|
||||
raise ValueError(f'Type: {type(data)} not expected: {message}')
|
||||
except websockets.ConnectionClosed as e:
|
||||
logging.error(f'Connection closed: {e}')
|
||||
logging.error(traceback.format_exc())
|
||||
continue
|
||||
except Exception as e:
|
||||
logging.error(f'Connection closed: {e}')
|
||||
logging.error(traceback.format_exc())
|
||||
else:
|
||||
raise ValueError(f'Type: {type(data)} not expected: {message}')
|
||||
except websockets.ConnectionClosed as e:
|
||||
logging.error(f'Connection closed: {e}')
|
||||
logging.error(traceback.format_exc())
|
||||
continue
|
||||
except Exception as e:
|
||||
logging.error(f'Connection closed: {e}')
|
||||
logging.error(traceback.format_exc())
|
||||
|
||||
|
||||
async def main():
|
||||
@@ -88,16 +106,16 @@ async def main():
|
||||
if USE_VK:
|
||||
VAL_KEY = valkey.Valkey(host='localhost', port=6379, db=0)
|
||||
else:
|
||||
VAL_KEY = None
|
||||
logging.warning("VALKEY NOT BEING USED, NO DATA WILL BE PUBLISHED")
|
||||
raise NotImplementedError('Cannot run without VK')
|
||||
|
||||
if USE_DB:
|
||||
engine = create_async_engine('mysql+asyncmy://root:pwd@localhost/fund_rate')
|
||||
async with engine.connect() as CON:
|
||||
# await create_rtds_btcusd_table(CON=CON)
|
||||
await ws_stream()
|
||||
raise NotImplementedError('DB not implemented')
|
||||
# engine = create_async_engine('mysql+asyncmy://root:pwd@localhost/fund_rate')
|
||||
# async with engine.connect() as CON:
|
||||
# # await create_rtds_btcusd_table(CON=CON)
|
||||
# await ws_stream()
|
||||
else:
|
||||
CON = None
|
||||
logging.warning("DATABASE NOT BEING USED, NO DATA WILL BE RECORDED")
|
||||
await ws_stream()
|
||||
|
||||
|
||||
@@ -29,68 +29,88 @@ USE_DB: bool = True
|
||||
USE_VK: bool = True
|
||||
|
||||
VK_LAST_TRADE = 'fut_last_trade_extended'
|
||||
CON: AsyncContextManager | None = None
|
||||
VAL_KEY = None
|
||||
CON: AsyncContextManager
|
||||
VAL_KEY: valkey.Valkey
|
||||
|
||||
### Logging ###
|
||||
load_dotenv()
|
||||
LOG_FILEPATH: str = os.getenv("LOGS_PATH") + '/Fund_Rate_Extended_Trades.log'
|
||||
LOG_FILEPATH: str = f'{os.getenv("LOGS_PATH")}/Fund_Rate_Extended_Trades.log'
|
||||
|
||||
### CONSTANTS ###
|
||||
WS_SYMBOL: str = 'ETH-USD'
|
||||
SYMBOL: str = 'ETH-USD'
|
||||
|
||||
### Globals ###
|
||||
WSS_URL = f"wss://api.starknet.extended.exchange/stream.extended.exchange/v1/publicTrades/{WS_SYMBOL}"
|
||||
ALLOW_SYMBOL_CHG: bool = False
|
||||
|
||||
|
||||
### Websocket ###
|
||||
async def ws_stream():
|
||||
async for websocket in websockets.connect(WSS_URL):
|
||||
logging.info(f"Connected to {WSS_URL}")
|
||||
try:
|
||||
async for message in websocket:
|
||||
ts_arrival = round(datetime.now().timestamp()*1000)
|
||||
if isinstance(message, str):
|
||||
try:
|
||||
data = json.loads(message)
|
||||
if data.get('data', None) is not None:
|
||||
if data['seq'] == 1: # Skip first msg that has historical trades
|
||||
global SYMBOL
|
||||
|
||||
while True:
|
||||
CHANGE_SYMBOL: bool = False
|
||||
WSS_URL = f"wss://api.starknet.extended.exchange/stream.extended.exchange/v1/publicTrades/{SYMBOL}"
|
||||
async for websocket in websockets.connect(WSS_URL):
|
||||
if CHANGE_SYMBOL:
|
||||
break
|
||||
logging.info(f"Connected to {WSS_URL}")
|
||||
try:
|
||||
async for message in websocket:
|
||||
### Update Symbol if Algo Outputs Change ###
|
||||
if ALLOW_SYMBOL_CHG:
|
||||
best_symbol_by_exchange: dict = json.loads(s=VAL_KEY.get(name='fr_algo_working_symbol')) # ty:ignore[invalid-argument-type]
|
||||
best_symbol: str = f'{best_symbol_by_exchange['EXTEND']['lh_asset']}-{best_symbol_by_exchange['EXTEND']['rh_asset']}'
|
||||
if best_symbol != SYMBOL:
|
||||
logging.info(f'Symbol Change: {SYMBOL} -> {best_symbol}')
|
||||
SYMBOL = best_symbol
|
||||
CHANGE_SYMBOL = True
|
||||
await websocket.close()
|
||||
break
|
||||
|
||||
ts_arrival = round(datetime.now().timestamp()*1000)
|
||||
if isinstance(message, str):
|
||||
try:
|
||||
data = json.loads(message)
|
||||
if data.get('data', None) is not None:
|
||||
# print(data)
|
||||
if data['seq'] == 1: # Skip first msg that has historical trades
|
||||
continue
|
||||
list_for_df = []
|
||||
for t in data['data']:
|
||||
trade_obj = {
|
||||
'sequence_id': data['seq'],
|
||||
'timestamp_arrival': ts_arrival,
|
||||
'timestamp_msg': data['ts'],
|
||||
'timestamp_trade': t['T'],
|
||||
'symbol': t['m'],
|
||||
'side_taker': t['S'],
|
||||
'trade_type': t['tT'],
|
||||
'price': float(t['p']),
|
||||
'qty': float(t['q']),
|
||||
'trade_id': t['i'],
|
||||
'is_buyer_mkt_maker': True if t['S']=='SELL' else False,
|
||||
}
|
||||
list_for_df.append(trade_obj)
|
||||
# VAL_KEY.set(VK_LAST_TRADE, json.dumps(trade_obj))
|
||||
if USE_DB:
|
||||
await db.insert_df_to_mysql(table_name='fr_extended_mkt_trades', params=list_for_df, CON=CON)
|
||||
pass
|
||||
continue
|
||||
list_for_df = []
|
||||
for t in data['data']:
|
||||
trade_obj = {
|
||||
'sequence_id': data['seq'],
|
||||
'timestamp_arrival': ts_arrival,
|
||||
'timestamp_msg': data['ts'],
|
||||
'timestamp_trade': t['T'],
|
||||
'symbol': t['m'],
|
||||
'side_taker': t['S'],
|
||||
'trade_type': t['tT'],
|
||||
'price': float(t['p']),
|
||||
'qty': float(t['q']),
|
||||
'trade_id': t['i'],
|
||||
'is_buyer_mkt_maker': True if t['S']=='SELL' else False,
|
||||
}
|
||||
list_for_df.append(trade_obj)
|
||||
# VAL_KEY.set(VK_LAST_TRADE, json.dumps(trade_obj))
|
||||
if USE_DB:
|
||||
await db.insert_df_to_mysql(table_name='fr_extended_mkt_trades', params=list_for_df, CON=CON)
|
||||
pass
|
||||
else:
|
||||
logging.info(f'Initial or unexpected data struct, skipping: {data}')
|
||||
continue
|
||||
except (json.JSONDecodeError, ValueError):
|
||||
logging.warning(f'Message not in JSON format, skipping: {message}')
|
||||
continue
|
||||
else:
|
||||
logging.info(f'Initial or unexpected data struct, skipping: {data}')
|
||||
continue
|
||||
except (json.JSONDecodeError, ValueError):
|
||||
logging.warning(f'Message not in JSON format, skipping: {message}')
|
||||
continue
|
||||
else:
|
||||
raise ValueError(f'Type: {type(data)} not expected: {message}')
|
||||
except websockets.ConnectionClosed as e:
|
||||
logging.error(f'Connection closed: {e}')
|
||||
logging.error(traceback.format_exc())
|
||||
continue
|
||||
except Exception as e:
|
||||
logging.error(f'Connection closed: {e}')
|
||||
logging.error(traceback.format_exc())
|
||||
else:
|
||||
raise ValueError(f'Type: {type(data)} not expected: {message}')
|
||||
except websockets.ConnectionClosed as e:
|
||||
logging.error(f'Connection closed: {e}')
|
||||
logging.error(traceback.format_exc())
|
||||
continue
|
||||
except Exception as e:
|
||||
logging.error(f'Connection closed: {e}')
|
||||
logging.error(traceback.format_exc())
|
||||
|
||||
|
||||
async def main():
|
||||
@@ -100,8 +120,8 @@ async def main():
|
||||
if USE_VK:
|
||||
VAL_KEY = valkey.Valkey(host='localhost', port=6379, db=0)
|
||||
else:
|
||||
VAL_KEY = None
|
||||
logging.warning("VALKEY NOT BEING USED, NO DATA WILL BE PUBLISHED")
|
||||
raise NotImplementedError('Cannot run without VK')
|
||||
|
||||
if USE_DB:
|
||||
engine = create_async_engine('mysql+asyncmy://root:pwd@localhost/fund_rate')
|
||||
@@ -109,9 +129,9 @@ async def main():
|
||||
await extended_db.create_fr_extended_mkt_trades(CON=CON)
|
||||
await ws_stream()
|
||||
else:
|
||||
CON = None
|
||||
logging.warning("DATABASE NOT BEING USED, NO DATA WILL BE RECORDED")
|
||||
await ws_stream()
|
||||
raise NotImplementedError('Cannot run without DB')
|
||||
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
|
||||
@@ -32,16 +32,16 @@ VK_ORDERS = 'fr_extended_user_orders'
|
||||
VK_TRADES = 'fr_extended_user_trades'
|
||||
VK_BALANCES = 'fr_extended_user_balances'
|
||||
VK_POSITIONS = 'fr_extended_user_positions'
|
||||
CON: AsyncContextManager | None = None
|
||||
VAL_KEY = None
|
||||
CON: AsyncContextManager
|
||||
VAL_KEY: valkey.Valkey
|
||||
|
||||
### Logging ###
|
||||
load_dotenv()
|
||||
LOG_FILEPATH: str = os.getenv("LOGS_PATH") + '/Fund_Rate_Extended_User.log'
|
||||
LOG_FILEPATH: str = f'{os.getenv("LOGS_PATH")}/Fund_Rate_Extended_User.log'
|
||||
|
||||
### CONSTANTS ###
|
||||
WSS_URL = "wss://api.starknet.extended.exchange/stream.extended.exchange/v1/account"
|
||||
API_KEY = os.getenv('EXTENDED_API_KEY')
|
||||
API_KEY: str = os.getenv('EXTENDED_API_KEY') # ty:ignore[invalid-assignment]
|
||||
LOCAL_RECENT_UPDATES_LOOKBACK_SEC = 30
|
||||
|
||||
### Globals ###
|
||||
@@ -226,8 +226,8 @@ async def main():
|
||||
if USE_VK:
|
||||
VAL_KEY = valkey.Valkey(host='localhost', port=6379, db=0)
|
||||
else:
|
||||
VAL_KEY = None
|
||||
logging.warning("VALKEY NOT BEING USED, NO DATA WILL BE PUBLISHED")
|
||||
raise NotImplementedError('Cannot run without VK')
|
||||
|
||||
if USE_DB:
|
||||
engine = create_async_engine('mysql+asyncmy://root:pwd@localhost/fund_rate')
|
||||
@@ -238,9 +238,9 @@ async def main():
|
||||
await extended_db.create_fr_extended_user_trade(CON=CON)
|
||||
await ws_stream()
|
||||
else:
|
||||
CON = None
|
||||
logging.warning("DATABASE NOT BEING USED, NO DATA WILL BE RECORDED")
|
||||
await ws_stream()
|
||||
raise NotImplementedError('Cannot run without DB')
|
||||
# await ws_stream()
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
|
||||
Reference in New Issue
Block a user