Compare commits
10 Commits
64266366dd
...
japan_link
| Author | SHA1 | Date | |
|---|---|---|---|
| 73c4eb1bf8 | |||
| dac490ca5b | |||
| b0d031d452 | |||
| afa2d1fd79 | |||
| ea46b173fa | |||
| c12835a96d | |||
| 73c9ed67f5 | |||
| ff209603b6 | |||
| 539e6004cf | |||
| 408a63fe58 |
@@ -218,7 +218,7 @@ async def main():
|
||||
logging.warning("VALKEY NOT BEING USED, NO DATA WILL BE PUBLISHED")
|
||||
|
||||
if USE_DB:
|
||||
engine = create_async_engine('mysql+asyncmy://root:pwd@localhost/polymarket')
|
||||
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()
|
||||
145
algo.ipynb
Normal file
145
algo.ipynb
Normal file
@@ -0,0 +1,145 @@
|
||||
{
|
||||
"cells": [
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 1,
|
||||
"id": "d1eed397",
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"import modules.structs as structs\n",
|
||||
"import json\n",
|
||||
"from dataclasses import dataclass, asdict\n",
|
||||
"import valkey\n",
|
||||
"\n",
|
||||
"with open('algo_config.json', 'r', encoding='utf-8') as file:\n",
|
||||
" ALGO_CONFIG = json.load(file, object_hook=lambda d: structs.Algo_Config(**d))"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 2,
|
||||
"id": "c6151613",
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"VAL_KEY = valkey.Valkey(host='localhost', port=6379, db=0, decode_responses=True)"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 3,
|
||||
"id": "d83c61e5",
|
||||
"metadata": {},
|
||||
"outputs": [
|
||||
{
|
||||
"data": {
|
||||
"text/plain": [
|
||||
"1"
|
||||
]
|
||||
},
|
||||
"execution_count": 3,
|
||||
"metadata": {},
|
||||
"output_type": "execute_result"
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"config_update = {'Min_Time_To_Funding_Minutes': 7}\n",
|
||||
"VAL_KEY.publish('fr_orchestrator_input', json.dumps(config_update))"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 11,
|
||||
"id": "45fae761",
|
||||
"metadata": {},
|
||||
"outputs": [
|
||||
{
|
||||
"data": {
|
||||
"text/plain": [
|
||||
"Algo_Config(Config_Updated_Timestamp=1777151524162, Allow_Ordering_Aster=True, Allow_Ordering_Extend=True, Loop_Sleep_Sec=1, Max_Target_Notional=0.0, Min_Time_To_Funding_Minutes=60, Price_Worsener_Aster=0.0, Price_Worsener_Extend=0.0, Target_Open_Cash_Position=10)"
|
||||
]
|
||||
},
|
||||
"execution_count": 11,
|
||||
"metadata": {},
|
||||
"output_type": "execute_result"
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"json.loads(VAL_KEY.get('fr_orchestrator_output'), object_hook=lambda d: structs.Algo_Config(**d))"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 16,
|
||||
"metadata": {},
|
||||
"outputs": [
|
||||
{
|
||||
"data": {
|
||||
"text/plain": [
|
||||
"{'Config_Updated_Timestamp': 1777098091913,\n",
|
||||
" 'Allow_Ordering_Aster': True,\n",
|
||||
" 'Allow_Ordering_Extend': True,\n",
|
||||
" 'Loop_Sleep_Sec': 1,\n",
|
||||
" 'Max_Target_Notional': 0.0,\n",
|
||||
" 'Min_Time_To_Funding_Minutes': 60,\n",
|
||||
" 'Price_Worsener_Aster': 0.0,\n",
|
||||
" 'Price_Worsener_Extend': 0.0,\n",
|
||||
" 'Target_Open_Cash_Position': 10}"
|
||||
]
|
||||
},
|
||||
"execution_count": 16,
|
||||
"metadata": {},
|
||||
"output_type": "execute_result"
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"asdict(ALGO_CONFIG)"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"id": "d2e26271",
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": []
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"id": "a0df43de",
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": []
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": []
|
||||
}
|
||||
],
|
||||
"metadata": {
|
||||
"kernelspec": {
|
||||
"display_name": "py_313",
|
||||
"language": "python",
|
||||
"name": "python3"
|
||||
},
|
||||
"language_info": {
|
||||
"codemirror_mode": {
|
||||
"name": "ipython",
|
||||
"version": 3
|
||||
},
|
||||
"file_extension": ".py",
|
||||
"mimetype": "text/x-python",
|
||||
"name": "python",
|
||||
"nbconvert_exporter": "python",
|
||||
"pygments_lexer": "ipython3",
|
||||
"version": "3.13.12"
|
||||
}
|
||||
},
|
||||
"nbformat": 4,
|
||||
"nbformat_minor": 5
|
||||
}
|
||||
19
algo/Dockerfile
Normal file
19
algo/Dockerfile
Normal file
@@ -0,0 +1,19 @@
|
||||
FROM python:3.13-slim
|
||||
|
||||
RUN apt-get update && \
|
||||
apt-get install -y build-essential
|
||||
|
||||
RUN gcc --version
|
||||
RUN rm -rf /var/lib/apt/lists/*
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
COPY requirements.txt .
|
||||
|
||||
RUN pip install --no-cache-dir -r requirements.txt
|
||||
|
||||
COPY . .
|
||||
|
||||
# Finally, run gunicorn.
|
||||
CMD [ "python", "-u" ,"main.py"]
|
||||
# CMD [ "gunicorn", "--workers=5", "--threads=1", "-b 0.0.0.0:8000", "app:server"]
|
||||
12
algo_config.json
Normal file
12
algo_config.json
Normal file
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"Config_Updated_Timestamp": 1777098091913,
|
||||
"Allow_Ordering_Aster": true,
|
||||
"Allow_Ordering_Extend": true,
|
||||
"Loop_Sleep_Sec": 1,
|
||||
"Max_Target_Notional": 0.00,
|
||||
"Min_Time_To_Funding_Minutes": 60,
|
||||
"Price_Worsener_Aster": 0.0,
|
||||
"Price_Worsener_Extend": 0.0,
|
||||
"Target_Open_Cash_Position": 10,
|
||||
"Print_Summary_Each_Loop" : false
|
||||
}
|
||||
95
algo_orchestrator.py
Normal file
95
algo_orchestrator.py
Normal file
@@ -0,0 +1,95 @@
|
||||
import asyncio
|
||||
import json
|
||||
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
|
||||
|
||||
'''
|
||||
TO DO:
|
||||
- Insert config changes into database for analysis later / general tracking
|
||||
'''
|
||||
|
||||
### Database ###
|
||||
CON: AsyncContextManager | None = None
|
||||
VAL_KEY = None
|
||||
VK_IN = 'fr_orchestrator_input'
|
||||
VK_OUT = 'fr_orchestrator_output'
|
||||
|
||||
### Logging ###
|
||||
load_dotenv()
|
||||
LOG_FILEPATH: str = os.getenv("LOGS_PATH") + '/Fund_Rate_Algo_Orchestrator.log'
|
||||
|
||||
ALGO_CONFIG: None | dict
|
||||
# ALGO_CONFIG: None | Algo_Config = None
|
||||
|
||||
async def orchestrator() -> None:
|
||||
global ALGO_CONFIG
|
||||
|
||||
try:
|
||||
VK_PUBSUB = VAL_KEY.pubsub()
|
||||
VK_PUBSUB.subscribe(VK_IN)
|
||||
|
||||
print(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']
|
||||
|
||||
for k, v in data.items():
|
||||
if ALGO_CONFIG.get(k, None) is not None:
|
||||
ALGO_CONFIG[k] = v
|
||||
|
||||
ALGO_CONFIG['Config_Updated_Timestamp'] = timestamp
|
||||
VAL_KEY.set(VK_OUT, json.dumps(ALGO_CONFIG))
|
||||
with open('algo_config.json', 'w', encoding='utf-8') as f:
|
||||
json.dump(ALGO_CONFIG, f, indent=4)
|
||||
print(f"Algo Config Updated @ {timestamp}; {data}")
|
||||
|
||||
except valkey.exceptions.ConnectionError as e:
|
||||
print(f"Could not connect to Valkey. Please check the publish server is up; {e}")
|
||||
except KeyboardInterrupt:
|
||||
logging.info('ORCHESTRATOR SHUTTING DOWN...')
|
||||
except Exception as e:
|
||||
logging.error(traceback.format_exc())
|
||||
logging.critical(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_config.json', 'r', encoding='utf-8') as f:
|
||||
# ALGO_CONFIG = json.load(f, object_hook=lambda d: Algo_Config(**d))
|
||||
ALGO_CONFIG = json.load(f)
|
||||
ALGO_CONFIG['Config_Updated_Timestamp'] = round(datetime.now().timestamp()*1000)
|
||||
|
||||
# async with engine.connect() as CON:
|
||||
await orchestrator()
|
||||
|
||||
if __name__ == '__main__':
|
||||
START_TIME = round(datetime.now().timestamp()*1000)
|
||||
|
||||
logging.info(f'Log FilePath: {LOG_FILEPATH}')
|
||||
|
||||
logging.basicConfig(
|
||||
force=True,
|
||||
filename=LOG_FILEPATH,
|
||||
level=logging.INFO,
|
||||
format='%(asctime)s - %(levelname)s - %(message)s',
|
||||
filemode='w'
|
||||
)
|
||||
logging.info(f"STARTED: {START_TIME}")
|
||||
|
||||
asyncio.run(main())
|
||||
19
algo_orchestrator/Dockerfile
Normal file
19
algo_orchestrator/Dockerfile
Normal file
@@ -0,0 +1,19 @@
|
||||
FROM python:3.13-slim
|
||||
|
||||
RUN apt-get update && \
|
||||
apt-get install -y build-essential
|
||||
|
||||
RUN gcc --version
|
||||
RUN rm -rf /var/lib/apt/lists/*
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
COPY requirements.txt .
|
||||
|
||||
RUN pip install --no-cache-dir -r requirements.txt
|
||||
|
||||
COPY . .
|
||||
|
||||
# Finally, run gunicorn.
|
||||
CMD [ "python", "-u" ,"algo_orchestrator.py"]
|
||||
# CMD [ "gunicorn", "--workers=5", "--threads=1", "-b 0.0.0.0:8000", "app:server"]
|
||||
198
apex.ipynb
198
apex.ipynb
@@ -1,198 +0,0 @@
|
||||
{
|
||||
"cells": [
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 2,
|
||||
"id": "b6c46d40",
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"import modules.apex_api as apex_api"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 4,
|
||||
"id": "7fb6d9dc",
|
||||
"metadata": {},
|
||||
"outputs": [
|
||||
{
|
||||
"name": "stdout",
|
||||
"output_type": "stream",
|
||||
"text": [
|
||||
"Authenticating...\n",
|
||||
"...Authenticated\n"
|
||||
]
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"client = apex_api.apex_create_client()"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 5,
|
||||
"id": "d5a1203a",
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"# print(\"*** POSTING ORDER ***\")\n",
|
||||
"# createOrderRes = client.create_order_v3(\n",
|
||||
"# symbol=\"ETH-USDT\", \n",
|
||||
"# side=\"BUY\",\n",
|
||||
"# type=\"LIMIT\",\n",
|
||||
"# size=\"0.01\",\n",
|
||||
"# price=\"2100\",\n",
|
||||
"# )\n",
|
||||
"# print(createOrderRes)\n"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"id": "c21254eb",
|
||||
"metadata": {},
|
||||
"outputs": [
|
||||
{
|
||||
"data": {
|
||||
"text/plain": [
|
||||
"{'data': {'totalEquityValue': '13.840000000000000000',\n",
|
||||
" 'availableBalance': '13.840000000000000000',\n",
|
||||
" 'initialMargin': '0',\n",
|
||||
" 'maintenanceMargin': '0',\n",
|
||||
" 'walletBalance': '',\n",
|
||||
" 'realizedPnl': '-5.399416243793950000',\n",
|
||||
" 'unrealizedPnl': '0.00',\n",
|
||||
" 'totalRisk': '0',\n",
|
||||
" 'totalValueWithoutDiscount': '13.840000000000000000',\n",
|
||||
" 'liabilities': '13.840000000000000000',\n",
|
||||
" 'totalAvailableBalance': '13.840000000000000000'},\n",
|
||||
" 'timeCost': 6327944}"
|
||||
]
|
||||
},
|
||||
"execution_count": 7,
|
||||
"metadata": {},
|
||||
"output_type": "execute_result"
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"client.get_account_balance_v3()"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 8,
|
||||
"id": "7cba63d4",
|
||||
"metadata": {},
|
||||
"outputs": [
|
||||
{
|
||||
"data": {
|
||||
"text/plain": [
|
||||
"{'data': [], 'timeCost': 3984811}"
|
||||
]
|
||||
},
|
||||
"execution_count": 8,
|
||||
"metadata": {},
|
||||
"output_type": "execute_result"
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"client.open_orders_v3()"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 12,
|
||||
"id": "b072c0de",
|
||||
"metadata": {},
|
||||
"outputs": [
|
||||
{
|
||||
"data": {
|
||||
"text/plain": [
|
||||
"{'timeCost': 4389124}"
|
||||
]
|
||||
},
|
||||
"execution_count": 12,
|
||||
"metadata": {},
|
||||
"output_type": "execute_result"
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"client.delete_open_orders_v3(symbol=\"ETH-USDT\")"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 11,
|
||||
"id": "5ea177f8",
|
||||
"metadata": {},
|
||||
"outputs": [
|
||||
{
|
||||
"name": "stdout",
|
||||
"output_type": "stream",
|
||||
"text": [
|
||||
"TOKEN: USDT == 13.840000000000000000\n",
|
||||
"TOKEN: USDC == 0.000000000000000000\n"
|
||||
]
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"account_and_pos = client.get_account_v3()\n",
|
||||
"for c in account_and_pos['contractWallets']:\n",
|
||||
" print(f'TOKEN: {c['token']} == {c['balance']}')"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"id": "70eb3b4f",
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": []
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": []
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"id": "fefca500",
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": []
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"id": "dc048386",
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": []
|
||||
}
|
||||
],
|
||||
"metadata": {
|
||||
"kernelspec": {
|
||||
"display_name": "py_313",
|
||||
"language": "python",
|
||||
"name": "python3"
|
||||
},
|
||||
"language_info": {
|
||||
"codemirror_mode": {
|
||||
"name": "ipython",
|
||||
"version": 3
|
||||
},
|
||||
"file_extension": ".py",
|
||||
"mimetype": "text/x-python",
|
||||
"name": "python",
|
||||
"nbconvert_exporter": "python",
|
||||
"pygments_lexer": "ipython3",
|
||||
"version": "3.13.12"
|
||||
}
|
||||
},
|
||||
"nbformat": 4,
|
||||
"nbformat_minor": 5
|
||||
}
|
||||
528
aster.ipynb
528
aster.ipynb
@@ -0,0 +1,528 @@
|
||||
{
|
||||
"cells": [
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 3,
|
||||
"id": "3a269644",
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"import modules.aster_auth as aster_auth"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 4,
|
||||
"id": "4395fabb",
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"listen_key_request = {\n",
|
||||
" \"url\": \"/fapi/v3/listenKey\",\n",
|
||||
" \"method\": \"POST\",\n",
|
||||
" \"params\": {}\n",
|
||||
"}\n",
|
||||
"cancel_all_open_orders = {\n",
|
||||
" \"url\": \"/fapi/v3/allOpenOrders\",\n",
|
||||
" \"method\": \"DELETE\",\n",
|
||||
" \"params\": {\n",
|
||||
" 'symbol': 'ETHUSDT',\n",
|
||||
" }\n",
|
||||
"}\n",
|
||||
"cancel_order = {\n",
|
||||
" \"url\": \"/fapi/v3/order\",\n",
|
||||
" \"method\": \"DELETE\",\n",
|
||||
" \"params\": {\n",
|
||||
" 'symbol': 'ETHUSDT',\n",
|
||||
" 'orderId': 17349955824,\n",
|
||||
" }\n",
|
||||
"}\n",
|
||||
"fut_acct_balances = {\n",
|
||||
" \"url\": \"/fapi/v3/balance\",\n",
|
||||
" \"method\": \"GET\",\n",
|
||||
" \"params\": {}\n",
|
||||
"}\n",
|
||||
"fut_acct_openOrders = {\n",
|
||||
" \"url\": \"/fapi/v3/openOrders\",\n",
|
||||
" \"method\": \"GET\",\n",
|
||||
" \"params\": {}\n",
|
||||
"}\n",
|
||||
"fut_acct_positionRisk = {\n",
|
||||
" \"url\": \"/fapi/v3/positionRisk\",\n",
|
||||
" \"method\": \"GET\",\n",
|
||||
" \"params\": {\n",
|
||||
" 'symbol': 'ETHUSDT',\n",
|
||||
" }\n",
|
||||
"}\n",
|
||||
"fut_acct_exchangeInfo = {\n",
|
||||
" \"url\": \"/fapi/v3/exchangeInfo\",\n",
|
||||
" \"method\": \"GET\",\n",
|
||||
" \"params\": {}\n",
|
||||
"}\n",
|
||||
"commission_rate = {\n",
|
||||
" \"url\": \"/fapi/v3/commissionRate\",\n",
|
||||
" \"method\": \"GET\",\n",
|
||||
" \"params\": {\n",
|
||||
" 'symbol': 'ETHUSDT',\n",
|
||||
" }\n",
|
||||
"}\n",
|
||||
"post_order = {\n",
|
||||
" \"url\": \"/fapi/v3/order\",\n",
|
||||
" \"method\": \"POST\",\n",
|
||||
" \"params\": {\n",
|
||||
" 'symbol': 'ETHUSDT',\n",
|
||||
" 'side': 'SELL',\n",
|
||||
" 'type': 'LIMIT',\n",
|
||||
" 'timeInForce': 'GTC',\n",
|
||||
" 'quantity': '0.01',\n",
|
||||
" 'price': '2500',\n",
|
||||
" }\n",
|
||||
"}"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 9,
|
||||
"id": "2122885a",
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"j = await aster_auth.post_authenticated_url(fut_acct_positionRisk)\n"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 10,
|
||||
"id": "e895ac52",
|
||||
"metadata": {},
|
||||
"outputs": [
|
||||
{
|
||||
"data": {
|
||||
"text/plain": [
|
||||
"[{'symbol': 'ETHUSDT',\n",
|
||||
" 'positionAmt': '-0.215',\n",
|
||||
" 'entryPrice': '2316.616543779',\n",
|
||||
" 'markPrice': '2318.47005039',\n",
|
||||
" 'unRealizedProfit': '-0.39850392',\n",
|
||||
" 'liquidationPrice': '2422.12954566',\n",
|
||||
" 'leverage': '150',\n",
|
||||
" 'maxNotionalValue': '300000',\n",
|
||||
" 'marginType': 'cross',\n",
|
||||
" 'isolatedMargin': '0.00000000',\n",
|
||||
" 'isAutoAddMargin': 'false',\n",
|
||||
" 'positionSide': 'BOTH',\n",
|
||||
" 'notional': '-498.47106083',\n",
|
||||
" 'isolatedWallet': '0',\n",
|
||||
" 'updateTime': 1777000243527}]"
|
||||
]
|
||||
},
|
||||
"execution_count": 10,
|
||||
"metadata": {},
|
||||
"output_type": "execute_result"
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"j"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 7,
|
||||
"id": "e61dca73",
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"d = [d for d in j if d.get('symbol', None) == 'ETHUSDT'][0]"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 8,
|
||||
"id": "f0538cde",
|
||||
"metadata": {},
|
||||
"outputs": [
|
||||
{
|
||||
"data": {
|
||||
"text/plain": [
|
||||
"{'symbol': 'ETHUSDT',\n",
|
||||
" 'positionAmt': '0.000',\n",
|
||||
" 'entryPrice': '0.0',\n",
|
||||
" 'markPrice': '2317.47321317',\n",
|
||||
" 'unRealizedProfit': '0.00000000',\n",
|
||||
" 'liquidationPrice': '0',\n",
|
||||
" 'leverage': '150',\n",
|
||||
" 'maxNotionalValue': '300000',\n",
|
||||
" 'marginType': 'cross',\n",
|
||||
" 'isolatedMargin': '0.00000000',\n",
|
||||
" 'isAutoAddMargin': 'false',\n",
|
||||
" 'positionSide': 'BOTH',\n",
|
||||
" 'notional': '0',\n",
|
||||
" 'isolatedWallet': '0',\n",
|
||||
" 'updateTime': 1776996575970}"
|
||||
]
|
||||
},
|
||||
"execution_count": 8,
|
||||
"metadata": {},
|
||||
"output_type": "execute_result"
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"d"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 54,
|
||||
"id": "80b9c0e5",
|
||||
"metadata": {},
|
||||
"outputs": [
|
||||
{
|
||||
"data": {
|
||||
"text/plain": [
|
||||
"-499.15475000000004"
|
||||
]
|
||||
},
|
||||
"execution_count": 54,
|
||||
"metadata": {},
|
||||
"output_type": "execute_result"
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"float(d['notional'])-float(d['unRealizedProfit'])"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 62,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"from datetime import datetime\n",
|
||||
"t = round(datetime.now().timestamp()*1000)"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 64,
|
||||
"id": "ebded6ad",
|
||||
"metadata": {},
|
||||
"outputs": [
|
||||
{
|
||||
"data": {
|
||||
"text/plain": [
|
||||
"Timestamp('2026-04-23 23:17:32.516000')"
|
||||
]
|
||||
},
|
||||
"execution_count": 64,
|
||||
"metadata": {},
|
||||
"output_type": "execute_result"
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"import pandas as pd\n",
|
||||
"pd.to_datetime(t, unit='ms')"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": []
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"id": "67a3bbb2",
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": []
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 23,
|
||||
"id": "e958e7da",
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"import json\n",
|
||||
"\n",
|
||||
"### User Data Stream ###\n",
|
||||
"# After posting limit order to rest\n",
|
||||
"after_order_post = json.loads('{\"e\":\"ORDER_TRADE_UPDATE\",\"T\":1776836992350,\"E\":1776836992388,\"o\":{\"s\":\"ETHUSDT\",\"c\":\"KFi375tZh5kzHsQJWKMJHe\",\"S\":\"BUY\",\"o\":\"LIMIT\",\"f\":\"GTC\",\"q\":\"0.010\",\"p\":\"2100\",\"ap\":\"0\",\"sp\":\"0\",\"x\":\"NEW\",\"X\":\"NEW\",\"i\":17341121450,\"l\":\"0\",\"z\":\"0\",\"L\":\"0\",\"T\":1776836992350,\"t\":0,\"b\":\"21\",\"a\":\"0\",\"m\":false,\"R\":false,\"wt\":\"CONTRACT_PRICE\",\"ot\":\"LIMIT\",\"ps\":\"BOTH\",\"cp\":false,\"rp\":\"0\",\"pP\":false,\"si\":0,\"ss\":0,\"h\":\"0xeec41a368072d5a47c13b3ad83703eded2e2b8cf9ff427095beb8b7684968db0\"}}')\n",
|
||||
"\n",
|
||||
"# After order cancellation in GUI\n",
|
||||
"after_order_cxl = json.loads('{\"e\":\"ORDER_TRADE_UPDATE\",\"T\":1776836998500,\"E\":1776836998533,\"o\":{\"s\":\"ETHUSDT\",\"c\":\"KFi375tZh5kzHsQJWKMJHe\",\"S\":\"BUY\",\"o\":\"LIMIT\",\"f\":\"GTC\",\"q\":\"0.010\",\"p\":\"2100\",\"ap\":\"0\",\"sp\":\"0\",\"x\":\"CANCELED\",\"X\":\"CANCELED\",\"i\":17341121450,\"l\":\"0\",\"z\":\"0\",\"L\":\"0\",\"T\":1776836998500,\"t\":0,\"b\":\"0\",\"a\":\"0\",\"m\":false,\"R\":false,\"wt\":\"CONTRACT_PRICE\",\"ot\":\"LIMIT\",\"ps\":\"BOTH\",\"cp\":false,\"rp\":\"0\",\"pP\":false,\"si\":0,\"ss\":0,\"h\":\"0x116bb48a4b41420aebbc76343d6f55f22d1ccbc36b04462e6172571fda836599\"}}')\n",
|
||||
"\n"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 24,
|
||||
"metadata": {},
|
||||
"outputs": [
|
||||
{
|
||||
"data": {
|
||||
"text/plain": [
|
||||
"{'e': 'ORDER_TRADE_UPDATE',\n",
|
||||
" 'T': 1776836992350,\n",
|
||||
" 'E': 1776836992388,\n",
|
||||
" 'o': {'s': 'ETHUSDT',\n",
|
||||
" 'c': 'KFi375tZh5kzHsQJWKMJHe',\n",
|
||||
" 'S': 'BUY',\n",
|
||||
" 'o': 'LIMIT',\n",
|
||||
" 'f': 'GTC',\n",
|
||||
" 'q': '0.010',\n",
|
||||
" 'p': '2100',\n",
|
||||
" 'ap': '0',\n",
|
||||
" 'sp': '0',\n",
|
||||
" 'x': 'NEW',\n",
|
||||
" 'X': 'NEW',\n",
|
||||
" 'i': 17341121450,\n",
|
||||
" 'l': '0',\n",
|
||||
" 'z': '0',\n",
|
||||
" 'L': '0',\n",
|
||||
" 'T': 1776836992350,\n",
|
||||
" 't': 0,\n",
|
||||
" 'b': '21',\n",
|
||||
" 'a': '0',\n",
|
||||
" 'm': False,\n",
|
||||
" 'R': False,\n",
|
||||
" 'wt': 'CONTRACT_PRICE',\n",
|
||||
" 'ot': 'LIMIT',\n",
|
||||
" 'ps': 'BOTH',\n",
|
||||
" 'cp': False,\n",
|
||||
" 'rp': '0',\n",
|
||||
" 'pP': False,\n",
|
||||
" 'si': 0,\n",
|
||||
" 'ss': 0,\n",
|
||||
" 'h': '0xeec41a368072d5a47c13b3ad83703eded2e2b8cf9ff427095beb8b7684968db0'}}"
|
||||
]
|
||||
},
|
||||
"execution_count": 24,
|
||||
"metadata": {},
|
||||
"output_type": "execute_result"
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"after_order_post"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 25,
|
||||
"id": "1ea320f2",
|
||||
"metadata": {},
|
||||
"outputs": [
|
||||
{
|
||||
"data": {
|
||||
"text/plain": [
|
||||
"{'e': 'ORDER_TRADE_UPDATE',\n",
|
||||
" 'T': 1776836998500,\n",
|
||||
" 'E': 1776836998533,\n",
|
||||
" 'o': {'s': 'ETHUSDT',\n",
|
||||
" 'c': 'KFi375tZh5kzHsQJWKMJHe',\n",
|
||||
" 'S': 'BUY',\n",
|
||||
" 'o': 'LIMIT',\n",
|
||||
" 'f': 'GTC',\n",
|
||||
" 'q': '0.010',\n",
|
||||
" 'p': '2100',\n",
|
||||
" 'ap': '0',\n",
|
||||
" 'sp': '0',\n",
|
||||
" 'x': 'CANCELED',\n",
|
||||
" 'X': 'CANCELED',\n",
|
||||
" 'i': 17341121450,\n",
|
||||
" 'l': '0',\n",
|
||||
" 'z': '0',\n",
|
||||
" 'L': '0',\n",
|
||||
" 'T': 1776836998500,\n",
|
||||
" 't': 0,\n",
|
||||
" 'b': '0',\n",
|
||||
" 'a': '0',\n",
|
||||
" 'm': False,\n",
|
||||
" 'R': False,\n",
|
||||
" 'wt': 'CONTRACT_PRICE',\n",
|
||||
" 'ot': 'LIMIT',\n",
|
||||
" 'ps': 'BOTH',\n",
|
||||
" 'cp': False,\n",
|
||||
" 'rp': '0',\n",
|
||||
" 'pP': False,\n",
|
||||
" 'si': 0,\n",
|
||||
" 'ss': 0,\n",
|
||||
" 'h': '0x116bb48a4b41420aebbc76343d6f55f22d1ccbc36b04462e6172571fda836599'}}"
|
||||
]
|
||||
},
|
||||
"execution_count": 25,
|
||||
"metadata": {},
|
||||
"output_type": "execute_result"
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"after_order_cxl"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 26,
|
||||
"id": "07ef4360",
|
||||
"metadata": {},
|
||||
"outputs": [
|
||||
{
|
||||
"data": {
|
||||
"text/plain": [
|
||||
"Timestamp('2019-09-19 07:51:05.651000')"
|
||||
]
|
||||
},
|
||||
"execution_count": 26,
|
||||
"metadata": {},
|
||||
"output_type": "execute_result"
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"import pandas as pd\n",
|
||||
"\n",
|
||||
"pd.to_datetime(1568879465651, unit='ms')"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 2,
|
||||
"metadata": {},
|
||||
"outputs": [
|
||||
{
|
||||
"data": {
|
||||
"text/plain": [
|
||||
"{'ok': True,\n",
|
||||
" 'result': {'message_id': 24,\n",
|
||||
" 'sender_chat': {'id': -1003864341457,\n",
|
||||
" 'title': 'Atwater_Alert',\n",
|
||||
" 'type': 'channel'},\n",
|
||||
" 'chat': {'id': -1003864341457, 'title': 'Atwater_Alert', 'type': 'channel'},\n",
|
||||
" 'date': 1777009128,\n",
|
||||
" 'text': 'alert!'}}"
|
||||
]
|
||||
},
|
||||
"execution_count": 2,
|
||||
"metadata": {},
|
||||
"output_type": "execute_result"
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"import modules.utils as utils\n",
|
||||
"\n",
|
||||
"utils.send_tg_alert('alert!')"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 2,
|
||||
"id": "284b7266",
|
||||
"metadata": {},
|
||||
"outputs": [
|
||||
{
|
||||
"data": {
|
||||
"text/plain": [
|
||||
"'sd'"
|
||||
]
|
||||
},
|
||||
"execution_count": 2,
|
||||
"metadata": {},
|
||||
"output_type": "execute_result"
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"\"sdfsdfsfdsdfs\"[:2]"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 11,
|
||||
"id": "98ed3a27",
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"from dataclasses import dataclass\n",
|
||||
"\n",
|
||||
"@dataclass(kw_only=True)\n",
|
||||
"class Flags:\n",
|
||||
" LIQUIDATE_POS_AND_KILL_ALGO_FLAG: bool = False\n",
|
||||
" NET_FUNDING_IS_ZERO: bool = False\n",
|
||||
" # list = field(init=False) \n",
|
||||
"Flags = Flags()"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 12,
|
||||
"id": "73a525c2",
|
||||
"metadata": {},
|
||||
"outputs": [
|
||||
{
|
||||
"data": {
|
||||
"text/plain": [
|
||||
"Flags(LIQUIDATE_POS_AND_KILL_ALGO_FLAG=False, NET_FUNDING_IS_ZERO=False)"
|
||||
]
|
||||
},
|
||||
"execution_count": 12,
|
||||
"metadata": {},
|
||||
"output_type": "execute_result"
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"Flags"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": []
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"id": "094e374c",
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": []
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"id": "15c3bcef",
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": []
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"id": "940ceba7",
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": []
|
||||
}
|
||||
],
|
||||
"metadata": {
|
||||
"kernelspec": {
|
||||
"display_name": "py_313",
|
||||
"language": "python",
|
||||
"name": "python3"
|
||||
},
|
||||
"language_info": {
|
||||
"codemirror_mode": {
|
||||
"name": "ipython",
|
||||
"version": 3
|
||||
},
|
||||
"file_extension": ".py",
|
||||
"mimetype": "text/x-python",
|
||||
"name": "python",
|
||||
"nbconvert_exporter": "python",
|
||||
"pygments_lexer": "ipython3",
|
||||
"version": "3.13.12"
|
||||
}
|
||||
},
|
||||
"nbformat": 4,
|
||||
"nbformat_minor": 5
|
||||
}
|
||||
|
||||
92
docker-compose.yml
Normal file
92
docker-compose.yml
Normal file
@@ -0,0 +1,92 @@
|
||||
# tail -f Fund_Rate_Algo.log Fund_Rate_Aster_User.log Fund_Rate_Aster.log Fund_Rate_Extended_FR.log Fund_Rate_Extended_OB.log Fund_Rate_Extended_User.log
|
||||
|
||||
services:
|
||||
algo:
|
||||
container_name: algo
|
||||
restart: "no"
|
||||
build:
|
||||
context: ./
|
||||
dockerfile: ./algo/Dockerfile
|
||||
depends_on:
|
||||
- algo_orchestrator
|
||||
- ws_aster
|
||||
- ws_aster_user
|
||||
- ws_extended_fund_rate
|
||||
- ws_extended_orderbook
|
||||
- ws_extended_user
|
||||
volumes:
|
||||
- /home/ubuntu/data:/home/ubuntu/data:rw # Read-write access to data
|
||||
- /home/ubuntu/logs:/home/ubuntu/logs:rw # Read-write access to data
|
||||
network_mode: "host"
|
||||
algo_orchestrator:
|
||||
container_name: algo_orchestrator
|
||||
restart: "unless-stopped"
|
||||
build:
|
||||
context: ./
|
||||
dockerfile: ./algo_orchestrator/Dockerfile
|
||||
volumes:
|
||||
- /home/ubuntu/data:/home/ubuntu/data:rw # Read-write access to data
|
||||
- /home/ubuntu/logs:/home/ubuntu/logs:rw # Read-write access to data
|
||||
network_mode: "host"
|
||||
ws_aster:
|
||||
container_name: ws_aster
|
||||
restart: "unless-stopped"
|
||||
build:
|
||||
context: ./
|
||||
dockerfile: ./ws_aster/Dockerfile
|
||||
volumes:
|
||||
- /home/ubuntu/data:/home/ubuntu/data:rw # Read-write access to data
|
||||
- /home/ubuntu/logs:/home/ubuntu/logs:rw # Read-write access to data
|
||||
network_mode: "host"
|
||||
ws_aster_user:
|
||||
container_name: ws_aster_user
|
||||
restart: "unless-stopped"
|
||||
build:
|
||||
context: ./
|
||||
dockerfile: ./ws_aster_user/Dockerfile
|
||||
volumes:
|
||||
- /home/ubuntu/data:/home/ubuntu/data:rw # Read-write access to data
|
||||
- /home/ubuntu/logs:/home/ubuntu/logs:rw # Read-write access to data
|
||||
network_mode: "host"
|
||||
ws_extended_fund_rate:
|
||||
container_name: ws_extended_fund_rate
|
||||
restart: "unless-stopped"
|
||||
build:
|
||||
context: ./
|
||||
dockerfile: ./ws_extended_fund_rate/Dockerfile
|
||||
volumes:
|
||||
- /home/ubuntu/data:/home/ubuntu/data:rw # Read-write access to data
|
||||
- /home/ubuntu/logs:/home/ubuntu/logs:rw # Read-write access to data
|
||||
network_mode: "host"
|
||||
ws_extended_orderbook:
|
||||
container_name: ws_extended_orderbook
|
||||
restart: "unless-stopped"
|
||||
build:
|
||||
context: ./
|
||||
dockerfile: ./ws_extended_orderbook/Dockerfile
|
||||
volumes:
|
||||
- /home/ubuntu/data:/home/ubuntu/data:rw # Read-write access to dataw
|
||||
- /home/ubuntu/logs:/home/ubuntu/logs:rw # Read-write access to data
|
||||
network_mode: "host"
|
||||
ws_extended_user:
|
||||
container_name: ws_extended_user
|
||||
restart: "unless-stopped"
|
||||
build:
|
||||
context: ./
|
||||
dockerfile: ./ws_extended_user/Dockerfile
|
||||
volumes:
|
||||
- /home/ubuntu/data:/home/ubuntu/data:rw # Read-write access to data
|
||||
- /home/ubuntu/logs:/home/ubuntu/logs:rw # Read-write access to data
|
||||
network_mode: "host"
|
||||
|
||||
|
||||
# ng:
|
||||
# container_name: ng
|
||||
# restart: "unless-stopped"
|
||||
# build:
|
||||
# context: ./
|
||||
# dockerfile: ./ng/Dockerfile
|
||||
# volumes:
|
||||
# - /home/ubuntu/data:/home/ubuntu/data:rw # Read-write access to data
|
||||
# - /home/ubuntu/logs:/home/ubuntu/logs:rw # Read-write access to data
|
||||
# network_mode: "host"
|
||||
797
extended.ipynb
Normal file
797
extended.ipynb
Normal file
@@ -0,0 +1,797 @@
|
||||
{
|
||||
"cells": [
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 1,
|
||||
"id": "6c70a8c3",
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"\n",
|
||||
"import asyncio\n",
|
||||
"import requests\n",
|
||||
"from x10.config import MAINNET_CONFIG, TESTNET_CONFIG\n",
|
||||
"from x10.core.stark_account import StarkPerpetualAccount\n",
|
||||
"from x10.perpetual.trading_client import PerpetualTradingClient\n",
|
||||
"from x10.models.order import OrderSide, OrderType\n",
|
||||
"import time\n",
|
||||
"from dotenv import load_dotenv\n",
|
||||
"import os\n",
|
||||
"import uuid\n",
|
||||
"import asyncio\n",
|
||||
"import logging\n",
|
||||
"from decimal import Decimal\n",
|
||||
"import modules.extended_auth as extend_auth\n",
|
||||
"\n"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 2,
|
||||
"id": "ff971ca9",
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"load_dotenv()\n",
|
||||
"\n",
|
||||
"API_KEY = os.getenv('EXTENDED_API_KEY')\n",
|
||||
"PUBLIC_KEY = os.getenv('EXTENDED_STARK_KEY_PUBLIC') # public Stark key (l2Key from account info)\n",
|
||||
"PRIVATE_KEY = os.getenv('EXTENDED_STARK_KEY_PRIVATE') # private Stark key (hex)\n",
|
||||
"VAULT = int(os.getenv('EXTENDED_VAULT_NUMBER')) # l2Vault from account info (integer)\n",
|
||||
"\n",
|
||||
"CONFIG = MAINNET_CONFIG\n",
|
||||
"\n",
|
||||
"ORDER_MARKET = \"ETH-USD\"\n",
|
||||
"ORDER_SIDE = OrderSide.BUY\n",
|
||||
"ORDER_QTY = Decimal(\"0.01\")\n",
|
||||
"ORDER_PRICE = Decimal(\"2200\")"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 3,
|
||||
"id": "fc2c6d2b",
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"client, trading_client = await extend_auth.create_auth_account_and_trading_client()"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 41,
|
||||
"id": "c366706f",
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"placed_order = await trading_client.place_order(\n",
|
||||
" market_name=ORDER_MARKET,\n",
|
||||
" amount_of_synthetic=ORDER_QTY,\n",
|
||||
" price=ORDER_PRICE,\n",
|
||||
" side=ORDER_SIDE,\n",
|
||||
" taker_fee=Decimal(\"0.00025\"),\n",
|
||||
" previous_order_id='1295034892466447624365619416628580523728221205816494340545831832663414858661'\n",
|
||||
")"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 5,
|
||||
"metadata": {},
|
||||
"outputs": [
|
||||
{
|
||||
"data": {
|
||||
"text/plain": [
|
||||
"WrappedApiResponse[EmptyModel](status='OK', data=EmptyModel(), error=None, pagination=None)"
|
||||
]
|
||||
},
|
||||
"execution_count": 5,
|
||||
"metadata": {},
|
||||
"output_type": "execute_result"
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"c = await trading_client.orders.mass_cancel(markets=['ETH-USD'])\n",
|
||||
"c"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"id": "03913674",
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": []
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 6,
|
||||
"id": "8dd8aa73",
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"d = await trading_client.account.get_positions_history()"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 12,
|
||||
"id": "5f74f7cc",
|
||||
"metadata": {},
|
||||
"outputs": [
|
||||
{
|
||||
"data": {
|
||||
"text/html": [
|
||||
"<div>\n",
|
||||
"<style scoped>\n",
|
||||
" .dataframe tbody tr th:only-of-type {\n",
|
||||
" vertical-align: middle;\n",
|
||||
" }\n",
|
||||
"\n",
|
||||
" .dataframe tbody tr th {\n",
|
||||
" vertical-align: top;\n",
|
||||
" }\n",
|
||||
"\n",
|
||||
" .dataframe thead th {\n",
|
||||
" text-align: right;\n",
|
||||
" }\n",
|
||||
"</style>\n",
|
||||
"<table border=\"1\" class=\"dataframe\">\n",
|
||||
" <thead>\n",
|
||||
" <tr style=\"text-align: right;\">\n",
|
||||
" <th></th>\n",
|
||||
" <th>0</th>\n",
|
||||
" <th>1</th>\n",
|
||||
" <th>2</th>\n",
|
||||
" <th>3</th>\n",
|
||||
" <th>4</th>\n",
|
||||
" <th>5</th>\n",
|
||||
" <th>6</th>\n",
|
||||
" <th>7</th>\n",
|
||||
" <th>8</th>\n",
|
||||
" <th>9</th>\n",
|
||||
" <th>10</th>\n",
|
||||
" <th>11</th>\n",
|
||||
" <th>12</th>\n",
|
||||
" <th>13</th>\n",
|
||||
" </tr>\n",
|
||||
" </thead>\n",
|
||||
" <tbody>\n",
|
||||
" <tr>\n",
|
||||
" <th>0</th>\n",
|
||||
" <td>(id, 2047947640758337536)</td>\n",
|
||||
" <td>(account_id, 270571)</td>\n",
|
||||
" <td>(market, ETH-USD)</td>\n",
|
||||
" <td>(side, SHORT)</td>\n",
|
||||
" <td>(size, 0.2160000000000000)</td>\n",
|
||||
" <td>(max_position_size, 0.2160000000000000)</td>\n",
|
||||
" <td>(leverage, 50.0000000000000000)</td>\n",
|
||||
" <td>(open_price, 2315.5000000000000000)</td>\n",
|
||||
" <td>(exit_price, 2308.6000000000000000)</td>\n",
|
||||
" <td>(realised_pnl, 1.4249250000000000)</td>\n",
|
||||
" <td>(realised_pnl_breakdown, trade_pnl=Decimal('1....</td>\n",
|
||||
" <td>(created_time, 1777103741241)</td>\n",
|
||||
" <td>(exit_type, TRADE)</td>\n",
|
||||
" <td>(closed_time, 1777133049026)</td>\n",
|
||||
" </tr>\n",
|
||||
" <tr>\n",
|
||||
" <th>1</th>\n",
|
||||
" <td>(id, 2047512381621272576)</td>\n",
|
||||
" <td>(account_id, 270571)</td>\n",
|
||||
" <td>(market, ETH-USD)</td>\n",
|
||||
" <td>(side, LONG)</td>\n",
|
||||
" <td>(size, 0.2150000000000000)</td>\n",
|
||||
" <td>(max_position_size, 0.2150000000000000)</td>\n",
|
||||
" <td>(leverage, 50.0000000000000000)</td>\n",
|
||||
" <td>(open_price, 2316.6000000000000000)</td>\n",
|
||||
" <td>(exit_price, 2315.5000000000000000)</td>\n",
|
||||
" <td>(realised_pnl, 0.0720660000000000)</td>\n",
|
||||
" <td>(realised_pnl_breakdown, trade_pnl=Decimal('-0...</td>\n",
|
||||
" <td>(created_time, 1776999967376)</td>\n",
|
||||
" <td>(exit_type, TRADE)</td>\n",
|
||||
" <td>(closed_time, 1777103741241)</td>\n",
|
||||
" </tr>\n",
|
||||
" <tr>\n",
|
||||
" <th>2</th>\n",
|
||||
" <td>(id, 2047419314696355840)</td>\n",
|
||||
" <td>(account_id, 270571)</td>\n",
|
||||
" <td>(market, ETH-USD)</td>\n",
|
||||
" <td>(side, LONG)</td>\n",
|
||||
" <td>(size, 0.2150000000000000)</td>\n",
|
||||
" <td>(max_position_size, 0.2150000000000000)</td>\n",
|
||||
" <td>(leverage, 50.0000000000000000)</td>\n",
|
||||
" <td>(open_price, 2321.7000000000000000)</td>\n",
|
||||
" <td>(exit_price, 2327.3000000000000000)</td>\n",
|
||||
" <td>(realised_pnl, 1.3196460000000000)</td>\n",
|
||||
" <td>(realised_pnl_breakdown, trade_pnl=Decimal('1....</td>\n",
|
||||
" <td>(created_time, 1776977778492)</td>\n",
|
||||
" <td>(exit_type, TRADE)</td>\n",
|
||||
" <td>(closed_time, 1776996621824)</td>\n",
|
||||
" </tr>\n",
|
||||
" </tbody>\n",
|
||||
"</table>\n",
|
||||
"</div>"
|
||||
],
|
||||
"text/plain": [
|
||||
" 0 1 2 \\\n",
|
||||
"0 (id, 2047947640758337536) (account_id, 270571) (market, ETH-USD) \n",
|
||||
"1 (id, 2047512381621272576) (account_id, 270571) (market, ETH-USD) \n",
|
||||
"2 (id, 2047419314696355840) (account_id, 270571) (market, ETH-USD) \n",
|
||||
"\n",
|
||||
" 3 4 \\\n",
|
||||
"0 (side, SHORT) (size, 0.2160000000000000) \n",
|
||||
"1 (side, LONG) (size, 0.2150000000000000) \n",
|
||||
"2 (side, LONG) (size, 0.2150000000000000) \n",
|
||||
"\n",
|
||||
" 5 6 \\\n",
|
||||
"0 (max_position_size, 0.2160000000000000) (leverage, 50.0000000000000000) \n",
|
||||
"1 (max_position_size, 0.2150000000000000) (leverage, 50.0000000000000000) \n",
|
||||
"2 (max_position_size, 0.2150000000000000) (leverage, 50.0000000000000000) \n",
|
||||
"\n",
|
||||
" 7 8 \\\n",
|
||||
"0 (open_price, 2315.5000000000000000) (exit_price, 2308.6000000000000000) \n",
|
||||
"1 (open_price, 2316.6000000000000000) (exit_price, 2315.5000000000000000) \n",
|
||||
"2 (open_price, 2321.7000000000000000) (exit_price, 2327.3000000000000000) \n",
|
||||
"\n",
|
||||
" 9 \\\n",
|
||||
"0 (realised_pnl, 1.4249250000000000) \n",
|
||||
"1 (realised_pnl, 0.0720660000000000) \n",
|
||||
"2 (realised_pnl, 1.3196460000000000) \n",
|
||||
"\n",
|
||||
" 10 \\\n",
|
||||
"0 (realised_pnl_breakdown, trade_pnl=Decimal('1.... \n",
|
||||
"1 (realised_pnl_breakdown, trade_pnl=Decimal('-0... \n",
|
||||
"2 (realised_pnl_breakdown, trade_pnl=Decimal('1.... \n",
|
||||
"\n",
|
||||
" 11 12 \\\n",
|
||||
"0 (created_time, 1777103741241) (exit_type, TRADE) \n",
|
||||
"1 (created_time, 1776999967376) (exit_type, TRADE) \n",
|
||||
"2 (created_time, 1776977778492) (exit_type, TRADE) \n",
|
||||
"\n",
|
||||
" 13 \n",
|
||||
"0 (closed_time, 1777133049026) \n",
|
||||
"1 (closed_time, 1777103741241) \n",
|
||||
"2 (closed_time, 1776996621824) "
|
||||
]
|
||||
},
|
||||
"execution_count": 12,
|
||||
"metadata": {},
|
||||
"output_type": "execute_result"
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"import pandas as pd\n",
|
||||
"pd.DataFrame(list(dict(d)['data']))"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": []
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"id": "e119aaac",
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": []
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 17,
|
||||
"metadata": {},
|
||||
"outputs": [
|
||||
{
|
||||
"data": {
|
||||
"text/plain": [
|
||||
"Timestamp('2026-04-25 16:04:09.026000')"
|
||||
]
|
||||
},
|
||||
"execution_count": 17,
|
||||
"metadata": {},
|
||||
"output_type": "execute_result"
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"import pandas as pd\n",
|
||||
"pd.to_datetime(1777133049026, unit='ms')"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": []
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 8,
|
||||
"metadata": {},
|
||||
"outputs": [
|
||||
{
|
||||
"data": {
|
||||
"text/plain": [
|
||||
"[PositionModel(id=2047419314696355840, account_id=270571, market='ETH-USD', status='OPENED', side='LONG', leverage=Decimal('50'), size=Decimal('0.215'), value=Decimal('500.247305'), open_price=Decimal('2321.7'), mark_price=Decimal('2326.731653474999'), liquidation_price=Decimal('2222.8'), unrealised_pnl=Decimal('1.081805'), realised_pnl=Decimal('0.065534'), tp_price=None, sl_price=None, adl=2, created_at=1776977778498, updated_at=1776986778819)]"
|
||||
]
|
||||
},
|
||||
"execution_count": 8,
|
||||
"metadata": {},
|
||||
"output_type": "execute_result"
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"d = dict(await trading_client.account.get_positions()).get('data')\n",
|
||||
"d"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 10,
|
||||
"id": "b9ac87f2",
|
||||
"metadata": {},
|
||||
"outputs": [
|
||||
{
|
||||
"data": {
|
||||
"text/plain": [
|
||||
"{'id': 2047419314696355840,\n",
|
||||
" 'account_id': 270571,\n",
|
||||
" 'market': 'ETH-USD',\n",
|
||||
" 'status': 'OPENED',\n",
|
||||
" 'side': 'LONG',\n",
|
||||
" 'leverage': Decimal('50'),\n",
|
||||
" 'size': Decimal('0.215'),\n",
|
||||
" 'value': Decimal('500.247305'),\n",
|
||||
" 'open_price': Decimal('2321.7'),\n",
|
||||
" 'mark_price': Decimal('2326.731653474999'),\n",
|
||||
" 'liquidation_price': Decimal('2222.8'),\n",
|
||||
" 'unrealised_pnl': Decimal('1.081805'),\n",
|
||||
" 'realised_pnl': Decimal('0.065534'),\n",
|
||||
" 'tp_price': None,\n",
|
||||
" 'sl_price': None,\n",
|
||||
" 'adl': 2,\n",
|
||||
" 'created_at': 1776977778498,\n",
|
||||
" 'updated_at': 1776986778819}"
|
||||
]
|
||||
},
|
||||
"execution_count": 10,
|
||||
"metadata": {},
|
||||
"output_type": "execute_result"
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"dict(d[0])"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"id": "62299776",
|
||||
"metadata": {},
|
||||
"outputs": [
|
||||
{
|
||||
"ename": "IndexError",
|
||||
"evalue": "list index out of range",
|
||||
"output_type": "error",
|
||||
"traceback": [
|
||||
"\u001b[31m---------------------------------------------------------------------------\u001b[39m",
|
||||
"\u001b[31mIndexError\u001b[39m Traceback (most recent call last)",
|
||||
"\u001b[36mCell\u001b[39m\u001b[36m \u001b[39m\u001b[32mIn[40]\u001b[39m\u001b[32m, line 1\u001b[39m\n\u001b[32m----> \u001b[39m\u001b[32m1\u001b[39m [j \u001b[38;5;28;01mfor\u001b[39;00m j \u001b[38;5;28;01min\u001b[39;00m d \u001b[38;5;28;01mif\u001b[39;00m j.get(\u001b[33m'market'\u001b[39m) == \u001b[33m'ETH-USD'\u001b[39m][\u001b[32m0\u001b[39m]\n",
|
||||
"\u001b[31mIndexError\u001b[39m: list index out of range"
|
||||
]
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"[j for j in d if j.get('market') == 'ETH-USD']"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 31,
|
||||
"id": "7cd3413d",
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"balance = dict(dict(await trading_client.account.get_balance()).get('data', {}))"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"id": "62632db3",
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"r = await trading_client.markets_info.get_pos"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 55,
|
||||
"id": "e9c3fdc4",
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"d = r['ETH-USD'].trading_config"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 58,
|
||||
"metadata": {},
|
||||
"outputs": [
|
||||
{
|
||||
"data": {
|
||||
"text/plain": [
|
||||
"0.01"
|
||||
]
|
||||
},
|
||||
"execution_count": 58,
|
||||
"metadata": {},
|
||||
"output_type": "execute_result"
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"float(d.min_order_size)"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": []
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 1,
|
||||
"id": "2b5d7d21",
|
||||
"metadata": {},
|
||||
"outputs": [
|
||||
{
|
||||
"data": {
|
||||
"text/plain": [
|
||||
"{'type': 'SNAPSHOT',\n",
|
||||
" 'data': {'t': 'SNAPSHOT',\n",
|
||||
" 'm': 'ETH-USD',\n",
|
||||
" 'b': [{'q': '362.381', 'p': '2318.1'}],\n",
|
||||
" 'a': [{'q': '70.572', 'p': '2318.2'}],\n",
|
||||
" 'd': '1'},\n",
|
||||
" 'ts': 1776822518279,\n",
|
||||
" 'seq': 14}"
|
||||
]
|
||||
},
|
||||
"execution_count": 1,
|
||||
"metadata": {},
|
||||
"output_type": "execute_result"
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"{\"type\":\"SNAPSHOT\",\"data\":{\"t\":\"SNAPSHOT\",\"m\":\"ETH-USD\",\"b\":[{\"q\":\"362.381\",\"p\":\"2318.1\"}],\"a\":[{\"q\":\"70.572\",\"p\":\"2318.2\"}],\"d\":\"1\"},\"ts\":1776822518279,\"seq\":14}"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 2,
|
||||
"id": "19f51572",
|
||||
"metadata": {},
|
||||
"outputs": [
|
||||
{
|
||||
"data": {
|
||||
"text/plain": [
|
||||
"{'data': {'m': 'ETH-USD', 'f': '-0.000001', 'T': 1776823319595},\n",
|
||||
" 'ts': 1776823319595,\n",
|
||||
" 'seq': 2}"
|
||||
]
|
||||
},
|
||||
"execution_count": 2,
|
||||
"metadata": {},
|
||||
"output_type": "execute_result"
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"{'data': {'m': 'ETH-USD', 'f': '-0.000001', 'T': 1776823319595}, 'ts': 1776823319595, 'seq': 2}"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 41,
|
||||
"id": "68006231",
|
||||
"metadata": {},
|
||||
"outputs": [
|
||||
{
|
||||
"data": {
|
||||
"text/plain": [
|
||||
"Timestamp('2026-04-22 03:00:00')"
|
||||
]
|
||||
},
|
||||
"execution_count": 41,
|
||||
"metadata": {},
|
||||
"output_type": "execute_result"
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"import pandas as pd\n",
|
||||
"\n",
|
||||
"pd.to_datetime(1776826800000, unit='ms')"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 12,
|
||||
"id": "5561153b",
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"def time_round_down(dt, interval_mins=5) -> int: # returns timestamp in seconds\n",
|
||||
" interval_secs = interval_mins * 60\n",
|
||||
" seconds = dt.timestamp()\n",
|
||||
" rounded_seconds = math.floor(seconds / interval_secs) * interval_secs\n",
|
||||
" \n",
|
||||
" return rounded_seconds"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 40,
|
||||
"id": "bd83b59f",
|
||||
"metadata": {},
|
||||
"outputs": [
|
||||
{
|
||||
"data": {
|
||||
"text/plain": [
|
||||
"1776826800000"
|
||||
]
|
||||
},
|
||||
"execution_count": 40,
|
||||
"metadata": {},
|
||||
"output_type": "execute_result"
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"(time_round_down(dt=datetime.now(timezone.utc), interval_mins=60)+(60*60))*1000"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"id": "086dbfb3",
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": []
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"id": "2d734f79",
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"\n"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 2,
|
||||
"id": "e6b59c51",
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"import json\n",
|
||||
"\n",
|
||||
"### USER DATA STREAM ###\n",
|
||||
"order = json.loads('{\"type\":\"ORDER\",\"data\":{\"isSnapshot\":true,\"orders\":[]},\"ts\":1776839215443,\"seq\":1}')\n",
|
||||
"position = json.loads('{\"type\":\"POSITION\",\"data\":{\"isSnapshot\":true,\"positions\":[]},\"ts\":1776839215443,\"seq\":2}')\n",
|
||||
"balance = json.loads('{\"type\":\"BALANCE\",\"data\":{\"isSnapshot\":true,\"balance\":{\"collateralName\":\"USD\",\"balance\":\"25.979998\",\"status\":\"ACTIVE\",\"equity\":\"25.979998\",\"spotEquity\":\"0\",\"spotEquityForAvailableForTrade\":\"0\",\"availableForTrade\":\"25.979998\",\"availableForWithdrawal\":\"25.979998\",\"unrealisedPnl\":\"0\",\"initialMargin\":\"0.000000\",\"marginRatio\":\"0.0000\",\"updatedTime\":1776822250184,\"exposure\":\"0\",\"leverage\":\"0.0000\"}},\"ts\":1776839215443,\"seq\":3}')"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 3,
|
||||
"id": "ee8420c4",
|
||||
"metadata": {},
|
||||
"outputs": [
|
||||
{
|
||||
"data": {
|
||||
"text/plain": [
|
||||
"{'type': 'ORDER',\n",
|
||||
" 'data': {'isSnapshot': True, 'orders': []},\n",
|
||||
" 'ts': 1776839215443,\n",
|
||||
" 'seq': 1}"
|
||||
]
|
||||
},
|
||||
"execution_count": 3,
|
||||
"metadata": {},
|
||||
"output_type": "execute_result"
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"order"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 4,
|
||||
"id": "e8d20d9f",
|
||||
"metadata": {},
|
||||
"outputs": [
|
||||
{
|
||||
"data": {
|
||||
"text/plain": [
|
||||
"{'type': 'POSITION',\n",
|
||||
" 'data': {'isSnapshot': True, 'positions': []},\n",
|
||||
" 'ts': 1776839215443,\n",
|
||||
" 'seq': 2}"
|
||||
]
|
||||
},
|
||||
"execution_count": 4,
|
||||
"metadata": {},
|
||||
"output_type": "execute_result"
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"position"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 5,
|
||||
"metadata": {},
|
||||
"outputs": [
|
||||
{
|
||||
"data": {
|
||||
"text/plain": [
|
||||
"{'type': 'BALANCE',\n",
|
||||
" 'data': {'isSnapshot': True,\n",
|
||||
" 'balance': {'collateralName': 'USD',\n",
|
||||
" 'balance': '25.979998',\n",
|
||||
" 'status': 'ACTIVE',\n",
|
||||
" 'equity': '25.979998',\n",
|
||||
" 'spotEquity': '0',\n",
|
||||
" 'spotEquityForAvailableForTrade': '0',\n",
|
||||
" 'availableForTrade': '25.979998',\n",
|
||||
" 'availableForWithdrawal': '25.979998',\n",
|
||||
" 'unrealisedPnl': '0',\n",
|
||||
" 'initialMargin': '0.000000',\n",
|
||||
" 'marginRatio': '0.0000',\n",
|
||||
" 'updatedTime': 1776822250184,\n",
|
||||
" 'exposure': '0',\n",
|
||||
" 'leverage': '0.0000'}},\n",
|
||||
" 'ts': 1776839215443,\n",
|
||||
" 'seq': 3}"
|
||||
]
|
||||
},
|
||||
"execution_count": 5,
|
||||
"metadata": {},
|
||||
"output_type": "execute_result"
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"balance"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"id": "fb16dfb9",
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": []
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 11,
|
||||
"id": "b0388dc7",
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"import valkey\n",
|
||||
"import json\n",
|
||||
"VAL_KEY = valkey.Valkey(host='localhost', port=6379, db=0, decode_responses=True)"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"b = json.loads(VAL_KEY.get('fund_rate_aster'))"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 12,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"a = json.loads(VAL_KEY.get('fund_rate_aster'))"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 16,
|
||||
"id": "665377af",
|
||||
"metadata": {},
|
||||
"outputs": [
|
||||
{
|
||||
"data": {
|
||||
"text/plain": [
|
||||
"{'timestamp_arrival': 1776970338127,\n",
|
||||
" 'timestamp_msg': 1776970338000,\n",
|
||||
" 'symbol': 'ETHUSDT',\n",
|
||||
" 'mark_price': '2310.59000000',\n",
|
||||
" 'index_price': '2311.29372093',\n",
|
||||
" 'estimated_settle_price': '2312.11380907',\n",
|
||||
" 'funding_rate': '-0.00001108',\n",
|
||||
" 'next_funding_time_ts_ms': 1776988800000}"
|
||||
]
|
||||
},
|
||||
"execution_count": 16,
|
||||
"metadata": {},
|
||||
"output_type": "execute_result"
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"json.loads(VAL_KEY.get('fund_rate_aster'))"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 18,
|
||||
"id": "f15dd3c7",
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"VAL_KEY.get('fr_aster_user_positions')"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"id": "23f88a3e",
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": []
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": []
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"id": "af5c751b",
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": []
|
||||
}
|
||||
],
|
||||
"metadata": {
|
||||
"kernelspec": {
|
||||
"display_name": "py_313",
|
||||
"language": "python",
|
||||
"name": "python3"
|
||||
},
|
||||
"language_info": {
|
||||
"codemirror_mode": {
|
||||
"name": "ipython",
|
||||
"version": 3
|
||||
},
|
||||
"file_extension": ".py",
|
||||
"mimetype": "text/x-python",
|
||||
"name": "python",
|
||||
"nbconvert_exporter": "python",
|
||||
"pygments_lexer": "ipython3",
|
||||
"version": "3.13.12"
|
||||
}
|
||||
},
|
||||
"nbformat": 4,
|
||||
"nbformat_minor": 5
|
||||
}
|
||||
580
main.py
580
main.py
@@ -5,22 +5,30 @@ import math
|
||||
import os
|
||||
import time
|
||||
import traceback
|
||||
from dataclasses import asdict, dataclass
|
||||
from dataclasses import asdict, dataclass, field
|
||||
from datetime import datetime, timezone
|
||||
from decimal import ROUND_DOWN, Decimal
|
||||
from typing import AsyncContextManager
|
||||
from dotenv import load_dotenv
|
||||
|
||||
from typing import Any
|
||||
import numpy as np
|
||||
import pandas as pd
|
||||
import requests
|
||||
|
||||
# import talib
|
||||
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
|
||||
|
||||
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 ###
|
||||
CLIENT = None
|
||||
EXTEND_CLIENT = None
|
||||
CON: AsyncContextManager | None = None
|
||||
VAL_KEY = None
|
||||
|
||||
@@ -28,42 +36,578 @@ VAL_KEY = None
|
||||
load_dotenv()
|
||||
LOG_FILEPATH: str = os.getenv("LOGS_PATH") + '/Fund_Rate_Algo.log'
|
||||
|
||||
### Algo Config ###
|
||||
ALGO_CONFIG: structs.Algo_Config = None
|
||||
|
||||
### CONSTANTS ###
|
||||
ASTER = structs.Perpetual_Exchange(
|
||||
mult = 150,
|
||||
lh_asset = 'ETH',
|
||||
rh_asset = 'USDT',
|
||||
symbol_asset_separator = '',
|
||||
)
|
||||
|
||||
EXTEND_LH_ASSET: str = 'ETH'
|
||||
EXTEND_RH_ASSET: str = 'USD'
|
||||
EXTEND_TICKER: str = EXTEND_LH_ASSET + '-' + EXTEND_RH_ASSET
|
||||
|
||||
### GLOBALS ###
|
||||
ASTER_MULT = 150
|
||||
EXTEND_MULT = 50
|
||||
|
||||
ASTER_MIN_ORDER_QTY = 0.001
|
||||
EXTEND_MIN_ORDER_QTY = 0.01
|
||||
|
||||
ASTER_AVAIL_COLLATERAL = 0
|
||||
EXTEND_AVAIL_COLLATERAL = 0
|
||||
|
||||
ASTER_NOTIONAL_POSITION = 0
|
||||
EXTEND_NOTIONAL_POSITION = 0
|
||||
|
||||
ASTER_OPEN_ORDERS = []
|
||||
EXTEND_OPEN_ORDERS = []
|
||||
|
||||
# ASTER_OPEN_POSITIONS = []
|
||||
# EXTEND_OPEN_POSITIONS = []
|
||||
|
||||
# EXCHANGES: list = [ Aster(), Extend() ]
|
||||
|
||||
### FLAGS ###
|
||||
Flags = structs.Flags()
|
||||
|
||||
### UTILS ###
|
||||
def round_decimal_down(value, decimal_places):
|
||||
# Construct precision string like '0.01' for 2 places
|
||||
fmt = f'0.{"0" * decimal_places}' if decimal_places > 0 else '0'
|
||||
precision = Decimal(fmt)
|
||||
return Decimal(str(value)).quantize(precision, rounding=ROUND_DOWN)
|
||||
|
||||
### OPEN ORDERS ###
|
||||
async def get_aster_open_orders():
|
||||
global ASTER_OPEN_ORDERS
|
||||
|
||||
fut_acct_openOrders = {
|
||||
"url": "/fapi/v3/openOrders",
|
||||
"method": "GET",
|
||||
"params": {}
|
||||
}
|
||||
ASTER_OPEN_ORDERS = await aster_auth.post_authenticated_url(fut_acct_openOrders)
|
||||
|
||||
async def get_extend_open_orders():
|
||||
global EXTEND_OPEN_ORDERS
|
||||
|
||||
EXTEND_OPEN_ORDERS = list(dict(await EXTEND_CLIENT.account.get_open_orders()).get('data', 0))
|
||||
|
||||
### WALLLET ###
|
||||
async def get_aster_collateral():
|
||||
global ASTER_AVAIL_COLLATERAL
|
||||
|
||||
fut_acct_balances = {
|
||||
"url": "/fapi/v3/balance",
|
||||
"method": "GET",
|
||||
"params": {}
|
||||
}
|
||||
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):
|
||||
global ASTER_NOTIONAL_POSITION
|
||||
global ASTER_MULT
|
||||
|
||||
if not resp:
|
||||
fut_acct_positionRisk = {
|
||||
"url": "/fapi/v3/positionRisk",
|
||||
"method": "GET",
|
||||
"params": {
|
||||
'symbol': ASTER.symbol,
|
||||
}
|
||||
}
|
||||
resp = await aster_auth.post_authenticated_url(fut_acct_positionRisk)
|
||||
|
||||
d = [x for x in resp if x.get('symbol', None) == ASTER.symbol][0]
|
||||
|
||||
if len(d) < 1:
|
||||
logging.info(f'BAD NOTIONAL - ASTER CHANGE: Empty d: {d}; resp: {resp}')
|
||||
await kill_algo()
|
||||
|
||||
aster_unrealized_pnl = float(d['unrealized_pnl']) if d.get('unrealized_pnl') is not None else float(d['unRealizedProfit'])
|
||||
|
||||
if d.get('notional') is not None:
|
||||
notional = float(d['notional'])
|
||||
else:
|
||||
notional = float(d['position_amount'])*float(d['entry_price'])
|
||||
|
||||
previous_notional_position = ASTER_NOTIONAL_POSITION
|
||||
ASTER_NOTIONAL_POSITION = notional - aster_unrealized_pnl
|
||||
if not resp:
|
||||
ASTER_MULT = float(d['leverage'])
|
||||
if abs(ASTER_NOTIONAL_POSITION) > ALGO_CONFIG.Max_Target_Notional*1.01:
|
||||
logging.info(f'BAD NOTIONAL - ASTER CHANGE: {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} -> {ASTER_NOTIONAL_POSITION:.2f}; UR PNL: {aster_unrealized_pnl:.2f}; MULT: {ASTER_MULT:.0f}; resp: {bool(resp)}')
|
||||
|
||||
async def get_extend_collateral():
|
||||
global EXTEND_AVAIL_COLLATERAL
|
||||
|
||||
get_bals = dict(dict(await EXTEND_CLIENT.account.get_balance()).get('data', {}))
|
||||
EXTEND_AVAIL_COLLATERAL = get_bals.get('available_for_trade', 0) if get_bals.get('collateral_name', None)==EXTEND_RH_ASSET else 0
|
||||
|
||||
async def get_extend_notional(resp: dict | None = None):
|
||||
global EXTEND_NOTIONAL_POSITION
|
||||
global EXTEND_MULT
|
||||
|
||||
if not resp:
|
||||
resp = dict(await EXTEND_CLIENT.account.get_positions()).get('data', {})
|
||||
|
||||
pos_dict = [dict(d) for d in resp if dict(d).get('market') == EXTEND_TICKER]
|
||||
|
||||
if not pos_dict:
|
||||
logging.info('get_extend_notional - No Positions')
|
||||
else:
|
||||
pos_dict = pos_dict[0]
|
||||
unrealized_pnl = pos_dict.get('unrealised_pnl', 0)
|
||||
previous_notional_position = EXTEND_NOTIONAL_POSITION
|
||||
position_side = pos_dict['side'] # LONG or SHORT
|
||||
notional_pos_abs = abs(float(pos_dict['value']))
|
||||
if position_side == 'LONG':
|
||||
notional_pos_sided = notional_pos_abs
|
||||
elif position_side == 'SHORT':
|
||||
notional_pos_sided = notional_pos_abs * -1
|
||||
else:
|
||||
logging.info(f'EXTEND BAD SIDE ON POSITION UPDATE: {pos_dict}')
|
||||
|
||||
EXTEND_NOTIONAL_POSITION = notional_pos_sided - float(unrealized_pnl)
|
||||
EXTEND_MULT = pos_dict.get('leverage', EXTEND_MULT)
|
||||
if EXTEND_NOTIONAL_POSITION != previous_notional_position:
|
||||
logging.info(f'EXTEND NOTIONAL CHANGE: {previous_notional_position} -> {EXTEND_NOTIONAL_POSITION:.2f}; UR PNL: {unrealized_pnl:.2f}; MULT: {EXTEND_MULT:.0f}; resp: {bool(resp)}')
|
||||
|
||||
### EXCHANGE INFO ###
|
||||
async def get_aster_exch_info():
|
||||
global ASTER_MIN_ORDER_QTY
|
||||
|
||||
fut_acct_exchangeInfo = {
|
||||
"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]
|
||||
ASTER_MIN_ORDER_QTY = float(f['minQty'])
|
||||
|
||||
async def get_extend_exch_info():
|
||||
global EXTEND_MIN_ORDER_QTY
|
||||
|
||||
r = await EXTEND_CLIENT.markets_info.get_markets_dict()
|
||||
EXTEND_MIN_ORDER_QTY = float(r['ETH-USD'].trading_config.min_order_size)
|
||||
|
||||
### CANCEL ORDERS ###
|
||||
async def aster_cancel_all_orders():
|
||||
cancel_all_open_orders = {
|
||||
"url": "/fapi/v3/allOpenOrders",
|
||||
"method": "DELETE",
|
||||
"params": {
|
||||
'symbol': 'ETHUSDT',
|
||||
}
|
||||
}
|
||||
r = await aster_auth.post_authenticated_url(cancel_all_open_orders)
|
||||
logging.info(f'ASTER CANCEL ALL OPEN ORDERS RESP: {r}')
|
||||
|
||||
async def extend_cancel_all_orders():
|
||||
r = await EXTEND_CLIENT.orders.mass_cancel(markets=[EXTEND_TICKER])
|
||||
logging.info(f'EXTEND CANCEL ALL OPEN ORDERS RESP: {r}')
|
||||
|
||||
### KILL ALGO ###
|
||||
async def kill_algo():
|
||||
await aster_cancel_all_orders()
|
||||
await extend_cancel_all_orders()
|
||||
logging.info('ALGO KILL FLAG ACTIVATED; CANCELLING OPEN ORDERS AND SHUTTING DOWN')
|
||||
raise ValueError('KILL FLAG ACTIVATED')
|
||||
|
||||
### ALGO LOOP ###
|
||||
async def run_algo():
|
||||
global ALGO_CONFIG
|
||||
|
||||
try:
|
||||
while True:
|
||||
loop_start = time.time()
|
||||
print('__________Start___________')
|
||||
# print('__________Start___________')
|
||||
### ALGO CONIFG ###
|
||||
ALGO_CONFIG = json.loads(VAL_KEY.get('fr_orchestrator_output'), object_hook=lambda d: structs.Algo_Config(**d))
|
||||
ALGO_CONFIG.Max_Target_Notional = float(min([ASTER_MULT, EXTEND_MULT]) * ALGO_CONFIG.Target_Open_Cash_Position)
|
||||
|
||||
MEXC_FUND_RATE = json.loads(VAL_KEY.get('fund_rate_mexc'))
|
||||
MEXC_TICKER = json.loads(VAL_KEY.get('fut_ticker_mexc'))
|
||||
APEX_TICKER = json.loads(VAL_KEY.get('fut_ticker_apex'))
|
||||
print(f'MEXC FUND RATE: {MEXC_FUND_RATE}')
|
||||
print(f'MEXC TICKER: {MEXC_TICKER}')
|
||||
print(f'APEX TICKER: {APEX_TICKER}')
|
||||
MIN_TIME_TO_FUNDING = ALGO_CONFIG.Min_Time_To_Funding_Minutes * 60 * 1000
|
||||
|
||||
time.sleep(5)
|
||||
### Load Data from Feedhandlers ###
|
||||
ASTER_FUND_RATE_DICT = json.loads(VAL_KEY.get('fund_rate_aster'))
|
||||
EXTENDED_FUND_RATE_DICT = json.loads(VAL_KEY.get('fund_rate_extended'))
|
||||
|
||||
ASTER_FUND_RATE = float(ASTER_FUND_RATE_DICT.get('funding_rate', 0))
|
||||
EXTEND_FUND_RATE = float(EXTENDED_FUND_RATE_DICT.get('funding_rate', 0))
|
||||
ASTER_FUND_RATE_TIME = float(ASTER_FUND_RATE_DICT.get('next_funding_time_ts_ms', 0))
|
||||
EXTEND_FUND_RATE_TIME = float(EXTENDED_FUND_RATE_DICT.get('next_funding_time_ts_ms', 0))
|
||||
|
||||
ASTER_TICKER_DICT = json.loads(VAL_KEY.get('fut_ticker_aster'))
|
||||
EXTENDED_TICKER_DICT = json.loads(VAL_KEY.get('fut_ticker_extended'))
|
||||
|
||||
### 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 []
|
||||
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 []
|
||||
|
||||
### 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 []
|
||||
|
||||
### 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 []
|
||||
|
||||
# CHECK NO MORE THAN 1 OPEN ORDER ON EITHER EXCHANGE #
|
||||
if len(ASTER_OPEN_ORDERS) > 1 or len(EXTEND_OPEN_ORDERS) > 1:
|
||||
logging.info(f'MORE THAN 1 ORDER OPEN - KILLING ALGO: ASTER_OPEN_ORDERS ({len(ASTER_OPEN_ORDERS)}): {ASTER_OPEN_ORDERS}; EXTEND_OPEN_ORDERS ({len(EXTEND_OPEN_ORDERS)}): {EXTEND_OPEN_ORDERS}')
|
||||
await kill_algo()
|
||||
raise ValueError('NOT HERE: MORE THAN 1 ORDER OPEN - KILLING ALGO: ASTER_OPEN_ORDERS')
|
||||
|
||||
### CHECK TIME TO FUNDING AND WHETHER TO BE ACTIVE ###
|
||||
now_ms = round(datetime.now().timestamp()*1000)
|
||||
time_to_funding_ms = min([ASTER_FUND_RATE_TIME, EXTEND_FUND_RATE_TIME]) - now_ms
|
||||
if ( time_to_funding_ms > MIN_TIME_TO_FUNDING ) and (not ASTER_OPEN_ORDERS) and (not EXTEND_OPEN_ORDERS):
|
||||
print(f'Outside action window (minutes) and no active order (sleeping for 5 sec): {pd.to_datetime(time_to_funding_ms, unit='ms').minute} > {pd.to_datetime(MIN_TIME_TO_FUNDING, unit='ms').minute}')
|
||||
time.sleep(5)
|
||||
continue
|
||||
|
||||
if len(ASTER_WS_POS_UPDATES) > 0:
|
||||
await get_aster_notional_position(resp=ASTER_WS_POS_UPDATES)
|
||||
###### *** returned 0 notional even though had a position, need to handle and safety check to not order above max notional.
|
||||
|
||||
if len(EXTEND_WS_POS_UPDATES) > 0:
|
||||
await get_extend_notional(resp=EXTEND_WS_POS_UPDATES)
|
||||
|
||||
if ASTER_WS_ORDER_UPDATES is not None:
|
||||
for idx, o in enumerate(ASTER_OPEN_ORDERS):
|
||||
order_id = o.get('order_id') if o.get('order_id') is not None else o.get('orderId')
|
||||
order_orig_status = o['status']
|
||||
order_update = [ou for ou in ASTER_WS_ORDER_UPDATES if ou.get('order_id', None) == order_id]
|
||||
|
||||
if len(order_update) > 0:
|
||||
order_update = order_update[0]
|
||||
order_update_status = order_update.get('status') if order_update.get('status') is not None else order_update.get('order_status')
|
||||
order_status_changed = order_orig_status.upper() != order_update_status.upper()
|
||||
|
||||
if order_status_changed:
|
||||
logging.info(f'ASTER ORDER ({order_id}): {order_orig_status} -> {order_update_status}')
|
||||
ASTER_OPEN_ORDERS[idx] = order_update
|
||||
if order_update_status in ['CANCELED','EXPIRED']:
|
||||
logging.info(f'ASTER ORDER CANCELLED or EXPIRED: {order_id}')
|
||||
ASTER_OPEN_ORDERS.pop(idx)
|
||||
elif order_update_status in ['PARTIALLY_FILLED']:
|
||||
logging.info(f'ASTER ORDER PARTIALLY FILLED: {order_id}')
|
||||
await get_aster_collateral()
|
||||
await get_aster_notional_position()
|
||||
elif order_update_status in ['FILLED']:
|
||||
logging.info(f'ASTER ORDER FILLED: {order_id}')
|
||||
ASTER_OPEN_ORDERS.pop(idx)
|
||||
await get_aster_collateral()
|
||||
await get_aster_notional_position()
|
||||
else:
|
||||
logging.critical(f'EXTEND ORDER STATUS CHG TO UNEXPECTED VALUE, KILLING... ({order_id}): {order_orig_status} -> {order_update_status}')
|
||||
if EXTEND_WS_ORDER_UPDATES is not None:
|
||||
for idx, o in enumerate(EXTEND_OPEN_ORDERS):
|
||||
o = dict(o)
|
||||
order_id = o.get('order_id') if o.get('order_id') is not None else o.get('id')
|
||||
order_orig_status = o['status']
|
||||
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()
|
||||
|
||||
if order_status_changed:
|
||||
logging.info(f'EXTEND ORDER ({order_id}): {order_orig_status} -> {order_update_status}')
|
||||
EXTEND_OPEN_ORDERS[idx] = order_update
|
||||
if order_update_status in ['CANCELLED','EXPIRED','REJECTED']:
|
||||
logging.info(f'EXTEND ORDER CANCELLED or EXPIRED: {order_id}')
|
||||
EXTEND_OPEN_ORDERS.pop(idx)
|
||||
elif order_update_status in ['PARTIALLY_FILLED']:
|
||||
logging.info(f'EXTEND ORDER PARTIALLY FILLED: {order_id}')
|
||||
await get_extend_collateral()
|
||||
await get_extend_notional()
|
||||
elif order_update_status in ['FILLED']:
|
||||
logging.info(f'EXTEND ORDER FILLED: {order_id}')
|
||||
EXTEND_OPEN_ORDERS.pop(idx)
|
||||
await get_extend_collateral()
|
||||
await get_extend_notional()
|
||||
else:
|
||||
logging.critical(f'EXTEND ORDER STATUS CHG TO UNEXPECTED VALUE, KILLING... ({order_id}): {order_orig_status} -> {order_update_status}')
|
||||
|
||||
|
||||
ASTER_PAYOUT_DIRECTION_STR = 'LONG PAYS SHORT' if ASTER_FUND_RATE > 0 else 'SHORT PAYS LONG'
|
||||
EXTEND_PAYOUT_DIRECTION_STR = 'LONG PAYS SHORT' if EXTEND_FUND_RATE > 0 else 'SHORT PAYS LONG'
|
||||
|
||||
min_between_fundings = round((abs(ASTER_FUND_RATE_TIME - EXTEND_FUND_RATE_TIME) / 1000 / 60))
|
||||
FUNDINGS_AT_SAME_TIME_NEXT_HR = min_between_fundings < 5
|
||||
# FUNDINGS_AT_SAME_TIME_NEXT_HR = ( (ASTER_FUND_RATE_TIME < 60*60*1000) and (EXTEND_FUND_RATE < 60*60*1000) )
|
||||
|
||||
|
||||
if ( abs(ASTER_FUND_RATE) > abs(EXTEND_FUND_RATE) ) and FUNDINGS_AT_SAME_TIME_NEXT_HR:
|
||||
ALPHA_EXCH = 'ASTER'
|
||||
ALPHA_FUND_RATE = ASTER_FUND_RATE
|
||||
else:
|
||||
ALPHA_EXCH = 'EXTEND'
|
||||
ALPHA_FUND_RATE = EXTEND_FUND_RATE
|
||||
|
||||
if ALPHA_FUND_RATE < 0:
|
||||
ALPHA_CARRY_SIDE = 'BUY'
|
||||
ALPHA_TGT_NOTIONAL = ALGO_CONFIG.Max_Target_Notional
|
||||
else:
|
||||
ALPHA_CARRY_SIDE = 'SELL'
|
||||
ALPHA_TGT_NOTIONAL = ALGO_CONFIG.Max_Target_Notional*-1
|
||||
|
||||
def calc_next_net_fund_rate(FUNDINGS_AT_SAME_TIME_NEXT_HR: bool) -> float:
|
||||
if FUNDINGS_AT_SAME_TIME_NEXT_HR:
|
||||
return ASTER_FUND_RATE + EXTEND_FUND_RATE
|
||||
else:
|
||||
return EXTEND_FUND_RATE
|
||||
|
||||
NEXT_NET_FUNDING_RATE = calc_next_net_fund_rate(FUNDINGS_AT_SAME_TIME_NEXT_HR)
|
||||
Flags.NET_FUNDING_IS_ZERO = NEXT_NET_FUNDING_RATE == 0.00
|
||||
if Flags.NET_FUNDING_IS_ZERO:
|
||||
logging.info('NET FUNDING = 0.00; Cancelling Open Orders; Wait Until Non-Zero.')
|
||||
|
||||
ALPHA_TGT_NOTIONAL = 0.00
|
||||
|
||||
if ALPHA_EXCH == 'EXTEND':
|
||||
ASTER_TGT_NOTIONAL = ALPHA_TGT_NOTIONAL*-1
|
||||
EXTEND_TGT_NOTIONAL = ALPHA_TGT_NOTIONAL
|
||||
if ALPHA_CARRY_SIDE == 'BUY':
|
||||
ASTER_TOB_PX = float(ASTER_TICKER_DICT['best_ask_px'])
|
||||
EXTEND_TOB_PX = float(EXTENDED_TICKER_DICT['best_bid_px'])
|
||||
else:
|
||||
ASTER_TOB_PX = float(ASTER_TICKER_DICT['best_bid_px'])
|
||||
EXTEND_TOB_PX = float(EXTENDED_TICKER_DICT['best_ask_px'])
|
||||
else:
|
||||
ASTER_TGT_NOTIONAL = ALPHA_TGT_NOTIONAL
|
||||
EXTEND_TGT_NOTIONAL = ALPHA_TGT_NOTIONAL*-1
|
||||
if ALPHA_CARRY_SIDE == 'BUY':
|
||||
ASTER_TOB_PX = float(ASTER_TICKER_DICT['best_bid_px'])
|
||||
EXTEND_TOB_PX = float(EXTENDED_TICKER_DICT['best_ask_px'])
|
||||
else:
|
||||
ASTER_TOB_PX = float(ASTER_TICKER_DICT['best_ask_px'])
|
||||
EXTEND_TOB_PX = float(EXTENDED_TICKER_DICT['best_bid_px'])
|
||||
|
||||
|
||||
ASTER_TGT_TAIL = ASTER_TGT_NOTIONAL - ASTER_NOTIONAL_POSITION
|
||||
EXTEND_TGT_TAIL = EXTEND_TGT_NOTIONAL - EXTEND_NOTIONAL_POSITION
|
||||
|
||||
ASTER_TGT_TAIL_BASE_QTY = Decimal(str(float(ASTER_TGT_TAIL) / float(ASTER_TOB_PX))).quantize(Decimal(str(0.001)), rounding=ROUND_DOWN)
|
||||
EXTEND_TGT_TAIL_BASE_QTY = Decimal(str(float(EXTEND_TGT_TAIL) / float(EXTEND_TOB_PX))).quantize(Decimal(str(0.001)), rounding=ROUND_DOWN)
|
||||
|
||||
MAX_MIN_ORDER_QTY = max([ASTER_MIN_ORDER_QTY, EXTEND_MIN_ORDER_QTY])
|
||||
|
||||
ASTER_TGT_TAIL_ORDERABLE = abs(ASTER_TGT_TAIL_BASE_QTY) >= MAX_MIN_ORDER_QTY
|
||||
EXTEND_TGT_TAIL_ORDERABLE = abs(EXTEND_TGT_TAIL_BASE_QTY) >= MAX_MIN_ORDER_QTY
|
||||
|
||||
def print_summary(use_logging: bool = False):
|
||||
OUT: print | logging.info = logging.info if use_logging else print
|
||||
|
||||
OUT(f'''
|
||||
{pd.to_datetime(ASTER_FUND_RATE_TIME, unit='ms')} ({(pd.to_datetime(ASTER_FUND_RATE_TIME, unit='ms')-datetime.now()):}) | {pd.to_datetime(EXTEND_FUND_RATE_TIME, unit='ms')} ({(pd.to_datetime(EXTEND_FUND_RATE_TIME, unit='ms')-datetime.now()):})
|
||||
ASTER: {ASTER_FUND_RATE:.6%} [{ASTER_FUND_RATE*10_000:.2f}bps] [{ASTER_FUND_RATE*1_000_000:.0f}pips] | EXTEND: {EXTEND_FUND_RATE:.6%} [{EXTEND_FUND_RATE*10_000:.2f}bps] [{EXTEND_FUND_RATE*1_000_000:.0f}pips]
|
||||
ASTER: {ASTER_PAYOUT_DIRECTION_STR} | EXTEND: {EXTEND_PAYOUT_DIRECTION_STR}
|
||||
ASTER: [ Available Collateral: {ASTER_AVAIL_COLLATERAL:.4f} ] | EXTEND: [ Available Collateral: {EXTEND_AVAIL_COLLATERAL:.4f} ]
|
||||
ASTER: [ Notional Position $ : {ASTER_NOTIONAL_POSITION:.4f} ] | EXTEND: [ Notional Position $ : {EXTEND_NOTIONAL_POSITION:.4f} ]
|
||||
|
||||
SAME TIME? : {FUNDINGS_AT_SAME_TIME_NEXT_HR} [ Minutes Between Fundings: {min_between_fundings} ]
|
||||
NET FUNDING : {NEXT_NET_FUNDING_RATE:.6%} [{NEXT_NET_FUNDING_RATE*10_000:.2f}bps] [{NEXT_NET_FUNDING_RATE*1_000_000:.0f}pips]; Is Zero?: {Flags.NET_FUNDING_IS_ZERO}
|
||||
ALPHA SIDE : {ALPHA_EXCH} [{ALPHA_CARRY_SIDE}]
|
||||
|
||||
TGT NOTIONAL: $ {ALGO_CONFIG.Max_Target_Notional if not Flags.NET_FUNDING_IS_ZERO else 0.00}
|
||||
|
||||
ASTER: {ASTER_NOTIONAL_POSITION:.4f} -> {ASTER_TGT_NOTIONAL:.2f} [ Remain: {ASTER_TGT_TAIL:.4f} ] | EXTEND: {EXTEND_NOTIONAL_POSITION:.4f} -> {EXTEND_TGT_NOTIONAL:.2f} [ Remain: {EXTEND_TGT_TAIL:.4f} ]
|
||||
ASTER: {ASTER_TGT_NOTIONAL:.4f} - {ASTER_NOTIONAL_POSITION:.4f} = Tail: {ASTER_TGT_TAIL:4f} | EXTEND: {EXTEND_TGT_NOTIONAL:.4f} - {EXTEND_NOTIONAL_POSITION:.4f} = Tail: {EXTEND_TGT_TAIL:4f}
|
||||
ASTER: {ASTER_TGT_TAIL_BASE_QTY:.4f} > {MAX_MIN_ORDER_QTY:.4f} min [ Order: {ASTER_TGT_TAIL_ORDERABLE} ] | EXTEND: {EXTEND_TGT_TAIL_BASE_QTY:.4f} > {MAX_MIN_ORDER_QTY:.4f} min [ Order: {EXTEND_TGT_TAIL_ORDERABLE} ]
|
||||
|
||||
--- ASTER OPEN ORDERS ---
|
||||
{ASTER_OPEN_ORDERS}
|
||||
|
||||
--- EXTEND OPEN ORDERS ---
|
||||
{EXTEND_OPEN_ORDERS}
|
||||
''')
|
||||
if ALGO_CONFIG.Print_Summary_Each_Loop:
|
||||
print_summary()
|
||||
# print_summary()
|
||||
|
||||
### ROUTES ###
|
||||
# ASTER
|
||||
if ASTER_TGT_TAIL_ORDERABLE and ALGO_CONFIG.Allow_Ordering_Aster:
|
||||
symbol = ASTER.symbol
|
||||
side = 'BUY' if ASTER_TGT_TAIL_BASE_QTY > 0.00 else 'SELL'
|
||||
qty = str(abs(ASTER_TGT_TAIL_BASE_QTY))
|
||||
price = ASTER_TOB_PX - ALGO_CONFIG.Price_Worsener_Aster if side == 'BUY' else ASTER_TOB_PX + ALGO_CONFIG.Price_Worsener_Aster
|
||||
|
||||
if abs( ( float(ASTER_TGT_TAIL_BASE_QTY)*float(price) ) + ASTER_NOTIONAL_POSITION ) > ALGO_CONFIG.Max_Target_Notional*1.01:
|
||||
logging.info(f'TRYING TO ORDER OVER MAX NOTIOANL - ASTER: {ASTER_NOTIONAL_POSITION} + {float(ASTER_TGT_TAIL_BASE_QTY)*float(price)} (qty: {float(ASTER_TGT_TAIL_BASE_QTY):.2f}; px: {float(price):.2f})')
|
||||
await kill_algo()
|
||||
if ASTER_OPEN_ORDERS:
|
||||
open_order_id = ASTER_OPEN_ORDERS[0].get('order_id') if ASTER_OPEN_ORDERS[0].get('order_id') is not None else ASTER_OPEN_ORDERS[0]['orderId']
|
||||
open_order_px = float(ASTER_OPEN_ORDERS[0].get('price')) if ASTER_OPEN_ORDERS[0].get('price') is not None else float(ASTER_OPEN_ORDERS[0]['original_price'])
|
||||
if round(open_order_px - float(price), 2) == 0.00:
|
||||
logging.info('ASTER OPEN ORDER NO PX CHG; SKIPPING')
|
||||
place_order = False
|
||||
else:
|
||||
cancel_order = {
|
||||
"url": "/fapi/v3/order",
|
||||
"method": "DELETE",
|
||||
"params": {
|
||||
'symbol': ASTER.symbol,
|
||||
'orderId': open_order_id,
|
||||
}
|
||||
}
|
||||
cr = await aster_auth.post_authenticated_url(cancel_order)
|
||||
if cr.get('status', None) == 'CANCELED':
|
||||
ASTER_OPEN_ORDERS.pop(0)
|
||||
place_order = True
|
||||
else:
|
||||
logging.warning(f'ASTER ORDER FAILED TO CANCEL DURING CR ({open_order_id}): RESP {cr}')
|
||||
place_order = False
|
||||
else:
|
||||
place_order = True
|
||||
|
||||
if ASTER_TGT_TAIL_BASE_QTY == 0.00:
|
||||
place_order = False
|
||||
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)
|
||||
post_order = {
|
||||
"url": "/fapi/v3/order",
|
||||
"method": "POST",
|
||||
"params": {
|
||||
'symbol': symbol,
|
||||
'side': side,
|
||||
'type': 'LIMIT',
|
||||
'timeInForce': 'GTC',
|
||||
'quantity': qty,
|
||||
'price': price,
|
||||
}
|
||||
}
|
||||
order_resp = await aster_auth.post_authenticated_url(post_order)
|
||||
if order_resp.get('orderId', None) is not None:
|
||||
order_resp['original_price'] = price
|
||||
ASTER_OPEN_ORDERS.append(order_resp)
|
||||
utils.send_tg_alert(f'FR_ALGO - ASTER Order. Start_$: {ASTER_NOTIONAL_POSITION:.2f}; Value: {float(ASTER_TGT_TAIL_BASE_QTY)*float(price):.2f}; Price: {float(price):.2f}')
|
||||
logging.info(f'ASTER ORDER PLACED SUCCESS: {order_resp}')
|
||||
print_summary(use_logging=True)
|
||||
else:
|
||||
logging.warning('ASTER PLACE ORDER CHECKS FAILED, SKIPPING')
|
||||
|
||||
elif not(ASTER_TGT_TAIL_ORDERABLE) and ASTER_OPEN_ORDERS:
|
||||
logging.info('ASTER HAS NO TAIL BUT OPEN ORDERS - CANCELLING OPEN ORDERS')
|
||||
await aster_cancel_all_orders()
|
||||
|
||||
# EXTEND
|
||||
if EXTEND_TGT_TAIL_ORDERABLE and ALGO_CONFIG.Allow_Ordering_Extend:
|
||||
symbol = EXTEND_TICKER
|
||||
side = OrderSide.BUY if EXTEND_TGT_TAIL_BASE_QTY > 0.00 else OrderSide.SELL
|
||||
qty = Decimal(str(abs(EXTEND_TGT_TAIL_BASE_QTY)))
|
||||
price = EXTEND_TOB_PX - ALGO_CONFIG.Price_Worsener_Extend if side == 'BUY' else EXTEND_TOB_PX + ALGO_CONFIG.Price_Worsener_Extend
|
||||
|
||||
if abs( ( float(EXTEND_TGT_TAIL_BASE_QTY)*float(price) ) + EXTEND_NOTIONAL_POSITION ) > ALGO_CONFIG.Max_Target_Notional*1.01:
|
||||
logging.info(f'TRYING TO ORDER OVER MAX NOTIOANL - EXTEND: {EXTEND_NOTIONAL_POSITION:.2f} + {float(EXTEND_TGT_TAIL_BASE_QTY)*float(price):.2f} (qty: {float(EXTEND_TGT_TAIL_BASE_QTY):.2f}; px: {float(price):.2f})')
|
||||
await kill_algo()
|
||||
if EXTEND_OPEN_ORDERS:
|
||||
open_order_dict = dict(EXTEND_OPEN_ORDERS[0])
|
||||
open_order_id = open_order_dict['external_id']
|
||||
open_order_px = float(open_order_dict['price'])
|
||||
place_order = True
|
||||
else:
|
||||
open_order_id = None
|
||||
open_order_px = 0
|
||||
place_order = True
|
||||
if place_order:
|
||||
price = Decimal(str(price)).quantize(Decimal(str(0.01)), rounding=ROUND_DOWN)
|
||||
if round(open_order_px - float(price), 2) == 0.00:
|
||||
logging.info('EXTEND OPEN ORDER NO PX CHG; SKIPPING')
|
||||
else:
|
||||
order_resp = await EXTEND_CLIENT.place_order(
|
||||
market_name=symbol,
|
||||
amount_of_synthetic=qty,
|
||||
price=price,
|
||||
side=side,
|
||||
taker_fee=Decimal("0.00025"),
|
||||
previous_order_id=open_order_id,
|
||||
)
|
||||
order_resp_dict = dict(order_resp)
|
||||
if order_resp_dict.get('status', None) == 'OK':
|
||||
if EXTEND_OPEN_ORDERS:
|
||||
EXTEND_OPEN_ORDERS.pop(0)
|
||||
|
||||
order_dict = dict(order_resp_dict['data'])
|
||||
order_dict['status'] = 'NEW'
|
||||
order_dict['price'] = str(price)
|
||||
|
||||
EXTEND_OPEN_ORDERS.append(order_dict)
|
||||
utils.send_tg_alert(f'FR_ALGO - EXTEND Order. Start_$: {EXTEND_NOTIONAL_POSITION:.2f}; Value: {float(EXTEND_TGT_TAIL_BASE_QTY)*float(price):.2f}; Price: {float(price):.2f}')
|
||||
logging.info(f'EXTEND ORDER PLACED SUCCESS: {order_dict}')
|
||||
print_summary(use_logging=True)
|
||||
else:
|
||||
logging.warning('EXTEND PLACE ORDER CHECKS FAILED, SKIPPING')
|
||||
|
||||
elif not(EXTEND_TGT_TAIL_ORDERABLE) and EXTEND_OPEN_ORDERS:
|
||||
logging.info('EXTEND HAS NO TAIL BUT OPEN ORDERS - CANCELLING OPEN ORDERS')
|
||||
await extend_cancel_all_orders()
|
||||
|
||||
print(f'__________ End ___________ (Algo Engine ms: {(time.time() - loop_start)*1000})')
|
||||
|
||||
time.sleep(ALGO_CONFIG.Loop_Sleep_Sec)
|
||||
|
||||
print(f'__________________________ (Algo Engine ms: {(time.time() - loop_start)*1000})')
|
||||
except KeyboardInterrupt:
|
||||
print('...algo stopped')
|
||||
# await cancel_all_orders(CLIENT=CLIENT)
|
||||
logging.info('CANCELLING OPEN ORDERS')
|
||||
await kill_algo()
|
||||
except Exception as e:
|
||||
logging.critical(f'*** ALGO ENGINE CRASHED: {e}')
|
||||
logging.error(traceback.format_exc())
|
||||
# await cancel_all_orders(CLIENT=CLIENT)
|
||||
logging.critical(f'*** ALGO ENGINE CRASHED: {e}')
|
||||
logging.info('CANCELLING OPEN ORDERS')
|
||||
utils.send_tg_alert(f'FR_ALGO_CRASHED: {str(e)}')
|
||||
await kill_algo()
|
||||
|
||||
|
||||
### MAIN STARTUP ###
|
||||
async def main():
|
||||
global CLIENT
|
||||
global EXTEND_CLIENT
|
||||
global VAL_KEY
|
||||
global CON
|
||||
global ALGO_CONFIG
|
||||
|
||||
_, EXTEND_CLIENT = await extend_auth.create_auth_account_and_trading_client()
|
||||
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:
|
||||
ALGO_CONFIG = json.load(file, object_hook=lambda d: structs.Algo_Config(**d))
|
||||
ALGO_CONFIG.Max_Target_Notional = float(min([ASTER_MULT, EXTEND_MULT]) * ALGO_CONFIG.Target_Open_Cash_Position)
|
||||
|
||||
async with engine.connect() as CON:
|
||||
# await create_executions_orders_table(CON=CON)
|
||||
### ASTER SETUP ###
|
||||
await get_aster_collateral()
|
||||
await get_aster_notional_position()
|
||||
await get_aster_exch_info()
|
||||
await get_aster_open_orders()
|
||||
### EXTEND SETUP ###
|
||||
await get_extend_collateral()
|
||||
await get_extend_notional()
|
||||
await get_extend_exch_info()
|
||||
await get_extend_open_orders()
|
||||
|
||||
await run_algo()
|
||||
|
||||
if __name__ == '__main__':
|
||||
|
||||
771
main_v0.py
Normal file
771
main_v0.py
Normal file
@@ -0,0 +1,771 @@
|
||||
import asyncio
|
||||
import json
|
||||
import logging
|
||||
import math
|
||||
import os
|
||||
import time
|
||||
import traceback
|
||||
from dataclasses import asdict, dataclass, field
|
||||
from datetime import datetime, timezone
|
||||
from decimal import ROUND_DOWN, Decimal
|
||||
from typing import AsyncContextManager
|
||||
|
||||
from typing import Any
|
||||
import numpy as np
|
||||
import pandas as pd
|
||||
import requests
|
||||
|
||||
# import talib
|
||||
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
|
||||
|
||||
import modules.utils as utils
|
||||
import modules.aster_auth as aster_auth
|
||||
import modules.extended_auth as extend_auth
|
||||
|
||||
### Database ###
|
||||
EXTEND_CLIENT = None
|
||||
CON: AsyncContextManager | None = None
|
||||
VAL_KEY = None
|
||||
|
||||
### Logging ###
|
||||
load_dotenv()
|
||||
LOG_FILEPATH: str = os.getenv("LOGS_PATH") + '/Fund_Rate_Algo.log'
|
||||
|
||||
### CONSTANTS ###
|
||||
ASTER_ALLOW_ORDERING: bool = False
|
||||
EXTEND_ALLOW_ORDERING: bool = False
|
||||
LOOP_SLEEP_SEC = 1
|
||||
PRICE_WORSENER_ASTER = 0.00
|
||||
PRICE_WORSENER_EXTEND = 0.0
|
||||
|
||||
MIN_TIME_TO_FUNDING: int = 1000 * 60 * 7 # 5 minutes.
|
||||
|
||||
ASTER_LH_ASSET: str = 'ETH'
|
||||
ASTER_RH_ASSET: str = 'USDT'
|
||||
ASTER_TICKER: str = ASTER_LH_ASSET + ASTER_RH_ASSET
|
||||
EXTEND_LH_ASSET: str = 'ETH'
|
||||
EXTEND_RH_ASSET: str = 'USD'
|
||||
EXTEND_TICKER: str = EXTEND_LH_ASSET + '-' + EXTEND_RH_ASSET
|
||||
|
||||
TARGET_OPEN_CASH_POSITION: float = 10 # Each side (alpha and hedge)
|
||||
|
||||
### GLOBALS ###
|
||||
ASTER_MULT = 150
|
||||
EXTEND_MULT = 50
|
||||
MAX_TARGET_NOTIONAL = min([ASTER_MULT, EXTEND_MULT]) * TARGET_OPEN_CASH_POSITION
|
||||
|
||||
ASTER_MIN_ORDER_QTY = 0.001
|
||||
EXTEND_MIN_ORDER_QTY = 0.01
|
||||
|
||||
ASTER_AVAIL_COLLATERAL = 0
|
||||
EXTEND_AVAIL_COLLATERAL = 0
|
||||
|
||||
ASTER_NOTIONAL_POSITION = 0
|
||||
EXTEND_NOTIONAL_POSITION = 0
|
||||
|
||||
ASTER_OPEN_ORDERS = []
|
||||
EXTEND_OPEN_ORDERS = []
|
||||
|
||||
# ASTER_OPEN_POSITIONS = []
|
||||
# EXTEND_OPEN_POSITIONS = []
|
||||
|
||||
@dataclass(kw_only=True)
|
||||
class Valkey_Stream:
|
||||
channel: str
|
||||
data: Any = None
|
||||
none_fill: Any = None
|
||||
|
||||
async def update(self):
|
||||
r = VAL_KEY.get(self.channel)
|
||||
self.data = json.loads(r) if r is not None else self.none_fill
|
||||
|
||||
@dataclass(kw_only=True)
|
||||
class Position:
|
||||
market: str
|
||||
notional: float
|
||||
qty: float
|
||||
|
||||
@dataclass(kw_only=True)
|
||||
class Open_Positions:
|
||||
Valkey: Valkey_Stream
|
||||
Positions: list[Position] = field(default_factory = list)
|
||||
|
||||
async def update(self) -> None:
|
||||
self.Valkey = await self.Valkey.update()
|
||||
|
||||
### Collateral ###
|
||||
@dataclass(kw_only=True)
|
||||
class Asset:
|
||||
symbol: str
|
||||
balance: float
|
||||
# min_order_qty: float
|
||||
|
||||
@dataclass(kw_only=True)
|
||||
class Collateral:
|
||||
Valkey: Valkey_Stream
|
||||
# Last_Updated_Ts_Ms: int
|
||||
# Last_Pulled_Ts_Ms: int
|
||||
Assets: list[Asset] = field(default_factory = list)
|
||||
|
||||
async def update(self) -> None:
|
||||
self.Valkey = await self.Valkey.update()
|
||||
|
||||
### Orders ###
|
||||
@dataclass(kw_only=True)
|
||||
class Order:
|
||||
symbol: str
|
||||
order_id: str
|
||||
client_order_id: str
|
||||
side: str
|
||||
order_type: str
|
||||
original_qty: float
|
||||
original_price: float
|
||||
order_status: str
|
||||
last_filled_qty: float
|
||||
last_filled_price: float
|
||||
commission: float
|
||||
trade_is_maker: bool
|
||||
|
||||
@dataclass(kw_only=True)
|
||||
class Order_Updates:
|
||||
# Last_Updated_Ts_Ms: int
|
||||
# Last_Pulled_Ts_Ms: int
|
||||
Valkey: Valkey_Stream
|
||||
Orders: list[Order] = field(default_factory = list)
|
||||
|
||||
async def update(self) -> None:
|
||||
self.Valkey = await self.Valkey.update()
|
||||
|
||||
### Funding Rate ###
|
||||
@dataclass(kw_only=True)
|
||||
class Funding_Rate:
|
||||
# Last_Updated_Ts_Ms: int
|
||||
# Last_Pulled_Ts_Ms: int
|
||||
Valkey: Valkey_Stream
|
||||
timestamp_arrival: int
|
||||
timestamp_msg: int
|
||||
symbol: str
|
||||
funding_rate: float
|
||||
next_funding_time_ts_ms: int
|
||||
mark_price: float
|
||||
index_price: float
|
||||
estimated_settle_price: float
|
||||
|
||||
async def update(self) -> None:
|
||||
self.Valkey = await self.Valkey.update()
|
||||
|
||||
### Markets Info ###
|
||||
@dataclass(kw_only=True)
|
||||
class Market:
|
||||
symbol: str
|
||||
min_order_qty: float
|
||||
|
||||
@dataclass(kw_only=True)
|
||||
class Markets_Details:
|
||||
Markets: list[Market] = field(default_factory=list)
|
||||
|
||||
|
||||
### Exchanges ###
|
||||
@dataclass(kw_only=True)
|
||||
class Perpetual_Exchange:
|
||||
Order_Updates: Order_Updates
|
||||
Position_Updates: Open_Positions
|
||||
Collateral_Updates: Collateral
|
||||
Funding_Rate: Funding_Rate
|
||||
Markets: Markets_Details
|
||||
mult: int
|
||||
lh_asset: str
|
||||
rh_asset: str
|
||||
symbol_asset_separator: str = ''
|
||||
symbol: 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()
|
||||
|
||||
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'
|
||||
|
||||
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 = '-'
|
||||
|
||||
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))
|
||||
|
||||
|
||||
# EXCHANGES: list = [ Aster(), Extend() ]
|
||||
|
||||
### FLAGS ###
|
||||
@dataclass(kw_only=True)
|
||||
class Flags:
|
||||
LIQUIDATE_POS_AND_KILL_ALGO_FLAG: bool = False
|
||||
NET_FUNDING_IS_ZERO: bool = False
|
||||
Flags = Flags()
|
||||
|
||||
|
||||
### UTILS ###
|
||||
def round_decimal_down(value, decimal_places):
|
||||
# Construct precision string like '0.01' for 2 places
|
||||
fmt = f'0.{"0" * decimal_places}' if decimal_places > 0 else '0'
|
||||
precision = Decimal(fmt)
|
||||
return Decimal(str(value)).quantize(precision, rounding=ROUND_DOWN)
|
||||
|
||||
### OPEN ORDERS ###
|
||||
async def get_aster_open_orders():
|
||||
global ASTER_OPEN_ORDERS
|
||||
|
||||
fut_acct_openOrders = {
|
||||
"url": "/fapi/v3/openOrders",
|
||||
"method": "GET",
|
||||
"params": {}
|
||||
}
|
||||
ASTER_OPEN_ORDERS = await aster_auth.post_authenticated_url(fut_acct_openOrders)
|
||||
|
||||
async def get_extend_open_orders():
|
||||
global EXTEND_OPEN_ORDERS
|
||||
|
||||
EXTEND_OPEN_ORDERS = list(dict(await EXTEND_CLIENT.account.get_open_orders()).get('data', 0))
|
||||
|
||||
### WALLLET ###
|
||||
async def get_aster_collateral():
|
||||
global ASTER_AVAIL_COLLATERAL
|
||||
|
||||
fut_acct_balances = {
|
||||
"url": "/fapi/v3/balance",
|
||||
"method": "GET",
|
||||
"params": {}
|
||||
}
|
||||
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):
|
||||
global ASTER_NOTIONAL_POSITION
|
||||
global ASTER_MULT
|
||||
|
||||
if not resp:
|
||||
fut_acct_positionRisk = {
|
||||
"url": "/fapi/v3/positionRisk",
|
||||
"method": "GET",
|
||||
"params": {
|
||||
'symbol': ASTER_TICKER,
|
||||
}
|
||||
}
|
||||
resp = await aster_auth.post_authenticated_url(fut_acct_positionRisk)
|
||||
|
||||
d = [x for x in resp if x.get('symbol', None) == ASTER_TICKER][0]
|
||||
if len(d) < 1:
|
||||
logging.info(f'BAD NOTIONAL - ASTER CHANGE: Empty d: {d}; resp: {resp}')
|
||||
kill_algo()
|
||||
|
||||
aster_unrealized_pnl = float(d['unrealized_pnl']) if d.get('unrealized_pnl') is not None else float(d['unRealizedProfit'])
|
||||
|
||||
if d.get('notional') is not None:
|
||||
notional = float(d['notional'])
|
||||
else:
|
||||
notional = float(d['position_amount'])*float(d['entry_price'])
|
||||
|
||||
previous_notional_position = ASTER_NOTIONAL_POSITION
|
||||
ASTER_NOTIONAL_POSITION = notional - aster_unrealized_pnl
|
||||
if not resp:
|
||||
ASTER_MULT = float(d['leverage'])
|
||||
if abs(ASTER_NOTIONAL_POSITION) > MAX_TARGET_NOTIONAL*1.01:
|
||||
logging.info(f'BAD NOTIONAL - ASTER CHANGE: {ASTER_NOTIONAL_POSITION}; UR PNL: {aster_unrealized_pnl}; MULT: {ASTER_MULT}; d: {d}; resp: {resp}')
|
||||
kill_algo()
|
||||
if ASTER_NOTIONAL_POSITION != previous_notional_position:
|
||||
logging.info(f'ASTER NOTIONAL CHANGE: {ASTER_NOTIONAL_POSITION:.2f}; UR PNL: {aster_unrealized_pnl:.2f}; MULT: {ASTER_MULT:.0f}; resp: {bool(resp)}')
|
||||
|
||||
async def get_extend_collateral():
|
||||
global EXTEND_AVAIL_COLLATERAL
|
||||
|
||||
get_bals = dict(dict(await EXTEND_CLIENT.account.get_balance()).get('data', {}))
|
||||
EXTEND_AVAIL_COLLATERAL = get_bals.get('available_for_trade', 0) if get_bals.get('collateral_name', None)==EXTEND_RH_ASSET else 0
|
||||
|
||||
async def get_extend_notional(resp: dict | None = None):
|
||||
global EXTEND_NOTIONAL_POSITION
|
||||
global EXTEND_MULT
|
||||
|
||||
if not resp:
|
||||
resp = dict(await EXTEND_CLIENT.account.get_positions()).get('data', {})
|
||||
|
||||
pos_dict = [dict(d) for d in resp if dict(d).get('market') == EXTEND_TICKER]
|
||||
pos_dict = pos_dict[0]
|
||||
unrealized_pnl = pos_dict.get('unrealised_pnl', 0)
|
||||
previous_notional_position = EXTEND_NOTIONAL_POSITION
|
||||
EXTEND_NOTIONAL_POSITION = float(pos_dict.get('value', 0)) - float(unrealized_pnl)
|
||||
EXTEND_MULT = pos_dict.get('leverage', EXTEND_MULT)
|
||||
if EXTEND_NOTIONAL_POSITION != previous_notional_position:
|
||||
logging.info(f'EXTEND NOTIONAL CHANGE: {EXTEND_NOTIONAL_POSITION:.2f}; UR PNL: {unrealized_pnl:.2f}; MULT: {EXTEND_MULT:.0f}; resp: {bool(resp)}')
|
||||
|
||||
|
||||
### EXCHANGE INFO ###
|
||||
async def get_aster_exch_info():
|
||||
global ASTER_MIN_ORDER_QTY
|
||||
|
||||
fut_acct_exchangeInfo = {
|
||||
"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]
|
||||
ASTER_MIN_ORDER_QTY = float(f['minQty'])
|
||||
|
||||
async def get_extend_exch_info():
|
||||
global EXTEND_MIN_ORDER_QTY
|
||||
|
||||
r = await EXTEND_CLIENT.markets_info.get_markets_dict()
|
||||
EXTEND_MIN_ORDER_QTY = float(r['ETH-USD'].trading_config.min_order_size)
|
||||
|
||||
### CANCEL ORDERS ###
|
||||
async def aster_cancel_all_orders():
|
||||
cancel_all_open_orders = {
|
||||
"url": "/fapi/v3/allOpenOrders",
|
||||
"method": "DELETE",
|
||||
"params": {
|
||||
'symbol': 'ETHUSDT',
|
||||
}
|
||||
}
|
||||
r = await aster_auth.post_authenticated_url(cancel_all_open_orders)
|
||||
logging.info(f'ASTER CANCEL ALL OPEN ORDERS RESP: {r}')
|
||||
|
||||
async def extend_cancel_all_orders():
|
||||
r = await EXTEND_CLIENT.orders.mass_cancel(markets=[EXTEND_TICKER])
|
||||
logging.info(f'EXTEND CANCEL ALL OPEN ORDERS RESP: {r}')
|
||||
|
||||
### KILL ALGO ###
|
||||
async def kill_algo():
|
||||
await aster_cancel_all_orders()
|
||||
await extend_cancel_all_orders()
|
||||
logging.info('ALGO KILL FLAG ACTIVATED; CANCELLING OPEN ORDERS AND SHUTTING DOWN')
|
||||
raise ValueError('KILL FLAG ACTIVATED')
|
||||
|
||||
### ROUTES ###
|
||||
# async def aster_remainder_route():
|
||||
# # Check open orders...cancel replace or new order?
|
||||
# # Check collateral to confirm you have enough money to trade
|
||||
# # if CR, what should be the new price? has it changed? maybe no action needed? how long has it been working?
|
||||
# # if not enough collateral then need to liquidate and kill algo - flip flag
|
||||
|
||||
# # if good to order, then create and post order. ADD to LOCAL OPEN ORDERS LIST
|
||||
|
||||
|
||||
# pass
|
||||
|
||||
# async def extend_remainder_route():
|
||||
# pass
|
||||
|
||||
|
||||
### ALGO LOOP ###
|
||||
async def run_algo():
|
||||
|
||||
try:
|
||||
while True:
|
||||
loop_start = time.time()
|
||||
print('__________Start___________')
|
||||
|
||||
### Load Data from Feedhandlers ###
|
||||
ASTER_FUND_RATE_DICT = json.loads(VAL_KEY.get('fund_rate_aster'))
|
||||
EXTENDED_FUND_RATE_DICT = json.loads(VAL_KEY.get('fund_rate_extended'))
|
||||
|
||||
ASTER_FUND_RATE = float(ASTER_FUND_RATE_DICT.get('funding_rate', 0))
|
||||
EXTEND_FUND_RATE = float(EXTENDED_FUND_RATE_DICT.get('funding_rate', 0))
|
||||
ASTER_FUND_RATE_TIME = float(ASTER_FUND_RATE_DICT.get('next_funding_time_ts_ms', 0))
|
||||
EXTEND_FUND_RATE_TIME = float(EXTENDED_FUND_RATE_DICT.get('next_funding_time_ts_ms', 0))
|
||||
|
||||
ASTER_TICKER_DICT = json.loads(VAL_KEY.get('fut_ticker_aster'))
|
||||
EXTENDED_TICKER_DICT = json.loads(VAL_KEY.get('fut_ticker_extended'))
|
||||
|
||||
### 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 []
|
||||
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 []
|
||||
|
||||
### 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 []
|
||||
|
||||
### 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 []
|
||||
|
||||
# CHECK NO MORE THAN 1 OPEN ORDER ON EITHER EXCHANGE #
|
||||
if len(ASTER_OPEN_ORDERS) > 1 or len(EXTEND_OPEN_ORDERS) > 1:
|
||||
logging.info(f'MORE THAN 1 ORDER OPEN - KILLING ALGO: ASTER_OPEN_ORDERS ({len(ASTER_OPEN_ORDERS)}): {ASTER_OPEN_ORDERS}; EXTEND_OPEN_ORDERS ({len(EXTEND_OPEN_ORDERS)}): {EXTEND_OPEN_ORDERS}')
|
||||
await kill_algo()
|
||||
raise ValueError('NOT HERE: MORE THAN 1 ORDER OPEN - KILLING ALGO: ASTER_OPEN_ORDERS')
|
||||
|
||||
### CHECK TIME TO FUNDING AND WHETHER TO BE ACTIVE ###
|
||||
now_ms = round(datetime.now().timestamp()*1000)
|
||||
time_to_funding_ms = min([ASTER_FUND_RATE_TIME, EXTEND_FUND_RATE_TIME]) - now_ms
|
||||
if ( time_to_funding_ms > MIN_TIME_TO_FUNDING ) and (not ASTER_OPEN_ORDERS) and (not EXTEND_OPEN_ORDERS):
|
||||
print(f'Outside action window (minutes) and no active order (sleeping for 5 sec): {pd.to_datetime(time_to_funding_ms, unit='ms').minute} > {pd.to_datetime(MIN_TIME_TO_FUNDING, unit='ms').minute}')
|
||||
time.sleep(5)
|
||||
continue
|
||||
|
||||
if len(ASTER_WS_POS_UPDATES) > 0:
|
||||
await get_aster_notional_position(resp=ASTER_WS_POS_UPDATES)
|
||||
###### *** returned 0 notional even though had a position, need to handle and safety check to not order above max notional.
|
||||
|
||||
if len(EXTEND_WS_POS_UPDATES) > 0:
|
||||
await get_extend_notional(resp=EXTEND_WS_POS_UPDATES)
|
||||
|
||||
if ASTER_WS_ORDER_UPDATES is not None:
|
||||
for idx, o in enumerate(ASTER_OPEN_ORDERS):
|
||||
order_id = o.get('order_id') if o.get('order_id') is not None else o.get('orderId')
|
||||
order_orig_status = o['status']
|
||||
order_update = [ou for ou in ASTER_WS_ORDER_UPDATES if ou.get('order_id', None) == order_id]
|
||||
|
||||
if len(order_update) > 0:
|
||||
order_update = order_update[0]
|
||||
order_update_status = order_update.get('status') if order_update.get('status') is not None else order_update.get('order_status')
|
||||
order_status_changed = order_orig_status.upper() != order_update_status.upper()
|
||||
|
||||
if order_status_changed:
|
||||
logging.info(f'ASTER ORDER ({order_id}): {order_orig_status} -> {order_update_status}')
|
||||
ASTER_OPEN_ORDERS[idx] = order_update
|
||||
if order_update_status in ['CANCELED','EXPIRED']:
|
||||
logging.info(f'ASTER ORDER CANCELLED or EXPIRED: {order_id}')
|
||||
ASTER_OPEN_ORDERS.pop(idx)
|
||||
elif order_update_status in ['PARTIALLY_FILLED']:
|
||||
logging.info(f'ASTER ORDER PARTIALLY FILLED: {order_id}')
|
||||
await get_aster_collateral()
|
||||
await get_aster_notional_position()
|
||||
elif order_update_status in ['FILLED']:
|
||||
logging.info(f'ASTER ORDER FILLED: {order_id}')
|
||||
ASTER_OPEN_ORDERS.pop(idx)
|
||||
await get_aster_collateral()
|
||||
await get_aster_notional_position()
|
||||
else:
|
||||
logging.critical(f'EXTEND ORDER STATUS CHG TO UNEXPECTED VALUE, KILLING... ({order_id}): {order_orig_status} -> {order_update_status}')
|
||||
if EXTEND_WS_ORDER_UPDATES is not None:
|
||||
for idx, o in enumerate(EXTEND_OPEN_ORDERS):
|
||||
o = dict(o)
|
||||
order_id = o.get('order_id') if o.get('order_id') is not None else o.get('id')
|
||||
order_orig_status = o['status']
|
||||
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()
|
||||
|
||||
if order_status_changed:
|
||||
logging.info(f'EXTEND ORDER ({order_id}): {order_orig_status} -> {order_update_status}')
|
||||
EXTEND_OPEN_ORDERS[idx] = order_update
|
||||
if order_update_status in ['CANCELLED','EXPIRED','REJECTED']:
|
||||
logging.info(f'EXTEND ORDER CANCELLED or EXPIRED: {order_id}')
|
||||
EXTEND_OPEN_ORDERS.pop(idx)
|
||||
elif order_update_status in ['PARTIALLY_FILLED']:
|
||||
logging.info(f'EXTEND ORDER PARTIALLY FILLED: {order_id}')
|
||||
await get_extend_collateral()
|
||||
await get_extend_notional()
|
||||
elif order_update_status in ['FILLED']:
|
||||
logging.info(f'EXTEND ORDER FILLED: {order_id}')
|
||||
EXTEND_OPEN_ORDERS.pop(idx)
|
||||
await get_extend_collateral()
|
||||
await get_extend_notional()
|
||||
else:
|
||||
logging.critical(f'EXTEND ORDER STATUS CHG TO UNEXPECTED VALUE, KILLING... ({order_id}): {order_orig_status} -> {order_update_status}')
|
||||
|
||||
|
||||
ASTER_PAYOUT_DIRECTION_STR = 'LONG PAYS SHORT' if ASTER_FUND_RATE > 0 else 'SHORT PAYS LONG'
|
||||
EXTEND_PAYOUT_DIRECTION_STR = 'LONG PAYS SHORT' if EXTEND_FUND_RATE > 0 else 'SHORT PAYS LONG'
|
||||
|
||||
min_between_fundings = round((abs(ASTER_FUND_RATE_TIME - EXTEND_FUND_RATE_TIME) / 1000 / 60))
|
||||
FUNDINGS_AT_SAME_TIME_NEXT_HR = min_between_fundings < 5
|
||||
# FUNDINGS_AT_SAME_TIME_NEXT_HR = ( (ASTER_FUND_RATE_TIME < 60*60*1000) and (EXTEND_FUND_RATE < 60*60*1000) )
|
||||
|
||||
|
||||
if ( abs(ASTER_FUND_RATE) > abs(EXTEND_FUND_RATE) ) and FUNDINGS_AT_SAME_TIME_NEXT_HR:
|
||||
ALPHA_EXCH = 'ASTER'
|
||||
ALPHA_FUND_RATE = ASTER_FUND_RATE
|
||||
else:
|
||||
ALPHA_EXCH = 'EXTEND'
|
||||
ALPHA_FUND_RATE = EXTEND_FUND_RATE
|
||||
|
||||
if ALPHA_FUND_RATE < 0:
|
||||
ALPHA_CARRY_SIDE = 'BUY'
|
||||
ALPHA_TGT_NOTIONAL = MAX_TARGET_NOTIONAL
|
||||
else:
|
||||
ALPHA_CARRY_SIDE = 'SELL'
|
||||
ALPHA_TGT_NOTIONAL = MAX_TARGET_NOTIONAL*-1
|
||||
|
||||
|
||||
def calc_next_net_fund_rate(FUNDINGS_AT_SAME_TIME_NEXT_HR: bool) -> float:
|
||||
if FUNDINGS_AT_SAME_TIME_NEXT_HR:
|
||||
return ASTER_FUND_RATE + EXTEND_FUND_RATE
|
||||
else:
|
||||
return EXTEND_FUND_RATE
|
||||
|
||||
NEXT_NET_FUNDING_RATE = calc_next_net_fund_rate(FUNDINGS_AT_SAME_TIME_NEXT_HR)
|
||||
Flags.NET_FUNDING_IS_ZERO = NEXT_NET_FUNDING_RATE == 0.00
|
||||
if Flags.NET_FUNDING_IS_ZERO:
|
||||
logging.info('NET FUNDING = 0.00; Cancelling Open Orders; Wait Until Non-Zero.')
|
||||
|
||||
ALPHA_TGT_NOTIONAL = 0.00
|
||||
|
||||
if ALPHA_EXCH == 'EXTEND':
|
||||
ASTER_TGT_NOTIONAL = ALPHA_TGT_NOTIONAL*-1
|
||||
EXTEND_TGT_NOTIONAL = ALPHA_TGT_NOTIONAL
|
||||
if ALPHA_CARRY_SIDE == 'BUY':
|
||||
ASTER_TOB_PX = float(ASTER_TICKER_DICT['best_ask_px'])
|
||||
EXTEND_TOB_PX = float(EXTENDED_TICKER_DICT['best_bid_px'])
|
||||
else:
|
||||
ASTER_TOB_PX = float(ASTER_TICKER_DICT['best_bid_px'])
|
||||
EXTEND_TOB_PX = float(EXTENDED_TICKER_DICT['best_ask_px'])
|
||||
else:
|
||||
ASTER_TGT_NOTIONAL = ALPHA_TGT_NOTIONAL
|
||||
EXTEND_TGT_NOTIONAL = ALPHA_TGT_NOTIONAL*-1
|
||||
if ALPHA_CARRY_SIDE == 'BUY':
|
||||
ASTER_TOB_PX = float(ASTER_TICKER_DICT['best_bid_px'])
|
||||
EXTEND_TOB_PX = float(EXTENDED_TICKER_DICT['best_ask_px'])
|
||||
else:
|
||||
ASTER_TOB_PX = float(ASTER_TICKER_DICT['best_ask_px'])
|
||||
EXTEND_TOB_PX = float(EXTENDED_TICKER_DICT['best_bid_px'])
|
||||
|
||||
|
||||
ASTER_TGT_TAIL = ASTER_TGT_NOTIONAL - ASTER_NOTIONAL_POSITION
|
||||
EXTEND_TGT_TAIL = EXTEND_TGT_NOTIONAL - EXTEND_NOTIONAL_POSITION
|
||||
|
||||
ASTER_TGT_TAIL_BASE_QTY = Decimal(str(float(ASTER_TGT_TAIL) / float(ASTER_TOB_PX))).quantize(Decimal(str(0.001)), rounding=ROUND_DOWN)
|
||||
EXTEND_TGT_TAIL_BASE_QTY = Decimal(str(float(EXTEND_TGT_TAIL) / float(EXTEND_TOB_PX))).quantize(Decimal(str(0.001)), rounding=ROUND_DOWN)
|
||||
|
||||
ASTER_TGT_TAIL_ORDERABLE = abs(ASTER_TGT_TAIL_BASE_QTY) >= ASTER_MIN_ORDER_QTY
|
||||
EXTEND_TGT_TAIL_ORDERABLE = abs(EXTEND_TGT_TAIL_BASE_QTY) >= EXTEND_MIN_ORDER_QTY
|
||||
|
||||
print(f'''
|
||||
{pd.to_datetime(ASTER_FUND_RATE_TIME, unit='ms')} ({(pd.to_datetime(ASTER_FUND_RATE_TIME, unit='ms')-datetime.now()):}) | {pd.to_datetime(EXTEND_FUND_RATE_TIME, unit='ms')} ({(pd.to_datetime(EXTEND_FUND_RATE_TIME, unit='ms')-datetime.now()):})
|
||||
ASTER: {ASTER_FUND_RATE:.6%} [{ASTER_FUND_RATE*10_000:.2f}bps] [{ASTER_FUND_RATE*1_000_000:.0f}pips] | EXTEND: {EXTEND_FUND_RATE:.6%} [{EXTEND_FUND_RATE*10_000:.2f}bps] [{EXTEND_FUND_RATE*1_000_000:.0f}pips]
|
||||
ASTER: {ASTER_PAYOUT_DIRECTION_STR} | EXTEND: {EXTEND_PAYOUT_DIRECTION_STR}
|
||||
ASTER: [ Available Collateral: {ASTER_AVAIL_COLLATERAL:.4f} ] | EXTEND: [ Available Collateral: {EXTEND_AVAIL_COLLATERAL:.4f} ]
|
||||
ASTER: [ Notional Position $ : {ASTER_NOTIONAL_POSITION:.4f} ] | EXTEND: [ Notional Position $ : {EXTEND_NOTIONAL_POSITION:.4f} ]
|
||||
|
||||
SAME TIME? : {FUNDINGS_AT_SAME_TIME_NEXT_HR} [ Minutes Between Fundings: {min_between_fundings} ]
|
||||
NET FUNDING : {NEXT_NET_FUNDING_RATE:.6%} [{NEXT_NET_FUNDING_RATE*10_000:.2f}bps] [{NEXT_NET_FUNDING_RATE*1_000_000:.0f}pips]; Is Zero?: {Flags.NET_FUNDING_IS_ZERO}
|
||||
ALPHA SIDE : {ALPHA_EXCH} [{ALPHA_CARRY_SIDE}]
|
||||
|
||||
TGT NOTIONAL: $ {MAX_TARGET_NOTIONAL if not Flags.NET_FUNDING_IS_ZERO else 0.00}
|
||||
|
||||
ASTER: {ASTER_NOTIONAL_POSITION:.4f} -> {ASTER_TGT_NOTIONAL:.2f} [ Remain: {ASTER_TGT_TAIL:.4f} ] | EXTEND: {EXTEND_NOTIONAL_POSITION:.4f} -> {EXTEND_TGT_NOTIONAL:.2f} [ Remain: {EXTEND_TGT_TAIL:.4f} ]
|
||||
ASTER: {ASTER_TGT_NOTIONAL:.4f} - {ASTER_NOTIONAL_POSITION:.4f} = Tail: {ASTER_TGT_TAIL:4f} | EXTEND: {EXTEND_TGT_NOTIONAL:.4f} - {EXTEND_NOTIONAL_POSITION:.4f} = Tail: {EXTEND_TGT_TAIL:4f}
|
||||
ASTER: {ASTER_TGT_TAIL_BASE_QTY:.4f} > {ASTER_MIN_ORDER_QTY:.4f} min [ Order: {ASTER_TGT_TAIL_ORDERABLE} ] | EXTEND: {EXTEND_TGT_TAIL_BASE_QTY:.4f} > {EXTEND_MIN_ORDER_QTY:.4f} min [ Order: {EXTEND_TGT_TAIL_ORDERABLE} ]
|
||||
|
||||
--- ASTER OPEN ORDERS ---
|
||||
{ASTER_OPEN_ORDERS}
|
||||
|
||||
--- EXTEND OPEN ORDERS ---
|
||||
{EXTEND_OPEN_ORDERS}
|
||||
''')
|
||||
|
||||
|
||||
### ROUTES ###
|
||||
# ASTER
|
||||
if ASTER_TGT_TAIL_ORDERABLE and ASTER_ALLOW_ORDERING:
|
||||
symbol = ASTER_TICKER
|
||||
side = 'BUY' if ASTER_TGT_TAIL_BASE_QTY > 0.00 else 'SELL'
|
||||
qty = str(abs(ASTER_TGT_TAIL_BASE_QTY))
|
||||
price = ASTER_TOB_PX - PRICE_WORSENER_ASTER if side == 'BUY' else ASTER_TOB_PX + PRICE_WORSENER_ASTER
|
||||
|
||||
if abs(abs(float(ASTER_TGT_TAIL_BASE_QTY))*float(price)) + abs(ASTER_NOTIONAL_POSITION) > MAX_TARGET_NOTIONAL*1.01:
|
||||
pass
|
||||
logging.info(f'TRYING TO ORDER OVER MAX NOTIOANL - ASTER: {ASTER_NOTIONAL_POSITION} + {float(ASTER_TGT_TAIL_BASE_QTY)*float(price)} (qty: {float(ASTER_TGT_TAIL_BASE_QTY):.2f}; px: {float(price):.2f})')
|
||||
# await aster_remainder_route()
|
||||
if ASTER_OPEN_ORDERS:
|
||||
open_order_id = ASTER_OPEN_ORDERS[0].get('order_id') if ASTER_OPEN_ORDERS[0].get('order_id') is not None else ASTER_OPEN_ORDERS[0]['orderId']
|
||||
open_order_px = float(ASTER_OPEN_ORDERS[0].get('price')) if ASTER_OPEN_ORDERS[0].get('price') is not None else float(ASTER_OPEN_ORDERS[0]['original_price'])
|
||||
if round(open_order_px - float(price), 2) == 0.00:
|
||||
logging.info('ASTER OPEN ORDER NO PX CHG; SKIPPING')
|
||||
place_order = False
|
||||
else:
|
||||
cancel_order = {
|
||||
"url": "/fapi/v3/order",
|
||||
"method": "DELETE",
|
||||
"params": {
|
||||
'symbol': ASTER_TICKER,
|
||||
'orderId': open_order_id,
|
||||
}
|
||||
}
|
||||
cr = await aster_auth.post_authenticated_url(cancel_order)
|
||||
if cr.get('status', None) == 'CANCELED':
|
||||
ASTER_OPEN_ORDERS.pop(0)
|
||||
place_order = True
|
||||
else:
|
||||
logging.warning(f'ASTER ORDER FAILED TO CANCEL DURING CR ({open_order_id}): RESP {cr}')
|
||||
place_order = False
|
||||
else:
|
||||
place_order = True
|
||||
|
||||
if ASTER_TGT_TAIL_BASE_QTY == 0.00:
|
||||
place_order = False
|
||||
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)
|
||||
post_order = {
|
||||
"url": "/fapi/v3/order",
|
||||
"method": "POST",
|
||||
"params": {
|
||||
'symbol': symbol,
|
||||
'side': side,
|
||||
'type': 'LIMIT',
|
||||
'timeInForce': 'GTC',
|
||||
'quantity': qty,
|
||||
'price': price,
|
||||
}
|
||||
}
|
||||
order_resp = await aster_auth.post_authenticated_url(post_order)
|
||||
if order_resp.get('orderId', None) is not None:
|
||||
order_resp['original_price'] = price
|
||||
ASTER_OPEN_ORDERS.append(order_resp)
|
||||
utils.send_tg_alert(f'FR_ALGO - ASTER Order. Start_$: {ASTER_NOTIONAL_POSITION:.2f}; Value: {float(ASTER_TGT_TAIL_BASE_QTY)*float(price):.2f}; Price: {float(price):.2f}')
|
||||
logging.info(f'ASTER ORDER PLACED SUCCESS: {order_resp}')
|
||||
else:
|
||||
logging.warning('ASTER PLACE ORDER CHECKS FAILED, SKIPPING')
|
||||
|
||||
elif not(ASTER_TGT_TAIL_ORDERABLE) and ASTER_OPEN_ORDERS:
|
||||
logging.info('ASTER HAS NO TAIL BUT OPEN ORDERS - CANCELLING OPEN ORDERS')
|
||||
await aster_cancel_all_orders()
|
||||
|
||||
# EXTEND
|
||||
if EXTEND_TGT_TAIL_ORDERABLE and EXTEND_ALLOW_ORDERING:
|
||||
symbol = EXTEND_TICKER
|
||||
side = OrderSide.BUY if EXTEND_TGT_TAIL_BASE_QTY > 0.00 else OrderSide.SELL
|
||||
qty = Decimal(str(abs(EXTEND_TGT_TAIL_BASE_QTY)))
|
||||
price = EXTEND_TOB_PX - PRICE_WORSENER_EXTEND if side == 'BUY' else EXTEND_TOB_PX + PRICE_WORSENER_EXTEND
|
||||
|
||||
if abs(float(EXTEND_TGT_TAIL_BASE_QTY)*float(price)) + abs(float(EXTEND_NOTIONAL_POSITION)) > MAX_TARGET_NOTIONAL*1.01:
|
||||
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})')
|
||||
pass
|
||||
# await extend_remainder_route()
|
||||
if EXTEND_OPEN_ORDERS:
|
||||
open_order_dict = dict(EXTEND_OPEN_ORDERS[0])
|
||||
open_order_id = open_order_dict['external_id']
|
||||
open_order_px = float(open_order_dict['price'])
|
||||
place_order = True
|
||||
else:
|
||||
open_order_id = None
|
||||
open_order_px = 0
|
||||
place_order = True
|
||||
if place_order:
|
||||
price = Decimal(str(price)).quantize(Decimal(str(0.01)), rounding=ROUND_DOWN)
|
||||
if round(open_order_px - float(price), 2) == 0.00:
|
||||
logging.info('EXTEND OPEN ORDER NO PX CHG; SKIPPING')
|
||||
else:
|
||||
order_resp = await EXTEND_CLIENT.place_order(
|
||||
market_name=symbol,
|
||||
amount_of_synthetic=qty,
|
||||
price=price,
|
||||
side=side,
|
||||
taker_fee=Decimal("0.00025"),
|
||||
previous_order_id=open_order_id,
|
||||
)
|
||||
order_resp_dict = dict(order_resp)
|
||||
if order_resp_dict.get('status', None) == 'OK':
|
||||
if EXTEND_OPEN_ORDERS:
|
||||
EXTEND_OPEN_ORDERS.pop(0)
|
||||
|
||||
order_dict = dict(order_resp_dict['data'])
|
||||
order_dict['status'] = 'NEW'
|
||||
order_dict['price'] = str(price)
|
||||
|
||||
EXTEND_OPEN_ORDERS.append(order_dict)
|
||||
utils.send_tg_alert(f'FR_ALGO - EXTEND Order. Start_$: {EXTEND_NOTIONAL_POSITION:.2f}; Value: {float(EXTEND_TGT_TAIL_BASE_QTY)*float(price):.2f}; Price: {float(price):.2f}')
|
||||
logging.info(f'EXTEND ORDER PLACED SUCCESS: {order_dict}')
|
||||
else:
|
||||
logging.warning('EXTEND PLACE ORDER CHECKS FAILED, SKIPPING')
|
||||
|
||||
elif not(EXTEND_TGT_TAIL_ORDERABLE) and EXTEND_OPEN_ORDERS:
|
||||
logging.info('EXTEND HAS NO TAIL BUT OPEN ORDERS - CANCELLING OPEN ORDERS')
|
||||
await extend_cancel_all_orders()
|
||||
|
||||
print(f'__________ End ___________ (Algo Engine ms: {(time.time() - loop_start)*1000})')
|
||||
|
||||
time.sleep(LOOP_SLEEP_SEC)
|
||||
|
||||
except KeyboardInterrupt:
|
||||
logging.info('CANCELLING OPEN ORDERS')
|
||||
await kill_algo()
|
||||
except Exception as e:
|
||||
logging.error(traceback.format_exc())
|
||||
logging.critical(f'*** ALGO ENGINE CRASHED: {e}')
|
||||
logging.info('CANCELLING OPEN ORDERS')
|
||||
utils.send_tg_alert(f'FR_ALGO_CRASHED: {str(e)}')
|
||||
await kill_algo()
|
||||
|
||||
|
||||
### MAIN STARTUP ###
|
||||
async def main():
|
||||
global EXTEND_CLIENT
|
||||
global VAL_KEY
|
||||
global CON
|
||||
|
||||
_, EXTEND_CLIENT = await extend_auth.create_auth_account_and_trading_client()
|
||||
VAL_KEY = valkey.Valkey(host='localhost', port=6379, db=0, decode_responses=True)
|
||||
engine = create_async_engine('mysql+asyncmy://root:pwd@localhost/fund_rate')
|
||||
|
||||
async with engine.connect() as CON:
|
||||
### ASTER SETUP ###
|
||||
await get_aster_collateral()
|
||||
await get_aster_notional_position()
|
||||
await get_aster_exch_info()
|
||||
await get_aster_open_orders()
|
||||
### EXTEND SETUP ###
|
||||
await get_extend_collateral()
|
||||
await get_extend_notional()
|
||||
await get_extend_exch_info()
|
||||
await get_extend_open_orders()
|
||||
|
||||
await run_algo()
|
||||
|
||||
if __name__ == '__main__':
|
||||
START_TIME = round(datetime.now().timestamp()*1000)
|
||||
|
||||
logging.info(f'Log FilePath: {LOG_FILEPATH}')
|
||||
|
||||
logging.basicConfig(
|
||||
force=True,
|
||||
filename=LOG_FILEPATH,
|
||||
level=logging.INFO,
|
||||
format='%(asctime)s - %(levelname)s - %(message)s',
|
||||
filemode='w'
|
||||
)
|
||||
logging.info(f"STARTED: {START_TIME}")
|
||||
|
||||
asyncio.run(main())
|
||||
|
||||
BIN
modules/__pycache__/aster_auth.cpython-313.pyc
Normal file
BIN
modules/__pycache__/aster_auth.cpython-313.pyc
Normal file
Binary file not shown.
BIN
modules/__pycache__/aster_auth_api.cpython-313.pyc
Normal file
BIN
modules/__pycache__/aster_auth_api.cpython-313.pyc
Normal file
Binary file not shown.
BIN
modules/__pycache__/aster_db.cpython-313.pyc
Normal file
BIN
modules/__pycache__/aster_db.cpython-313.pyc
Normal file
Binary file not shown.
BIN
modules/__pycache__/db.cpython-313.pyc
Normal file
BIN
modules/__pycache__/db.cpython-313.pyc
Normal file
Binary file not shown.
BIN
modules/__pycache__/extended_auth.cpython-313.pyc
Normal file
BIN
modules/__pycache__/extended_auth.cpython-313.pyc
Normal file
Binary file not shown.
BIN
modules/__pycache__/extended_db.cpython-313.pyc
Normal file
BIN
modules/__pycache__/extended_db.cpython-313.pyc
Normal file
Binary file not shown.
BIN
modules/__pycache__/structs.cpython-313.pyc
Normal file
BIN
modules/__pycache__/structs.cpython-313.pyc
Normal file
Binary file not shown.
BIN
modules/__pycache__/utils.cpython-313.pyc
Normal file
BIN
modules/__pycache__/utils.cpython-313.pyc
Normal file
Binary file not shown.
108
modules/aster_auth.py
Normal file
108
modules/aster_auth.py
Normal file
@@ -0,0 +1,108 @@
|
||||
import requests
|
||||
from dotenv import load_dotenv
|
||||
import os
|
||||
import time
|
||||
import threading
|
||||
import urllib
|
||||
|
||||
from eth_account.messages import encode_typed_data
|
||||
from eth_account import Account
|
||||
|
||||
load_dotenv()
|
||||
|
||||
user = os.getenv("RABBY_WALLET")
|
||||
signer = os.getenv("ASTER_API_WALLET_ADDRESS")
|
||||
private_key = os.getenv("ASTER_API_PRIVATE_KEY")
|
||||
|
||||
_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"
|
||||
}
|
||||
}
|
||||
headers = {
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
'User-Agent': 'PythonApp/1.0'
|
||||
}
|
||||
host = 'https://fapi.asterdex.com'
|
||||
|
||||
|
||||
def get_nonce():
|
||||
_nonce_lock = threading.Lock()
|
||||
global _last_ms, _i
|
||||
with _nonce_lock:
|
||||
now_ms = int(time.time())
|
||||
|
||||
if now_ms == _last_ms:
|
||||
_i += 1
|
||||
else:
|
||||
_last_ms = now_ms
|
||||
_i = 0
|
||||
|
||||
return now_ms * 1_000_000 + _i
|
||||
|
||||
def sign_typed_data(data: dict, private_key: str):
|
||||
"""Sign EIP-712 typed data using encode_typed_data."""
|
||||
message = encode_typed_data(
|
||||
domain_data=data["domain"],
|
||||
message_types={"Message": data["types"]["Message"]},
|
||||
message_data=data["message"],
|
||||
)
|
||||
return Account.sign_message(message, private_key=private_key)
|
||||
|
||||
async def send_by_url(req):
|
||||
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
|
||||
|
||||
param = urllib.parse.urlencode(my_dict)
|
||||
|
||||
typed_data['message']['msg'] = param
|
||||
signed = sign_typed_data(typed_data, private_key)
|
||||
|
||||
full_url = url + '?' + param + '&signature=' + signed.signature.hex()
|
||||
# print(full_url)
|
||||
|
||||
if method == 'GET':
|
||||
res = requests.get(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)
|
||||
return res.json()
|
||||
elif method == 'PUT':
|
||||
res = requests.put(full_url, headers=headers)
|
||||
# print(res.status_code, res.text)
|
||||
return res.json()
|
||||
elif method == 'DELETE':
|
||||
res = requests.delete(full_url, headers=headers)
|
||||
# print(res.status_code, res.text)
|
||||
return res.json()
|
||||
|
||||
return await send_by_url(req=req)
|
||||
147
modules/aster_db.py
Normal file
147
modules/aster_db.py
Normal file
@@ -0,0 +1,147 @@
|
||||
import logging
|
||||
from typing import AsyncContextManager
|
||||
from sqlalchemy import text
|
||||
|
||||
|
||||
### Orders and Trades Tables ####
|
||||
async def create_fr_aster_user_order_trade_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: fr_aster_user_order_trade')
|
||||
await CON.execute(text("""
|
||||
CREATE TABLE IF NOT EXISTS fr_aster_user_order_trade (
|
||||
timestamp_arrival BIGINT,
|
||||
timestamp_msg BIGINT,
|
||||
timestamp_transaction BIGINT,
|
||||
symbol VARCHAR(20),
|
||||
client_order_id VARCHAR(100),
|
||||
side VARCHAR(20),
|
||||
order_type VARCHAR(100),
|
||||
time_in_force VARCHAR(20),
|
||||
original_qty DOUBLE,
|
||||
original_price DOUBLE,
|
||||
avg_price DOUBLE,
|
||||
stop_price DOUBLE,
|
||||
execution_type VARCHAR(100),
|
||||
order_status VARCHAR(100),
|
||||
order_id BIGINT,
|
||||
last_filled_qty DOUBLE,
|
||||
filled_accumulated_qty DOUBLE,
|
||||
last_filled_price DOUBLE,
|
||||
commission_asset VARCHAR(20),
|
||||
commission DOUBLE,
|
||||
order_trade_time_ts BIGINT,
|
||||
trade_id VARCHAR(100),
|
||||
bid_notional DOUBLE,
|
||||
ask_notional DOUBLE,
|
||||
trade_is_maker BOOL,
|
||||
trade_is_reduce_only BOOL,
|
||||
stop_px_working_type VARCHAR(100),
|
||||
original_order_type VARCHAR(100),
|
||||
position_side VARCHAR(100),
|
||||
pushed_w_conditional_order BOOL,
|
||||
activation_price DOUBLE,
|
||||
callback_rate DOUBLE,
|
||||
realized_profit DOUBLE
|
||||
);
|
||||
"""))
|
||||
await CON.commit()
|
||||
else:
|
||||
raise ValueError('Only MySQL engine is implemented')
|
||||
|
||||
### Margin Calls Table ####
|
||||
async def create_fr_aster_user_margin_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: fr_aster_user_margin')
|
||||
await CON.execute(text("""
|
||||
CREATE TABLE IF NOT EXISTS fr_aster_user_margin (
|
||||
timestamp_arrival BIGINT,
|
||||
timestamp_msg BIGINT,
|
||||
cross_wallet_balance DOUBLE,
|
||||
symbol VARCHAR(20),
|
||||
position_side VARCHAR(20),
|
||||
position_amount DOUBLE,
|
||||
margin_type VARCHAR(20),
|
||||
isolated_wallet DOUBLE,
|
||||
mark_price DOUBLE,
|
||||
unrealized_pnl DOUBLE,
|
||||
maint_margin_required DOUBLE
|
||||
);
|
||||
"""))
|
||||
await CON.commit()
|
||||
else:
|
||||
raise ValueError('Only MySQL engine is implemented')
|
||||
|
||||
### Account Balance Table ####
|
||||
async def create_fr_aster_user_account_bal(
|
||||
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: fr_aster_user_account_bal')
|
||||
await CON.execute(text("""
|
||||
CREATE TABLE IF NOT EXISTS fr_aster_user_account_bal (
|
||||
timestamp_arrival BIGINT,
|
||||
timestamp_msg BIGINT,
|
||||
timestamp_transaction BIGINT,
|
||||
event_reason_type VARCHAR(20),
|
||||
asset VARCHAR(20),
|
||||
wallet_balance DOUBLE,
|
||||
cross_wallet_balance DOUBLE,
|
||||
balance_change_excl_pnl_comms DOUBLE
|
||||
);
|
||||
"""))
|
||||
await CON.commit()
|
||||
else:
|
||||
raise ValueError('Only MySQL engine is implemented')
|
||||
|
||||
### Account Positions Table ####
|
||||
async def create_fr_aster_user_account_pos(
|
||||
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: fr_aster_user_account_pos')
|
||||
await CON.execute(text("""
|
||||
CREATE TABLE IF NOT EXISTS fr_aster_user_account_pos (
|
||||
timestamp_arrival BIGINT,
|
||||
timestamp_msg BIGINT,
|
||||
timestamp_transaction BIGINT,
|
||||
event_reason_type VARCHAR(20),
|
||||
symbol VARCHAR(20),
|
||||
position_amount DOUBLE,
|
||||
entry_price DOUBLE,
|
||||
accumulated_realized_pre_fees DOUBLE,
|
||||
unrealized_pnl DOUBLE,
|
||||
margin_type VARCHAR(20),
|
||||
isolated_wallet DOUBLE,
|
||||
position_side VARCHAR(20)
|
||||
);
|
||||
"""))
|
||||
await CON.commit()
|
||||
else:
|
||||
raise ValueError('Only MySQL engine is implemented')
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
29
modules/db.py
Normal file
29
modules/db.py
Normal file
@@ -0,0 +1,29 @@
|
||||
### Database Funcs ###
|
||||
import logging
|
||||
from typing import AsyncContextManager
|
||||
import pandas as pd
|
||||
|
||||
|
||||
async def insert_df_to_mysql(
|
||||
table_name: str,
|
||||
params: dict | list | pd.DataFrame,
|
||||
CON: AsyncContextManager,
|
||||
engine: str = 'mysql', # mysql | duckdb
|
||||
) -> None:
|
||||
if CON is None:
|
||||
logging.info("NO DB CONNECTION, SKIPPING Insert Statements")
|
||||
else:
|
||||
if engine == 'mysql':
|
||||
if not isinstance(params, pd.DataFrame):
|
||||
if isinstance(params, dict):
|
||||
params = [params]
|
||||
df = pd.DataFrame(params)
|
||||
else:
|
||||
df = params
|
||||
print(f'DB INSERT: table: {table_name}; CON: {CON}; params: {params}')
|
||||
await CON.run_sync(
|
||||
lambda sync_conn: df.to_sql(name=table_name, con=sync_conn, if_exists='append', index=False)
|
||||
)
|
||||
await CON.commit()
|
||||
else:
|
||||
raise ValueError('Only MySQL engine is implemented')
|
||||
30
modules/extended_auth.py
Normal file
30
modules/extended_auth.py
Normal file
@@ -0,0 +1,30 @@
|
||||
import os
|
||||
|
||||
from dotenv import load_dotenv
|
||||
from x10.config import MAINNET_CONFIG
|
||||
from x10.core.stark_account import StarkPerpetualAccount
|
||||
from x10.perpetual.trading_client import PerpetualTradingClient
|
||||
import logging
|
||||
|
||||
async def create_auth_account_and_trading_client() -> tuple[StarkPerpetualAccount, PerpetualTradingClient]:
|
||||
load_dotenv()
|
||||
API_KEY = os.getenv('EXTENDED_API_KEY')
|
||||
PUBLIC_KEY = os.getenv('EXTENDED_STARK_KEY_PUBLIC')
|
||||
PRIVATE_KEY = os.getenv('EXTENDED_STARK_KEY_PRIVATE')
|
||||
VAULT = int(os.getenv('EXTENDED_VAULT_NUMBER'))
|
||||
|
||||
stark_account = StarkPerpetualAccount(
|
||||
vault=VAULT,
|
||||
private_key=PRIVATE_KEY,
|
||||
public_key=PUBLIC_KEY,
|
||||
api_key=API_KEY,
|
||||
)
|
||||
|
||||
trading_client = PerpetualTradingClient(MAINNET_CONFIG, stark_account)
|
||||
|
||||
try:
|
||||
await trading_client.account.get_balance()
|
||||
except ValueError as e:
|
||||
logging.critical(f'Failed to get balance after creation of trading account: {e}')
|
||||
|
||||
return stark_account, trading_client
|
||||
152
modules/extended_db.py
Normal file
152
modules/extended_db.py
Normal file
@@ -0,0 +1,152 @@
|
||||
import logging
|
||||
from typing import AsyncContextManager
|
||||
from sqlalchemy import text
|
||||
|
||||
|
||||
### Orders Table ####
|
||||
async def create_fr_extended_user_order(
|
||||
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: fr_extended_user_order')
|
||||
await CON.execute(text("""
|
||||
CREATE TABLE IF NOT EXISTS fr_extended_user_order (
|
||||
sequence_id INT,
|
||||
timestamp_arrival BIGINT,
|
||||
timestamp_msg BIGINT,
|
||||
order_id VARCHAR(100),
|
||||
account_id VARCHAR(100),
|
||||
external_id VARCHAR(100),
|
||||
market VARCHAR(20),
|
||||
type VARCHAR(20),
|
||||
side VARCHAR(20),
|
||||
status VARCHAR(20),
|
||||
status_reason VARCHAR(100),
|
||||
price DOUBLE,
|
||||
averagePrice DOUBLE,
|
||||
qty DOUBLE,
|
||||
filled_qty DOUBLE,
|
||||
payed_fee DOUBLE,
|
||||
tp_sl_type VARCHAR(20),
|
||||
reduce_only BOOL,
|
||||
post_only BOOL,
|
||||
created_time_ts BIGINT,
|
||||
updated_time_ts BIGINT,
|
||||
expire_time_ts BIGINT
|
||||
);
|
||||
"""))
|
||||
await CON.commit()
|
||||
else:
|
||||
raise ValueError('Only MySQL engine is implemented')
|
||||
|
||||
### Orders Table ####
|
||||
async def create_fr_extended_user_trade(
|
||||
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: fr_extended_user_trade')
|
||||
await CON.execute(text("""
|
||||
CREATE TABLE IF NOT EXISTS fr_extended_user_trade (
|
||||
sequence_id INT,
|
||||
timestamp_arrival BIGINT,
|
||||
timestamp_msg BIGINT,
|
||||
trade_id VARCHAR(100),
|
||||
account_id VARCHAR(100),
|
||||
market VARCHAR(20),
|
||||
order_id VARCHAR(100),
|
||||
external_order_id VARCHAR(100),
|
||||
side VARCHAR(20),
|
||||
price DOUBLE,
|
||||
qty DOUBLE,
|
||||
value DOUBLE,
|
||||
fee DOUBLE,
|
||||
trade_type VARCHAR(20),
|
||||
created_time_ts BIGINT,
|
||||
is_taker BOOL
|
||||
);
|
||||
"""))
|
||||
await CON.commit()
|
||||
else:
|
||||
raise ValueError('Only MySQL engine is implemented')
|
||||
|
||||
### Balance Table ####
|
||||
async def create_fr_extended_user_balance(
|
||||
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: fr_extended_user_balance')
|
||||
await CON.execute(text("""
|
||||
CREATE TABLE IF NOT EXISTS fr_extended_user_balance (
|
||||
sequence_id INT,
|
||||
timestamp_arrival BIGINT,
|
||||
timestamp_msg BIGINT,
|
||||
collateral_name VARCHAR(20),
|
||||
balance DOUBLE,
|
||||
equity DOUBLE,
|
||||
available_for_trade DOUBLE,
|
||||
available_for_withdrawal DOUBLE,
|
||||
unrealised_pnl DOUBLE,
|
||||
initial_margin DOUBLE,
|
||||
margin_ratio DOUBLE,
|
||||
updated_time_ts BIGINT,
|
||||
exposure DOUBLE,
|
||||
leverage DOUBLE
|
||||
);
|
||||
"""))
|
||||
await CON.commit()
|
||||
else:
|
||||
raise ValueError('Only MySQL engine is implemented')
|
||||
|
||||
### Balance Table ####
|
||||
async def create_fr_extended_user_position(
|
||||
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: fr_extended_user_position')
|
||||
await CON.execute(text("""
|
||||
CREATE TABLE IF NOT EXISTS fr_extended_user_position (
|
||||
sequence_id INT,
|
||||
timestamp_arrival BIGINT,
|
||||
timestamp_msg BIGINT,
|
||||
position_id VARCHAR(100),
|
||||
account_id VARCHAR(100),
|
||||
market VARCHAR(20),
|
||||
side VARCHAR(20),
|
||||
leverage DOUBLE,
|
||||
size DOUBLE,
|
||||
value DOUBLE,
|
||||
open_price DOUBLE,
|
||||
mark_price DOUBLE,
|
||||
liquidation_price DOUBLE,
|
||||
margin DOUBLE,
|
||||
unrealised_pnl DOUBLE,
|
||||
realised_pnl DOUBLE,
|
||||
tp_trigger_price DOUBLE,
|
||||
tp_limit_price DOUBLE,
|
||||
sl_trigger_price DOUBLE,
|
||||
sl_limit_price DOUBLE,
|
||||
adl_percentile INT,
|
||||
created_at_ts BIGINT,
|
||||
updated_at_ts BIGINT
|
||||
);
|
||||
"""))
|
||||
await CON.commit()
|
||||
else:
|
||||
raise ValueError('Only MySQL engine is implemented')
|
||||
|
||||
183
modules/structs.py
Normal file
183
modules/structs.py
Normal file
@@ -0,0 +1,183 @@
|
||||
import json
|
||||
from dataclasses import dataclass, field
|
||||
from typing import Any
|
||||
|
||||
import valkey
|
||||
|
||||
|
||||
@dataclass(kw_only=True)
|
||||
class Algo_Config:
|
||||
Config_Updated_Timestamp: int
|
||||
Allow_Ordering_Aster: bool
|
||||
Allow_Ordering_Extend: bool
|
||||
Loop_Sleep_Sec: int
|
||||
Max_Target_Notional: float
|
||||
Min_Time_To_Funding_Minutes: int
|
||||
Price_Worsener_Aster: float
|
||||
Price_Worsener_Extend: float
|
||||
Target_Open_Cash_Position: int
|
||||
|
||||
Print_Summary_Each_Loop: bool = False
|
||||
|
||||
@dataclass(kw_only=True)
|
||||
class Flags:
|
||||
LIQUIDATE_POS_AND_KILL_ALGO_FLAG: bool = False
|
||||
NET_FUNDING_IS_ZERO: bool = False
|
||||
|
||||
|
||||
@dataclass(kw_only=True)
|
||||
class Valkey_Stream:
|
||||
client: valkey.Valkey
|
||||
channel: str
|
||||
data: Any = None
|
||||
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
|
||||
|
||||
|
||||
@dataclass(kw_only=True)
|
||||
class Position:
|
||||
market: str
|
||||
notional: float
|
||||
qty: float
|
||||
|
||||
|
||||
@dataclass(kw_only=True)
|
||||
class Open_Positions:
|
||||
Valkey: Valkey_Stream
|
||||
Positions: list[Position] = field(default_factory = list)
|
||||
|
||||
async def update(self) -> None:
|
||||
self.Valkey = await self.Valkey.update()
|
||||
|
||||
|
||||
### Collateral ###
|
||||
@dataclass(kw_only=True)
|
||||
class Asset:
|
||||
symbol: str
|
||||
balance: float
|
||||
# min_order_qty: float
|
||||
|
||||
|
||||
@dataclass(kw_only=True)
|
||||
class Collateral:
|
||||
Valkey: Valkey_Stream
|
||||
# Last_Updated_Ts_Ms: int
|
||||
# Last_Pulled_Ts_Ms: int
|
||||
Assets: list[Asset] = field(default_factory = list)
|
||||
|
||||
async def update(self) -> None:
|
||||
self.Valkey = await self.Valkey.update()
|
||||
|
||||
|
||||
### Orders ###
|
||||
@dataclass(kw_only=True)
|
||||
class Order:
|
||||
symbol: str
|
||||
order_id: str
|
||||
client_order_id: str
|
||||
side: str
|
||||
order_type: str
|
||||
original_qty: float
|
||||
original_price: float
|
||||
order_status: str
|
||||
last_filled_qty: float
|
||||
last_filled_price: float
|
||||
commission: float
|
||||
trade_is_maker: bool
|
||||
|
||||
|
||||
@dataclass(kw_only=True)
|
||||
class Order_Updates:
|
||||
# Last_Updated_Ts_Ms: int
|
||||
# Last_Pulled_Ts_Ms: int
|
||||
Valkey: Valkey_Stream
|
||||
Orders: list[Order] = field(default_factory = list)
|
||||
|
||||
async def update(self) -> None:
|
||||
self.Valkey = await self.Valkey.update()
|
||||
|
||||
|
||||
### Funding Rate ###
|
||||
@dataclass(kw_only=True)
|
||||
class Funding_Rate:
|
||||
# Last_Updated_Ts_Ms: int
|
||||
# Last_Pulled_Ts_Ms: int
|
||||
Valkey: Valkey_Stream
|
||||
timestamp_arrival: int
|
||||
timestamp_msg: int
|
||||
symbol: str
|
||||
funding_rate: float
|
||||
next_funding_time_ts_ms: int
|
||||
mark_price: float
|
||||
index_price: float
|
||||
estimated_settle_price: float
|
||||
|
||||
async def update(self) -> None:
|
||||
self.Valkey = await self.Valkey.update()
|
||||
|
||||
|
||||
### Markets Info ###
|
||||
@dataclass(kw_only=True)
|
||||
class Market:
|
||||
symbol: str
|
||||
min_order_qty: float
|
||||
|
||||
|
||||
@dataclass(kw_only=True)
|
||||
class Markets_Details:
|
||||
Markets: list[Market] = field(default_factory=list)
|
||||
|
||||
|
||||
### Exchanges ###
|
||||
@dataclass(kw_only=True)
|
||||
class Perpetual_Exchange:
|
||||
# Order_Updates: Order_Updates
|
||||
# Position_Updates: Open_Positions
|
||||
# Collateral_Updates: Collateral
|
||||
# Funding_Rate: Funding_Rate
|
||||
# Markets: Markets_Details
|
||||
mult: int
|
||||
lh_asset: str
|
||||
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()
|
||||
|
||||
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'
|
||||
|
||||
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 = '-'
|
||||
|
||||
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))
|
||||
28
modules/utils.py
Normal file
28
modules/utils.py
Normal file
@@ -0,0 +1,28 @@
|
||||
import logging
|
||||
from dotenv import load_dotenv
|
||||
import requests
|
||||
import os
|
||||
|
||||
load_dotenv()
|
||||
|
||||
def upsert_list_of_dicts_by_id(list_of_dicts, new_dict, id='id', seq_check_field: str | None = None):
|
||||
for index, item in enumerate(list_of_dicts):
|
||||
if item.get(id) == new_dict.get(id):
|
||||
if seq_check_field is not None:
|
||||
if item.get(seq_check_field) > new_dict.get(seq_check_field):
|
||||
logging.info('Skipping out of sequence msg')
|
||||
return list_of_dicts
|
||||
list_of_dicts[index] = new_dict
|
||||
return list_of_dicts
|
||||
|
||||
list_of_dicts.append(new_dict)
|
||||
return list_of_dicts
|
||||
|
||||
def send_tg_alert(msg: str):
|
||||
token = os.getenv("TG_TOKEN")
|
||||
chat_id = os.getenv("TG_ALERTS_CHAT_ID")
|
||||
|
||||
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()
|
||||
36
ng.py
Normal file
36
ng.py
Normal file
@@ -0,0 +1,36 @@
|
||||
import os
|
||||
from nicegui import ui, app
|
||||
from sqlalchemy import create_engine
|
||||
import json
|
||||
import valkey
|
||||
|
||||
|
||||
VALKEY_R = valkey.Valkey(host='localhost', port=6379, db=0, decode_responses=True)
|
||||
|
||||
|
||||
|
||||
def root():
|
||||
app.add_static_files(max_cache_age=0, url_path='/static', local_directory=os.path.join(os.path.dirname(__file__), 'nicegui_modules/static'))
|
||||
ui.add_head_html('''
|
||||
<meta name="darkreader-lock">
|
||||
<link rel="stylesheet" type="text/css" href="/static/styles.css">
|
||||
<script type="text/javascript" src="https://unpkg.com/lightweight-charts/dist/lightweight-charts.standalone.production.js"></script>
|
||||
<script src="/static/script.js"></script>
|
||||
'''
|
||||
)
|
||||
# ui.add_head_html('<meta name="darkreader-lock">')
|
||||
# update_body_scroll(bool_override=ALLOW_BODY_SCROLL)
|
||||
|
||||
ui.sub_pages({
|
||||
'/': controls_grid,
|
||||
}).classes('w-full')
|
||||
|
||||
|
||||
async def controls_grid():
|
||||
with ui.grid(columns=16).classes('w-full gap-0 auto-fit'):
|
||||
with ui.card().tight().classes('w-full col-span-full no-shadow border border-black-200').style('overflow: auto;'):
|
||||
ui.html('<div id="tv" style="width:100%; height:800px;"></div>', sanitize=False).classes('w-full')
|
||||
ui.run_javascript('await create_tv();')
|
||||
|
||||
|
||||
ui.run(root, storage_secret="123ABC", reload=True, dark=True, title='Atwater_Trading')
|
||||
25
requirements.txt
Normal file
25
requirements.txt
Normal file
@@ -0,0 +1,25 @@
|
||||
pandas
|
||||
rel
|
||||
websockets
|
||||
pyarrow
|
||||
# plotly
|
||||
mysql-connector-python
|
||||
sqlalchemy
|
||||
requests
|
||||
pymysql
|
||||
scipy
|
||||
asyncmy
|
||||
cryptography
|
||||
# TA-Lib
|
||||
valkey
|
||||
nicegui
|
||||
# py_clob_client
|
||||
# google
|
||||
# google-api-core==2.30.0
|
||||
# google-api-python-client==2.190.0
|
||||
# googleapis-common-protos==1.72.0
|
||||
# grpcio==1.76.0
|
||||
# grpcio-tools==1.76.0
|
||||
x10-python-trading-starknet
|
||||
eth-keys
|
||||
eth-account
|
||||
@@ -128,7 +128,7 @@ async def ws_stream():
|
||||
if channel is not None:
|
||||
match channel:
|
||||
case c if c == STREAM_MARKPRICE:
|
||||
print(f'MP: {data}')
|
||||
# print(f'MP: {data}')
|
||||
VAL_KEY_OBJ = json.dumps({
|
||||
'timestamp_arrival': ts_arrival,
|
||||
'timestamp_msg': data['data']['E'],
|
||||
@@ -186,7 +186,7 @@ async def main():
|
||||
logging.warning("VALKEY NOT BEING USED, NO DATA WILL BE PUBLISHED")
|
||||
|
||||
if USE_DB:
|
||||
engine = create_async_engine('mysql+asyncmy://root:pwd@localhost/polymarket')
|
||||
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()
|
||||
|
||||
19
ws_aster/Dockerfile
Normal file
19
ws_aster/Dockerfile
Normal file
@@ -0,0 +1,19 @@
|
||||
FROM python:3.13-slim
|
||||
|
||||
RUN apt-get update && \
|
||||
apt-get install -y build-essential
|
||||
|
||||
RUN gcc --version
|
||||
RUN rm -rf /var/lib/apt/lists/*
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
COPY requirements.txt .
|
||||
|
||||
RUN pip install --no-cache-dir -r requirements.txt
|
||||
|
||||
COPY . .
|
||||
|
||||
# Finally, run gunicorn.
|
||||
CMD [ "python", "ws_aster.py"]
|
||||
# CMD [ "gunicorn", "--workers=5", "--threads=1", "-b 0.0.0.0:8000", "app:server"]
|
||||
292
ws_aster_user.py
Normal file
292
ws_aster_user.py
Normal file
@@ -0,0 +1,292 @@
|
||||
import asyncio
|
||||
import json
|
||||
import logging
|
||||
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
|
||||
from dotenv import load_dotenv
|
||||
import modules.aster_auth as aster_auth
|
||||
import modules.aster_db as aster_db
|
||||
import modules.db as db
|
||||
import modules.utils as utils
|
||||
|
||||
### Allow only ipv4 ###
|
||||
def allowed_gai_family():
|
||||
return socket.AF_INET
|
||||
urllib3_cn.allowed_gai_family = allowed_gai_family
|
||||
|
||||
### Database ###
|
||||
USE_DB: bool = 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
|
||||
|
||||
### Logging ###
|
||||
load_dotenv()
|
||||
LOG_FILEPATH: str = os.getenv("LOGS_PATH") + '/Fund_Rate_Aster_User.log'
|
||||
|
||||
### CONSTANTS ###
|
||||
WSS_URL = "wss://fstream.asterdex.com/ws/"
|
||||
LOCAL_RECENT_UPDATES_LOOKBACK_SEC = 30
|
||||
|
||||
### Globals ###
|
||||
LISTEN_KEY: str | None = None
|
||||
LISTEN_KEY_LAST_UPDATE_TS_S: int = 0
|
||||
LISTEN_KEY_PUT_INTERVAL_SEC = 1800
|
||||
|
||||
LOCAL_RECENT_ORDERS: list = []
|
||||
LOCAL_RECENT_MARGIN_CALLS: list = []
|
||||
LOCAL_RECENT_BALANCES: list = []
|
||||
LOCAL_RECENT_POSITIONS: list = []
|
||||
|
||||
|
||||
async def get_new_listen_key() -> str:
|
||||
global LISTEN_KEY_LAST_UPDATE_TS_S
|
||||
|
||||
listen_key_request = {
|
||||
"url": "/fapi/v3/listenKey",
|
||||
"method": "POST",
|
||||
"params": {}
|
||||
}
|
||||
r = await aster_auth.post_authenticated_url(listen_key_request)
|
||||
listen_key = r.get('listenKey', None)
|
||||
print(f'LISTEN KEY: {listen_key}')
|
||||
if listen_key is not None:
|
||||
LISTEN_KEY_LAST_UPDATE_TS_S = round(datetime.now().timestamp())
|
||||
return listen_key
|
||||
else:
|
||||
raise ValueError(f'Listen Key is None; Failed to Update. response: {r}')
|
||||
|
||||
async def listen_key_interval():
|
||||
global LISTEN_KEY
|
||||
|
||||
while True:
|
||||
await asyncio.sleep(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
|
||||
|
||||
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())
|
||||
try:
|
||||
async for message in websocket:
|
||||
ts_arrival = round(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)
|
||||
|
||||
match channel:
|
||||
case 'ORDER_TRADE_UPDATE':
|
||||
# logging.info(f'ORDER_TRADE_UPDATE: {data}')
|
||||
new_order_update = {
|
||||
'timestamp_arrival': ts_arrival,
|
||||
'timestamp_msg': data['E'],
|
||||
'timestamp_transaction': data['T'],
|
||||
|
||||
'symbol': data['o']["s"], # "BTCUSDT", // Symbol
|
||||
'client_order_id': data['o']["c"], # "TEST", // Client Order Id
|
||||
'side': data['o']["S"], # "SELL", // Side
|
||||
'order_type': data['o']["o"], # "TRAILING_STOP_MARKET", // Order Type
|
||||
'time_in_force': data['o']["f"], # "GTC", // Time in Force
|
||||
'original_qty': float(data['o']["q"]), # "0.001", // Original Quantity
|
||||
'original_price': float(data['o']["p"]), # "0", // Original Price
|
||||
'avg_price': float(data['o']["ap"]), # :"0", // Average Price
|
||||
'stop_price': float(data['o'].get("sp", 0)), # :"7103.04", // Stop Price. Please ignore with TRAILING_STOP_MARKET order
|
||||
'execution_type': data['o']["x"], # "NEW", // Execution Type
|
||||
'order_status': data['o']["X"], # "NEW", // Order Status
|
||||
'order_id': data['o']["i"], # 8886774, // Order Id
|
||||
'last_filled_qty': float(data['o']["l"]), # "0", // Order Last Filled Quantity
|
||||
'filled_accumulated_qty': float(data['o']["z"]), # "0", // Order Filled Accumulated Quantity
|
||||
'last_filled_price': float(data['o']["L"]), # "0", // Last Filled Price
|
||||
'commission_asset': data['o'].get("N", None), # "USDT", // Commission Asset, will not push if no commission
|
||||
'commission': float(data['o'].get("n",0)), # "0", // Commission, will not push if no commission
|
||||
'order_trade_time_ts': data['o']["T"], # 1568879465651, // Order Trade Time
|
||||
'trade_id': data['o']["t"], # 0, // Trade Id
|
||||
'bid_notional': float(data['o']["b"]), # "0", // Bids Notional
|
||||
'ask_notional': float(data['o']["a"]), # "9.91", // Ask Notional
|
||||
'trade_is_maker': data['o']["m"], # false, // Is this trade the maker side?
|
||||
'trade_is_reduce_only': data['o']["R"], # false, // Is this reduce only
|
||||
'stop_px_working_type': data['o']["wt"], # :"CONTRACT_PRICE", // Stop Price Working Type
|
||||
'original_order_type': data['o']["ot"], # :"TRAILING_STOP_MARKET", // Original Order Type
|
||||
'position_side': data['o']["ps"], # :"LONG", // Position Side
|
||||
'pushed_w_conditional_order': bool(data['o'].get("cp", False)), # :false, // If Close-All, pushed with conditional order
|
||||
'activation_price': float(data['o'].get("AP", 0)), # :"7476.89", // Activation Price, only puhed with TRAILING_STOP_MARKET order
|
||||
'callback_rate': float(data['o'].get("cr", 0)), # :"5.0", // Callback Rate, only puhed with TRAILING_STOP_MARKET order
|
||||
'realized_profit': float(data['o']["rp"]), # :"0" // Realized Profit of the trade
|
||||
}
|
||||
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)
|
||||
|
||||
await db.insert_df_to_mysql(table_name='fr_aster_user_order_trade', params=new_order_update, CON=CON)
|
||||
continue
|
||||
case 'MARGIN_CALL':
|
||||
# logging.info(f'MARGIN_CALL: {data}')
|
||||
list_for_df = []
|
||||
for p in list(data['p']):
|
||||
margin_call_update = {
|
||||
'timestamp_arrival': ts_arrival,
|
||||
'timestamp_msg': data['E'],
|
||||
'cross_wallet_balance': float(data.get('cw', 0)),
|
||||
|
||||
'symbol': p["s"], # "ETHUSDT", // Symbol
|
||||
'position_side': p["ps"], # :"LONG", // Position Side
|
||||
'position_amount': float(p["pa"]), # :"1.327", // Position Amount
|
||||
'margin_type': p["mt"], # :"CROSSED", // Margin Type
|
||||
'isolated_wallet': float(p.get("iw", 0)), # :"0", // Isolated Wallet (if isolated position)
|
||||
'mark_price': float(p["mp"]), # :"187.17127", // Mark Price
|
||||
'unrealized_pnl': float(p["up"]), # :"-1.166074", // Unrealized PnL
|
||||
'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]
|
||||
|
||||
VAL_KEY_OBJ = json.dumps(LOCAL_RECENT_MARGIN_CALLS)
|
||||
VAL_KEY.set(VK_MARGIN_CALLS, VAL_KEY_OBJ)
|
||||
|
||||
await db.insert_df_to_mysql(table_name='fr_aster_user_margin', params=list_for_df, CON=CON)
|
||||
continue
|
||||
case 'ACCOUNT_UPDATE':
|
||||
# logging.info(f'ACCOUNT_UPDATE: {data}')
|
||||
list_for_df_bal = []
|
||||
list_for_df_pos = []
|
||||
### Balance Updates ###
|
||||
if len(list(data['a']['B'])) > 0:
|
||||
for b in list(data['a']['B']):
|
||||
balance_update = {
|
||||
'timestamp_arrival': ts_arrival,
|
||||
'timestamp_msg': data['E'],
|
||||
'timestamp_transaction': data['T'],
|
||||
|
||||
'event_reason_type': data['a']["m"],
|
||||
|
||||
'asset': b['a'],
|
||||
'wallet_balance': float(b['wb']),
|
||||
'cross_wallet_balance': float(b.get('cw', 0)),
|
||||
'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))
|
||||
### Position Updates ###
|
||||
if len(list(data['a']['P'])) > 0:
|
||||
for p in list(data['a']['P']):
|
||||
position_update = {
|
||||
'timestamp_arrival': ts_arrival,
|
||||
'timestamp_msg': data['E'],
|
||||
'timestamp_transaction': data['T'],
|
||||
|
||||
'event_reason_type': data['a']["m"],
|
||||
|
||||
'symbol': p['s'],
|
||||
'position_amount': float(p['pa']),
|
||||
'entry_price': float(p['ep']),
|
||||
'accumulated_realized_pre_fees': float(p['cr']),
|
||||
'unrealized_pnl': float(p['up']),
|
||||
'margin_type': p['mt'],
|
||||
'isolated_wallet': float(p.get('iw', 0)),
|
||||
'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))
|
||||
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.')
|
||||
case _:
|
||||
logging.warning(f'UNMATCHED OTHER MSG: {data}')
|
||||
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())
|
||||
|
||||
|
||||
async def main():
|
||||
global VAL_KEY
|
||||
global CON
|
||||
|
||||
if USE_VK:
|
||||
VAL_KEY = valkey.Valkey(host='localhost', port=6379, db=0)
|
||||
else:
|
||||
VAL_KEY = None
|
||||
logging.warning("VALKEY NOT BEING USED, NO DATA WILL BE PUBLISHED")
|
||||
|
||||
if USE_DB:
|
||||
engine = create_async_engine('mysql+asyncmy://root:pwd@localhost/fund_rate')
|
||||
async with engine.connect() as CON:
|
||||
await aster_db.create_fr_aster_user_order_trade_table(CON=CON)
|
||||
await aster_db.create_fr_aster_user_margin_table(CON=CON)
|
||||
await aster_db.create_fr_aster_user_account_bal(CON=CON)
|
||||
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()
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
START_TIME = round(datetime.now().timestamp()*1000)
|
||||
|
||||
logging.info(f'Log FilePath: {LOG_FILEPATH}')
|
||||
|
||||
logging.basicConfig(
|
||||
force=True,
|
||||
filename=LOG_FILEPATH,
|
||||
level=logging.INFO,
|
||||
format='%(asctime)s - %(levelname)s - %(message)s',
|
||||
filemode='w'
|
||||
)
|
||||
logging.info(f"STARTED: {START_TIME}")
|
||||
|
||||
try:
|
||||
asyncio.run(main())
|
||||
except KeyboardInterrupt:
|
||||
logging.info("Stream stopped")
|
||||
19
ws_aster_user/Dockerfile
Normal file
19
ws_aster_user/Dockerfile
Normal file
@@ -0,0 +1,19 @@
|
||||
FROM python:3.13-slim
|
||||
|
||||
RUN apt-get update && \
|
||||
apt-get install -y build-essential
|
||||
|
||||
RUN gcc --version
|
||||
RUN rm -rf /var/lib/apt/lists/*
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
COPY requirements.txt .
|
||||
|
||||
RUN pip install --no-cache-dir -r requirements.txt
|
||||
|
||||
COPY . .
|
||||
|
||||
# Finally, run gunicorn.
|
||||
CMD [ "python", "ws_aster_user.py"]
|
||||
# CMD [ "gunicorn", "--workers=5", "--threads=1", "-b 0.0.0.0:8000", "app:server"]
|
||||
203
ws_extended_fund_rate.py
Normal file
203
ws_extended_fund_rate.py
Normal file
@@ -0,0 +1,203 @@
|
||||
import asyncio
|
||||
import json
|
||||
import logging
|
||||
import socket
|
||||
import traceback
|
||||
from datetime import datetime, timezone
|
||||
from typing import AsyncContextManager
|
||||
import math
|
||||
|
||||
import numpy as np
|
||||
import pandas as pd
|
||||
import requests.packages.urllib3.util.connection as urllib3_cn # type: ignore
|
||||
from sqlalchemy import text
|
||||
import websockets
|
||||
from sqlalchemy.ext.asyncio import create_async_engine
|
||||
import valkey
|
||||
import os
|
||||
from dotenv import load_dotenv
|
||||
|
||||
|
||||
### Allow only ipv4 ###
|
||||
def allowed_gai_family():
|
||||
return socket.AF_INET
|
||||
urllib3_cn.allowed_gai_family = allowed_gai_family
|
||||
|
||||
### Database ###
|
||||
USE_DB: bool = False
|
||||
USE_VK: bool = True
|
||||
VK_FUND_RATE = 'fund_rate_extended'
|
||||
|
||||
CON: AsyncContextManager | None = None
|
||||
VAL_KEY = None
|
||||
|
||||
### Logging ###
|
||||
load_dotenv()
|
||||
LOG_FILEPATH: str = os.getenv("LOGS_PATH") + '/Fund_Rate_Extended_FR.log'
|
||||
|
||||
### CONSTANTS ###
|
||||
WS_SYMBOL: str = 'ETH-USD'
|
||||
FUNDING_RATE_INTERVAL_MIN = 60
|
||||
|
||||
### 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
|
||||
|
||||
def time_round_down(dt, interval_mins=5) -> int: # returns timestamp in seconds
|
||||
interval_secs = interval_mins * 60
|
||||
seconds = dt.timestamp()
|
||||
rounded_seconds = math.floor(seconds / interval_secs) * interval_secs
|
||||
|
||||
return rounded_seconds
|
||||
|
||||
|
||||
# ### 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)
|
||||
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())
|
||||
|
||||
|
||||
async def main():
|
||||
global VAL_KEY
|
||||
global CON
|
||||
|
||||
if USE_VK:
|
||||
VAL_KEY = valkey.Valkey(host='localhost', port=6379, db=0)
|
||||
else:
|
||||
VAL_KEY = None
|
||||
logging.warning("VALKEY NOT BEING USED, NO DATA WILL BE PUBLISHED")
|
||||
|
||||
if USE_DB:
|
||||
engine = create_async_engine('mysql+asyncmy://root:pwd@localhost/fund_rate')
|
||||
async with engine.connect() as CON:
|
||||
# await create_rtds_btcusd_table(CON=CON)
|
||||
await ws_stream()
|
||||
else:
|
||||
CON = None
|
||||
logging.warning("DATABASE NOT BEING USED, NO DATA WILL BE RECORDED")
|
||||
await ws_stream()
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
START_TIME = round(datetime.now().timestamp()*1000)
|
||||
|
||||
logging.info(f'Log FilePath: {LOG_FILEPATH}')
|
||||
|
||||
logging.basicConfig(
|
||||
force=True,
|
||||
filename=LOG_FILEPATH,
|
||||
level=logging.INFO,
|
||||
format='%(asctime)s - %(levelname)s - %(message)s',
|
||||
filemode='w'
|
||||
)
|
||||
logging.info(f"STARTED: {START_TIME}")
|
||||
|
||||
try:
|
||||
asyncio.run(main())
|
||||
except KeyboardInterrupt:
|
||||
logging.info("Stream stopped")
|
||||
19
ws_extended_fund_rate/Dockerfile
Normal file
19
ws_extended_fund_rate/Dockerfile
Normal file
@@ -0,0 +1,19 @@
|
||||
FROM python:3.13-slim
|
||||
|
||||
RUN apt-get update && \
|
||||
apt-get install -y build-essential
|
||||
|
||||
RUN gcc --version
|
||||
RUN rm -rf /var/lib/apt/lists/*
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
COPY requirements.txt .
|
||||
|
||||
RUN pip install --no-cache-dir -r requirements.txt
|
||||
|
||||
COPY . .
|
||||
|
||||
# Finally, run gunicorn.
|
||||
CMD [ "python", "ws_extended_fund_rate.py"]
|
||||
# CMD [ "gunicorn", "--workers=5", "--threads=1", "-b 0.0.0.0:8000", "app:server"]
|
||||
122
ws_extended_orderbook.py
Normal file
122
ws_extended_orderbook.py
Normal file
@@ -0,0 +1,122 @@
|
||||
import asyncio
|
||||
import json
|
||||
import logging
|
||||
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
|
||||
from dotenv import load_dotenv
|
||||
|
||||
|
||||
### Allow only ipv4 ###
|
||||
def allowed_gai_family():
|
||||
return socket.AF_INET
|
||||
urllib3_cn.allowed_gai_family = allowed_gai_family
|
||||
|
||||
### Database ###
|
||||
USE_DB: bool = False
|
||||
USE_VK: bool = True
|
||||
|
||||
VK_TICKER = 'fut_ticker_extended'
|
||||
CON: AsyncContextManager | None = None
|
||||
VAL_KEY = None
|
||||
|
||||
### Logging ###
|
||||
load_dotenv()
|
||||
LOG_FILEPATH: str = os.getenv("LOGS_PATH") + '/Fund_Rate_Extended_OB.log'
|
||||
|
||||
### CONSTANTS ###
|
||||
WS_SYMBOL: str = 'ETH-USD'
|
||||
|
||||
### Globals ###
|
||||
WSS_URL = f"wss://api.starknet.extended.exchange/stream.extended.exchange/v1/orderbooks/{WS_SYMBOL}?depth=1"
|
||||
|
||||
### 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)
|
||||
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())
|
||||
|
||||
|
||||
async def main():
|
||||
global VAL_KEY
|
||||
global CON
|
||||
|
||||
if USE_VK:
|
||||
VAL_KEY = valkey.Valkey(host='localhost', port=6379, db=0)
|
||||
else:
|
||||
VAL_KEY = None
|
||||
logging.warning("VALKEY NOT BEING USED, NO DATA WILL BE PUBLISHED")
|
||||
|
||||
if USE_DB:
|
||||
engine = create_async_engine('mysql+asyncmy://root:pwd@localhost/fund_rate')
|
||||
async with engine.connect() as CON:
|
||||
# await create_rtds_btcusd_table(CON=CON)
|
||||
await ws_stream()
|
||||
else:
|
||||
CON = None
|
||||
logging.warning("DATABASE NOT BEING USED, NO DATA WILL BE RECORDED")
|
||||
await ws_stream()
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
START_TIME = round(datetime.now().timestamp()*1000)
|
||||
|
||||
logging.info(f'Log FilePath: {LOG_FILEPATH}')
|
||||
|
||||
logging.basicConfig(
|
||||
force=True,
|
||||
filename=LOG_FILEPATH,
|
||||
level=logging.INFO,
|
||||
format='%(asctime)s - %(levelname)s - %(message)s',
|
||||
filemode='w'
|
||||
)
|
||||
logging.info(f"STARTED: {START_TIME}")
|
||||
|
||||
try:
|
||||
asyncio.run(main())
|
||||
except KeyboardInterrupt:
|
||||
logging.info("Stream stopped")
|
||||
19
ws_extended_orderbook/Dockerfile
Normal file
19
ws_extended_orderbook/Dockerfile
Normal file
@@ -0,0 +1,19 @@
|
||||
FROM python:3.13-slim
|
||||
|
||||
RUN apt-get update && \
|
||||
apt-get install -y build-essential
|
||||
|
||||
RUN gcc --version
|
||||
RUN rm -rf /var/lib/apt/lists/*
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
COPY requirements.txt .
|
||||
|
||||
RUN pip install --no-cache-dir -r requirements.txt
|
||||
|
||||
COPY . .
|
||||
|
||||
# Finally, run gunicorn.
|
||||
CMD [ "python", "ws_extended_orderbook.py"]
|
||||
# CMD [ "gunicorn", "--workers=5", "--threads=1", "-b 0.0.0.0:8000", "app:server"]
|
||||
264
ws_extended_user.py
Normal file
264
ws_extended_user.py
Normal file
@@ -0,0 +1,264 @@
|
||||
import asyncio
|
||||
import json
|
||||
import logging
|
||||
import socket
|
||||
import traceback
|
||||
from datetime import datetime, timezone
|
||||
from typing import AsyncContextManager
|
||||
import math
|
||||
|
||||
import numpy as np
|
||||
import pandas as pd
|
||||
import requests.packages.urllib3.util.connection as urllib3_cn # type: ignore
|
||||
from sqlalchemy import text
|
||||
import websockets
|
||||
from sqlalchemy.ext.asyncio import create_async_engine
|
||||
import valkey
|
||||
import os
|
||||
from dotenv import load_dotenv
|
||||
import modules.extended_db as extended_db
|
||||
import modules.db as db
|
||||
import modules.utils as utils
|
||||
|
||||
### Allow only ipv4 ###
|
||||
def allowed_gai_family():
|
||||
return socket.AF_INET
|
||||
urllib3_cn.allowed_gai_family = allowed_gai_family
|
||||
|
||||
### Database ###
|
||||
USE_DB: bool = True
|
||||
USE_VK: bool = True
|
||||
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
|
||||
|
||||
### Logging ###
|
||||
load_dotenv()
|
||||
LOG_FILEPATH: str = 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')
|
||||
LOCAL_RECENT_UPDATES_LOOKBACK_SEC = 30
|
||||
|
||||
### Globals ###
|
||||
LOCAL_RECENT_ORDERS: list = []
|
||||
LOCAL_RECENT_TRADES: list = []
|
||||
LOCAL_RECENT_BALANCES: list = []
|
||||
LOCAL_RECENT_POSITIONS: list = []
|
||||
|
||||
### Websocket ###
|
||||
async def ws_stream():
|
||||
global LOCAL_RECENT_ORDERS
|
||||
global LOCAL_RECENT_TRADES
|
||||
global LOCAL_RECENT_BALANCES
|
||||
global LOCAL_RECENT_POSITIONS
|
||||
|
||||
async for websocket in websockets.connect(WSS_URL, extra_headers={'X-Api-Key': API_KEY}):
|
||||
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)
|
||||
channel = data.get('type', None)
|
||||
if channel is not None:
|
||||
LOOKBACK_MIN_TS_MS = ts_arrival - (LOCAL_RECENT_UPDATES_LOOKBACK_SEC*1000)
|
||||
match channel:
|
||||
case 'ORDER':
|
||||
list_for_df = []
|
||||
for o in data['data']['orders']:
|
||||
order_update = {
|
||||
'sequence_id': data['seq'],
|
||||
'timestamp_arrival': ts_arrival,
|
||||
'timestamp_msg': data['ts'],
|
||||
|
||||
'order_id': o['id'],
|
||||
'account_id': o['accountId'],
|
||||
'external_id': o.get('externalId', None),
|
||||
'market': o['market'],
|
||||
'type': o['type'],
|
||||
'side': o['side'],
|
||||
'status': o['status'],
|
||||
'status_reason': o.get('statusReason', None),
|
||||
'price': float(o.get('price', 0)),
|
||||
'averagePrice': float(o.get('averagePrice', 0)),
|
||||
'qty': float(o['qty']),
|
||||
'filled_qty': float(o.get('filledQty', 0)),
|
||||
'payed_fee': float(o.get('payedFee', 0)),
|
||||
# 'trigger_dict': o.get('trigger', None),
|
||||
'tp_sl_type': o.get('tpSlType', None),
|
||||
# 'take_profit_dict': o.get('takeProfit', None),
|
||||
# 'stop_loss_dict': o.get('stopLoss', None),
|
||||
'reduce_only': o.get('reduceOnly', False),
|
||||
'post_only': o.get('postOnly', False),
|
||||
'created_time_ts': o['createdTime'],
|
||||
'updated_time_ts': o['updatedTime'],
|
||||
'expire_time_ts': o['expireTime'],
|
||||
}
|
||||
list_for_df.append(order_update)
|
||||
LOCAL_RECENT_ORDERS = utils.upsert_list_of_dicts_by_id(LOCAL_RECENT_ORDERS, order_update, id='order_id', seq_check_field='sequence_id')
|
||||
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, VAL_KEY_OBJ)
|
||||
await db.insert_df_to_mysql(table_name='fr_extended_user_order', params=list_for_df, CON=CON)
|
||||
continue
|
||||
case 'TRADE':
|
||||
list_for_df = []
|
||||
for t in data['data']['trades']:
|
||||
trade_update = {
|
||||
'sequence_id': data['seq'],
|
||||
'timestamp_arrival': ts_arrival,
|
||||
'timestamp_msg': data['ts'],
|
||||
|
||||
'trade_id': t['id'],
|
||||
'account_id': t['accountId'],
|
||||
'market': t['market'],
|
||||
'order_id': t['orderId'],
|
||||
'external_order_id': t.get('externalOrderId', None),
|
||||
'side': t['side'],
|
||||
'price': float(t['price']),
|
||||
'qty': float(t['qty']),
|
||||
'value': float(t['value']),
|
||||
'fee': float(t['fee']),
|
||||
'trade_type': t['tradeType'],
|
||||
'created_time_ts': t['createdTime'],
|
||||
'is_taker': t['isTaker'],
|
||||
}
|
||||
list_for_df.append(trade_update)
|
||||
LOCAL_RECENT_TRADES = utils.upsert_list_of_dicts_by_id(LOCAL_RECENT_TRADES, trade_update, id='trade_id', seq_check_field='sequence_id')
|
||||
LOCAL_RECENT_TRADES = [t for t in LOCAL_RECENT_TRADES if t.get('timestamp_arrival', 0) >= LOOKBACK_MIN_TS_MS]
|
||||
|
||||
VAL_KEY_OBJ = json.dumps(LOCAL_RECENT_TRADES)
|
||||
VAL_KEY.set(VK_TRADES, VAL_KEY_OBJ)
|
||||
await db.insert_df_to_mysql(table_name='fr_extended_user_trade', params=list_for_df, CON=CON)
|
||||
continue
|
||||
case 'BALANCE':
|
||||
balance_update = {
|
||||
'sequence_id': data['seq'],
|
||||
'timestamp_arrival': ts_arrival,
|
||||
'timestamp_msg': data['ts'],
|
||||
|
||||
'collateral_name': data['data']['balance']['collateralName'],
|
||||
'balance': float(data['data']['balance']['balance']),
|
||||
'equity': float(data['data']['balance']['equity']),
|
||||
'available_for_trade': float(data['data']['balance']['availableForTrade']),
|
||||
'available_for_withdrawal': float(data['data']['balance']['availableForWithdrawal']),
|
||||
'unrealised_pnl': float(data['data']['balance']['unrealisedPnl']),
|
||||
'initial_margin': float(data['data']['balance']['initialMargin']),
|
||||
'margin_ratio': float(data['data']['balance']['marginRatio']),
|
||||
'updated_time_ts': data['data']['balance']['updatedTime'],
|
||||
'exposure': float(data['data']['balance']['exposure']),
|
||||
'leverage': float(data['data']['balance']['leverage']),
|
||||
}
|
||||
LOCAL_RECENT_BALANCES = utils.upsert_list_of_dicts_by_id(LOCAL_RECENT_BALANCES, balance_update, id='collateral_name', seq_check_field='sequence_id')
|
||||
LOCAL_RECENT_BALANCES = [t for t in LOCAL_RECENT_BALANCES if t.get('timestamp_arrival', 0) >= LOOKBACK_MIN_TS_MS]
|
||||
|
||||
VAL_KEY_OBJ = json.dumps(LOCAL_RECENT_BALANCES)
|
||||
VAL_KEY.set(VK_BALANCES, VAL_KEY_OBJ)
|
||||
await db.insert_df_to_mysql(table_name='fr_extended_user_balance', params=balance_update, CON=CON)
|
||||
continue
|
||||
case 'POSITION':
|
||||
list_for_df = []
|
||||
for p in data['data']['positions']:
|
||||
position_update = {
|
||||
'sequence_id': data['seq'],
|
||||
'timestamp_arrival': ts_arrival,
|
||||
'timestamp_msg': data['ts'],
|
||||
|
||||
'position_id': p['id'],
|
||||
'account_id': p['accountId'],
|
||||
'market': p['market'],
|
||||
'side': p['side'],
|
||||
'leverage': float(p['leverage']),
|
||||
'size': float(p['size']),
|
||||
'value': float(p['value']),
|
||||
'open_price': float(p['openPrice']),
|
||||
'mark_price': float(p['markPrice']),
|
||||
'liquidation_price': float(p['liquidationPrice']),
|
||||
'margin': float(p['margin']),
|
||||
'unrealised_pnl': float(p['unrealisedPnl']),
|
||||
'realised_pnl': float(p['realisedPnl']),
|
||||
'tp_trigger_price': float(p.get('tpTriggerPrice', 0)),
|
||||
'tp_limit_price': float(p.get('tpLimitPrice', 0)),
|
||||
'sl_trigger_price': float(p.get('slTriggerPrice', 0)),
|
||||
'sl_limit_price': float(p.get('slLimitPrice', 0)),
|
||||
'adl_percentile': p.get('adl', 0), # closer to 100 means higher chance of auto-deleveraging
|
||||
'created_at_ts': p['createdAt'],
|
||||
'updated_at_ts': p['updatedAt'],
|
||||
}
|
||||
list_for_df.append(position_update)
|
||||
LOCAL_RECENT_POSITIONS = utils.upsert_list_of_dicts_by_id(LOCAL_RECENT_POSITIONS, position_update, id='position_id', seq_check_field='sequence_id')
|
||||
LOCAL_RECENT_POSITIONS = [t for t in LOCAL_RECENT_POSITIONS if t.get('timestamp_arrival', 0) >= LOOKBACK_MIN_TS_MS]
|
||||
|
||||
VAL_KEY_OBJ = json.dumps(LOCAL_RECENT_POSITIONS)
|
||||
VAL_KEY.set(VK_POSITIONS, VAL_KEY_OBJ)
|
||||
|
||||
await db.insert_df_to_mysql(table_name='fr_extended_user_position', params=list_for_df, CON=CON)
|
||||
continue
|
||||
case _:
|
||||
logging.warning(f'UNMATCHED OTHER MSG: {data}')
|
||||
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())
|
||||
|
||||
|
||||
async def main():
|
||||
global VAL_KEY
|
||||
global CON
|
||||
|
||||
if USE_VK:
|
||||
VAL_KEY = valkey.Valkey(host='localhost', port=6379, db=0)
|
||||
else:
|
||||
VAL_KEY = None
|
||||
logging.warning("VALKEY NOT BEING USED, NO DATA WILL BE PUBLISHED")
|
||||
|
||||
if USE_DB:
|
||||
engine = create_async_engine('mysql+asyncmy://root:pwd@localhost/fund_rate')
|
||||
async with engine.connect() as CON:
|
||||
await extended_db.create_fr_extended_user_balance(CON=CON)
|
||||
await extended_db.create_fr_extended_user_position(CON=CON)
|
||||
await extended_db.create_fr_extended_user_order(CON=CON)
|
||||
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()
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
START_TIME = round(datetime.now().timestamp()*1000)
|
||||
|
||||
logging.info(f'Log FilePath: {LOG_FILEPATH}')
|
||||
|
||||
logging.basicConfig(
|
||||
force=True,
|
||||
filename=LOG_FILEPATH,
|
||||
level=logging.INFO,
|
||||
format='%(asctime)s - %(levelname)s - %(message)s',
|
||||
filemode='w'
|
||||
)
|
||||
logging.info(f"STARTED: {START_TIME}")
|
||||
|
||||
try:
|
||||
asyncio.run(main())
|
||||
except KeyboardInterrupt:
|
||||
logging.info("Stream stopped")
|
||||
19
ws_extended_user/Dockerfile
Normal file
19
ws_extended_user/Dockerfile
Normal file
@@ -0,0 +1,19 @@
|
||||
FROM python:3.13-slim
|
||||
|
||||
RUN apt-get update && \
|
||||
apt-get install -y build-essential
|
||||
|
||||
RUN gcc --version
|
||||
RUN rm -rf /var/lib/apt/lists/*
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
COPY requirements.txt .
|
||||
|
||||
RUN pip install --no-cache-dir -r requirements.txt
|
||||
|
||||
COPY . .
|
||||
|
||||
# Finally, run gunicorn.
|
||||
CMD [ "python", "ws_extended_user.py"]
|
||||
# CMD [ "gunicorn", "--workers=5", "--threads=1", "-b 0.0.0.0:8000", "app:server"]
|
||||
Reference in New Issue
Block a user