Compare commits

...

10 Commits

Author SHA1 Message Date
73c4eb1bf8 saving for japan 2026-04-25 23:47:07 +00:00
dac490ca5b saving 2026-04-25 23:43:28 +00:00
b0d031d452 algo orchestrator 2026-04-25 03:40:47 +00:00
afa2d1fd79 start of refactor to objects 2026-04-24 07:29:26 +00:00
ea46b173fa algo logic 1 2026-04-23 17:33:01 +00:00
c12835a96d test 2026-04-23 16:37:10 +00:00
73c9ed67f5 get open orders 2026-04-23 16:34:47 +00:00
ff209603b6 initial algo logic 2026-04-23 06:39:51 +00:00
539e6004cf feedhandlers extended 2026-04-23 03:11:52 +00:00
408a63fe58 aster auth hell 2026-04-22 05:24:40 +00:00
43 changed files with 4759 additions and 221 deletions

View File

@@ -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
View 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
View 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
View 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
View 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())

View 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"]

View File

@@ -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
}

View File

@@ -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
View 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
View 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
}

578
main.py
View File

@@ -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
### 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
View 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())

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

108
modules/aster_auth.py Normal file
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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

View File

@@ -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
View 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
View 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
View 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"]

View File

203
ws_extended_fund_rate.py Normal file
View 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")

View 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
View 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")

View 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
View 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")

View 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"]