bfr w buy_ratios and std, algo with symbol switching and fr exponentiation for optimization
This commit is contained in:
1425
algo.ipynb
1425
algo.ipynb
File diff suppressed because it is too large
Load Diff
@@ -1,19 +1,19 @@
|
|||||||
{
|
{
|
||||||
"Updated_Timestamp": 1777942827905,
|
"Updated_Timestamp": 1778048640336,
|
||||||
"Config": {
|
"Config": {
|
||||||
"Loop_Sleep_Sec": 0.0,
|
"Loop_Sleep_Sec": 0.0,
|
||||||
"Max_Order_Over_Notional_Ratio": 1.05,
|
"Max_Order_Over_Notional_Ratio": 1.05,
|
||||||
"Max_Target_Notional": 0.0,
|
"Max_Target_Notional": 0.0,
|
||||||
"Min_Time_To_Funding_Minutes": 57,
|
"Min_Time_To_Funding_Minutes": 57,
|
||||||
"Min_Fund_Rate_Pct_To_Trade": 0.0005,
|
"Min_Fund_Rate_Pct_To_Trade": 0.0,
|
||||||
"Price_Worsener_Aster": 1,
|
"Price_Worsener_Aster": 0,
|
||||||
"Price_Worsener_Extend": -1,
|
"Price_Worsener_Extend": -1,
|
||||||
"Switch_To_Taker_Seconds": 3,
|
"Switch_To_Taker_Seconds": 3,
|
||||||
"Target_Open_Cash_Position": 10
|
"Target_Open_Cash_Position": 10
|
||||||
},
|
},
|
||||||
"Logging": {
|
"Logging": {
|
||||||
"Log_Summary_Each_Loop": false,
|
"Log_Summary_Each_Loop": false,
|
||||||
"Print_Summary_Each_Loop": true
|
"Print_Summary_Each_Loop": false
|
||||||
},
|
},
|
||||||
"Overrides": {
|
"Overrides": {
|
||||||
"Allow_Ordering_Aster": true,
|
"Allow_Ordering_Aster": true,
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
@@ -54,8 +54,6 @@ async def get_extended_markets_info() -> pd.DataFrame:
|
|||||||
df['min_notional'] = 0
|
df['min_notional'] = 0
|
||||||
df['min_lot_size'] = df['tradingConfig'].apply(lambda x: x.get('minOrderSizeChange',{}))
|
df['min_lot_size'] = df['tradingConfig'].apply(lambda x: x.get('minOrderSizeChange',{}))
|
||||||
df['max_leverage'] = df['tradingConfig'].apply(lambda x: x.get('maxLeverage',{}))
|
df['max_leverage'] = df['tradingConfig'].apply(lambda x: x.get('maxLeverage',{}))
|
||||||
|
|
||||||
#### TODO: ADD IN LOT SIZE FOR ROUND LOTS (SEE IPYNB)
|
|
||||||
|
|
||||||
print('Extend markets info refreshed successfully')
|
print('Extend markets info refreshed successfully')
|
||||||
|
|
||||||
@@ -184,7 +182,12 @@ async def loop() -> None:
|
|||||||
df_comb_fr['net_mult'] = df_comb_fr['net_mult'].round(2)
|
df_comb_fr['net_mult'] = df_comb_fr['net_mult'].round(2)
|
||||||
df_comb_fr['net_mult_x_net_fr_abs'] = df_comb_fr['net_funding_rate_abs'] * df_comb_fr['net_mult']
|
df_comb_fr['net_mult_x_net_fr_abs'] = df_comb_fr['net_funding_rate_abs'] * df_comb_fr['net_mult']
|
||||||
|
|
||||||
df_best_fr_rate = df_comb_fr[['symbol_ext','symbol_ast','daily_volume_ext','daily_volume_ast','min_price_ext','min_price_ast','min_order_size_ext','min_order_size_ast','min_lot_size_ext','min_lot_size_ast','min_notional_ext','min_notional_ast','funding_rate_ext','funding_rate_ast','max_leverage_ext','max_leverage_ast','lh_asset_ext','lh_asset_ast','rh_asset_ext','rh_asset_ast','net_mult_x_net_fr_abs','net_funding_rate_abs','net_funding_rate','next_funding_at_same_time','last_trade_ts_ast']].sort_values(by='net_mult_x_net_fr_abs', ascending=False).reset_index(drop=True)
|
df_best_fr_rate = df_comb_fr[
|
||||||
|
['symbol_ext','symbol_ast','daily_volume_ext','daily_volume_ast','min_price_ext','min_price_ast','min_order_size_ext',
|
||||||
|
'min_order_size_ast','min_lot_size_ext','min_lot_size_ast','min_notional_ext','min_notional_ast','funding_rate_ext',
|
||||||
|
'funding_rate_ast','max_leverage_ext','max_leverage_ast','lh_asset_ext','lh_asset_ast','rh_asset_ext','rh_asset_ast',
|
||||||
|
'net_mult_x_net_fr_abs','net_funding_rate_abs','net_funding_rate','next_funding_at_same_time','last_trade_ts_ast']
|
||||||
|
].sort_values(by='net_mult_x_net_fr_abs', ascending=False).reset_index(drop=True)
|
||||||
|
|
||||||
# min_daily_volume = 100_000
|
# min_daily_volume = 100_000
|
||||||
# df_best_fr_rate = df_best_fr_rate.loc[ (df_best_fr_rate['daily_volume_ast']>=min_daily_volume) & (df_best_fr_rate['daily_volume_ext']>min_daily_volume) ,:].reset_index(drop=True)
|
# df_best_fr_rate = df_best_fr_rate.loc[ (df_best_fr_rate['daily_volume_ast']>=min_daily_volume) & (df_best_fr_rate['daily_volume_ext']>min_daily_volume) ,:].reset_index(drop=True)
|
||||||
@@ -211,7 +214,8 @@ async def loop() -> None:
|
|||||||
for index, row in df_best_fr_rate.iterrows():
|
for index, row in df_best_fr_rate.iterrows():
|
||||||
df = await get_candles(symbol=row['symbol_ext'])
|
df = await get_candles(symbol=row['symbol_ext'])
|
||||||
buy_ratio_ext = float(df['med_ratio_aster_over_extend'].median())
|
buy_ratio_ext = float(df['med_ratio_aster_over_extend'].median())
|
||||||
candles_ratios.append({'symbol_ext':row['symbol_ext'], 'buy_ratio_ext':buy_ratio_ext,'buy_ratio_ast':buy_ratio_ext*-1})
|
buy_ratio_std = float(df['med_ratio_aster_over_extend'].std())
|
||||||
|
candles_ratios.append({'symbol_ext':row['symbol_ext'], 'buy_ratio_std': buy_ratio_std, 'buy_ratio_ext':buy_ratio_ext,'buy_ratio_ast':buy_ratio_ext*-1})
|
||||||
|
|
||||||
df_best_fr_rate = df_best_fr_rate.merge(pd.DataFrame(candles_ratios), on='symbol_ext', how='left')
|
df_best_fr_rate = df_best_fr_rate.merge(pd.DataFrame(candles_ratios), on='symbol_ext', how='left')
|
||||||
|
|
||||||
@@ -230,6 +234,7 @@ async def loop() -> None:
|
|||||||
min_lot_size=float(df_best_fr_rate['min_lot_size_ast'].iloc[0]),
|
min_lot_size=float(df_best_fr_rate['min_lot_size_ast'].iloc[0]),
|
||||||
min_notional=float(df_best_fr_rate['min_notional_ast'].iloc[0]),
|
min_notional=float(df_best_fr_rate['min_notional_ast'].iloc[0]),
|
||||||
buy_ratio=float(df_best_fr_rate['buy_ratio_ast'].iloc[0]),
|
buy_ratio=float(df_best_fr_rate['buy_ratio_ast'].iloc[0]),
|
||||||
|
buy_ratio_std=float(df_best_fr_rate['buy_ratio_std'].iloc[0]),
|
||||||
)
|
)
|
||||||
EXTEND = structs.Perpetual_Exchange(
|
EXTEND = structs.Perpetual_Exchange(
|
||||||
mult = int(df_best_fr_rate['max_leverage_ext'].iloc[0]),
|
mult = int(df_best_fr_rate['max_leverage_ext'].iloc[0]),
|
||||||
@@ -242,6 +247,7 @@ async def loop() -> None:
|
|||||||
min_lot_size=float(df_best_fr_rate['min_lot_size_ext'].iloc[0]),
|
min_lot_size=float(df_best_fr_rate['min_lot_size_ext'].iloc[0]),
|
||||||
min_notional=float(df_best_fr_rate['min_notional_ext'].iloc[0]),
|
min_notional=float(df_best_fr_rate['min_notional_ext'].iloc[0]),
|
||||||
buy_ratio=float(df_best_fr_rate['buy_ratio_ext'].iloc[0]),
|
buy_ratio=float(df_best_fr_rate['buy_ratio_ext'].iloc[0]),
|
||||||
|
buy_ratio_std=float(df_best_fr_rate['buy_ratio_std'].iloc[0]),
|
||||||
)
|
)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logging.critical(f'Failed to build ASTER/EXTEND objs err: {e}; df cols: {df_best_fr_rate.columns}')
|
logging.critical(f'Failed to build ASTER/EXTEND objs err: {e}; df cols: {df_best_fr_rate.columns}')
|
||||||
@@ -252,7 +258,7 @@ async def loop() -> None:
|
|||||||
|
|
||||||
master_data = df_best_fr_rate[
|
master_data = df_best_fr_rate[
|
||||||
['symbol_ast','max_leverage_ast','lh_asset_ast','rh_asset_ast','funding_rate_ast','min_price_ast','min_order_size_ast','min_lot_size_ast','min_notional_ast','buy_ratio_ast',
|
['symbol_ast','max_leverage_ast','lh_asset_ast','rh_asset_ast','funding_rate_ast','min_price_ast','min_order_size_ast','min_lot_size_ast','min_notional_ast','buy_ratio_ast',
|
||||||
'symbol_ext','max_leverage_ext','lh_asset_ext','rh_asset_ext','funding_rate_ext','min_price_ext','min_order_size_ext','min_lot_size_ext','min_notional_ext','buy_ratio_ext']
|
'symbol_ext','max_leverage_ext','lh_asset_ext','rh_asset_ext','funding_rate_ext','min_price_ext','min_order_size_ext','min_lot_size_ext','min_notional_ext','buy_ratio_ext', 'buy_ratio_std']
|
||||||
].to_json(orient='records')
|
].to_json(orient='records')
|
||||||
|
|
||||||
VAL_KEY.set(name='fr_engine_best_fund_rate_master', value=str(master_data))
|
VAL_KEY.set(name='fr_engine_best_fund_rate_master', value=str(master_data))
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
"cells": [
|
"cells": [
|
||||||
{
|
{
|
||||||
"cell_type": "code",
|
"cell_type": "code",
|
||||||
"execution_count": 1,
|
"execution_count": 10,
|
||||||
"id": "6c70a8c3",
|
"id": "6c70a8c3",
|
||||||
"metadata": {},
|
"metadata": {},
|
||||||
"outputs": [],
|
"outputs": [],
|
||||||
@@ -27,7 +27,7 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"cell_type": "code",
|
"cell_type": "code",
|
||||||
"execution_count": 2,
|
"execution_count": 11,
|
||||||
"id": "ff971ca9",
|
"id": "ff971ca9",
|
||||||
"metadata": {},
|
"metadata": {},
|
||||||
"outputs": [],
|
"outputs": [],
|
||||||
@@ -49,7 +49,7 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"cell_type": "code",
|
"cell_type": "code",
|
||||||
"execution_count": 3,
|
"execution_count": 12,
|
||||||
"id": "fc2c6d2b",
|
"id": "fc2c6d2b",
|
||||||
"metadata": {},
|
"metadata": {},
|
||||||
"outputs": [],
|
"outputs": [],
|
||||||
@@ -59,7 +59,7 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"cell_type": "code",
|
"cell_type": "code",
|
||||||
"execution_count": 65,
|
"execution_count": 13,
|
||||||
"id": "32ed5ff1",
|
"id": "32ed5ff1",
|
||||||
"metadata": {},
|
"metadata": {},
|
||||||
"outputs": [],
|
"outputs": [],
|
||||||
@@ -73,29 +73,29 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"cell_type": "code",
|
"cell_type": "code",
|
||||||
"execution_count": 24,
|
"execution_count": 32,
|
||||||
"id": "13c77a09",
|
"id": "13c77a09",
|
||||||
"metadata": {},
|
"metadata": {},
|
||||||
"outputs": [
|
"outputs": [
|
||||||
{
|
{
|
||||||
"data": {
|
"data": {
|
||||||
"text/plain": [
|
"text/plain": [
|
||||||
"WrappedApiResponse[PlacedOrderModel](status='OK', data=PlacedOrderModel(id=2050968195556405248, external_id='568330596456633775076894572695283213707051390160460447061058608689459399656'), error=None, pagination=None)"
|
"WrappedApiResponse[PlacedOrderModel](status='OK', data=PlacedOrderModel(id=2051770708505657344, external_id='2340464894308509827608947727447767756843769945398481824754838465475567294613'), error=None, pagination=None)"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"execution_count": 24,
|
"execution_count": 32,
|
||||||
"metadata": {},
|
"metadata": {},
|
||||||
"output_type": "execute_result"
|
"output_type": "execute_result"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"source": [
|
"source": [
|
||||||
"symbol = 'LIT-USD'\n",
|
"symbol = 'HYPE-USD'\n",
|
||||||
"qty = 3\n",
|
"qty = 11.2\n",
|
||||||
"price = 0.9090\n",
|
"price = 45.455\n",
|
||||||
"side = 'BUY'\n",
|
"side = 'SELL'\n",
|
||||||
"taker_fee = 0.00025\n",
|
"taker_fee = 0.00025\n",
|
||||||
"post_only = False\n",
|
"post_only = False\n",
|
||||||
"reduce_only = True\n",
|
"reduce_only = False\n",
|
||||||
"\n",
|
"\n",
|
||||||
"order_resp = await trading_client.place_order(\n",
|
"order_resp = await trading_client.place_order(\n",
|
||||||
" market_name=symbol,\n",
|
" market_name=symbol,\n",
|
||||||
@@ -109,6 +109,53 @@
|
|||||||
"order_resp"
|
"order_resp"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"cell_type": "code",
|
||||||
|
"execution_count": 29,
|
||||||
|
"id": "d751e661",
|
||||||
|
"metadata": {},
|
||||||
|
"outputs": [
|
||||||
|
{
|
||||||
|
"ename": "_IncompleteInputError",
|
||||||
|
"evalue": "incomplete input (2582004791.py, line 1)",
|
||||||
|
"output_type": "error",
|
||||||
|
"traceback": [
|
||||||
|
" \u001b[36mCell\u001b[39m\u001b[36m \u001b[39m\u001b[32mIn[29]\u001b[39m\u001b[32m, line 1\u001b[39m\n\u001b[31m \u001b[39m\u001b[31mtaker_fee = str(0.00025\u001b[39m\n ^\n\u001b[31m_IncompleteInputError\u001b[39m\u001b[31m:\u001b[39m incomplete input\n"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"source": [
|
||||||
|
"taker_fee = str(0.00025"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cell_type": "code",
|
||||||
|
"execution_count": null,
|
||||||
|
"metadata": {},
|
||||||
|
"outputs": [
|
||||||
|
{
|
||||||
|
"data": {
|
||||||
|
"text/plain": [
|
||||||
|
"True"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"execution_count": 30,
|
||||||
|
"metadata": {},
|
||||||
|
"output_type": "execute_result"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"source": [
|
||||||
|
"taker_fee"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cell_type": "code",
|
||||||
|
"execution_count": null,
|
||||||
|
"id": "8af88032",
|
||||||
|
"metadata": {},
|
||||||
|
"outputs": [],
|
||||||
|
"source": []
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"cell_type": "code",
|
"cell_type": "code",
|
||||||
"execution_count": 17,
|
"execution_count": 17,
|
||||||
|
|||||||
@@ -1,870 +0,0 @@
|
|||||||
|
|
||||||
from x10.utils.http import WrappedApiResponse
|
|
||||||
from x10.perpetual.trading_client.trading_client import PerpetualTradingClient
|
|
||||||
import asyncio
|
|
||||||
import json
|
|
||||||
import logging
|
|
||||||
import math
|
|
||||||
import os
|
|
||||||
import time
|
|
||||||
import traceback
|
|
||||||
from datetime import datetime, timezone
|
|
||||||
from decimal import ROUND_DOWN, ROUND_UP, ROUND_HALF_UP, Decimal
|
|
||||||
from typing import AsyncContextManager
|
|
||||||
from dataclasses import dataclass, asdict
|
|
||||||
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, PlacedOrderModel
|
|
||||||
|
|
||||||
import modules.utils as utils
|
|
||||||
import modules.aster_auth as aster_auth
|
|
||||||
import modules.extended_auth as extend_auth
|
|
||||||
import modules.structs as structs
|
|
||||||
|
|
||||||
### Clients ###
|
|
||||||
EXTEND_CLIENT: PerpetualTradingClient
|
|
||||||
CON: AsyncContextManager
|
|
||||||
VAL_KEY: valkey.Valkey
|
|
||||||
|
|
||||||
### Logging ###
|
|
||||||
load_dotenv()
|
|
||||||
LOG_FILEPATH: str = f'{os.getenv(key="LOGS_PATH")}/Fund_Rate_Algo.log'
|
|
||||||
|
|
||||||
### Algo Config ###
|
|
||||||
Config: structs.Algo_Config
|
|
||||||
|
|
||||||
### Exchanges ###
|
|
||||||
Aster: structs.Perpetual_Exchange
|
|
||||||
Extend: structs.Perpetual_Exchange
|
|
||||||
|
|
||||||
### Globals ###
|
|
||||||
Open_Symbols: list[str] = []
|
|
||||||
Last_Aster_Fill_Time_Ts: float = 0.00
|
|
||||||
Just_Rejected_Or_Expired: bool = False
|
|
||||||
|
|
||||||
Aster_Open_Orders: list[dict] = []
|
|
||||||
Extend_Open_Orders: list[dict] = []
|
|
||||||
|
|
||||||
### Flags ###
|
|
||||||
# Flags = structs.Flags()
|
|
||||||
|
|
||||||
|
|
||||||
### Define Ordering Logic ###
|
|
||||||
'''
|
|
||||||
Notes
|
|
||||||
- handle increasing vs flattening
|
|
||||||
- if increasing, set not reduce only
|
|
||||||
- if flattening, set as reduce only and make sure allowed to trade below min notional, and qty calc should be exact
|
|
||||||
- handle opportunistic vs immediate
|
|
||||||
- handle cancel-replace manually for aster and sometimes manually for extend (e.g. cant change certain things on an existing order)
|
|
||||||
- gracefully handle err responses (well known err codes e.g.) and response errors (e.g. json fails to parse)
|
|
||||||
'''
|
|
||||||
|
|
||||||
async def cancel_aster_order(open_order_id: str):
|
|
||||||
global Aster_Open_Orders
|
|
||||||
start = time.time()
|
|
||||||
cancel_order: dict = {
|
|
||||||
"url": "/fapi/v3/order",
|
|
||||||
"method": "DELETE",
|
|
||||||
"params": {
|
|
||||||
'symbol': Aster.symbol,
|
|
||||||
'orderId': open_order_id,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
cr: dict = await aster_auth.post_authenticated_url(cancel_order) # ty:ignore[invalid-assignment]
|
|
||||||
if cr.get('status', None) == 'CANCELED':
|
|
||||||
Aster_Open_Orders.pop(0)
|
|
||||||
else:
|
|
||||||
logging.warning(f'ASTER ORDER FAILED TO CANCEL DURING CR ({open_order_id}): RESP {cr}')
|
|
||||||
logging.info(f'TIMING - cancel_aster_order: {(time.time() - start)*1000:.2f}')
|
|
||||||
|
|
||||||
async def post_aster_order(
|
|
||||||
symbol: str,
|
|
||||||
side: str,
|
|
||||||
qty: Decimal,
|
|
||||||
price: Decimal,
|
|
||||||
reduceOnly: bool,
|
|
||||||
postOnly: bool
|
|
||||||
):
|
|
||||||
global Aster_Open_Orders
|
|
||||||
global Just_Rejected_Or_Expired
|
|
||||||
|
|
||||||
if postOnly:
|
|
||||||
timeInForce = 'GTX'
|
|
||||||
else:
|
|
||||||
timeInForce = 'GTC'
|
|
||||||
|
|
||||||
post_order = {
|
|
||||||
"url": "/fapi/v3/order",
|
|
||||||
"method": "POST",
|
|
||||||
"params": {
|
|
||||||
'symbol': symbol,
|
|
||||||
'side': side,
|
|
||||||
'type': 'LIMIT',
|
|
||||||
'timeInForce': timeInForce,
|
|
||||||
'quantity': qty,
|
|
||||||
'price': price,
|
|
||||||
'reduceOnly': reduceOnly
|
|
||||||
}
|
|
||||||
}
|
|
||||||
order_resp: dict = await aster_auth.post_authenticated_url(post_order) # ty:ignore[invalid-assignment]
|
|
||||||
if order_resp.get('orderId', None) is not None:
|
|
||||||
order_resp['original_price'] = price
|
|
||||||
order_resp['order_status'] = order_resp['status']
|
|
||||||
|
|
||||||
Aster_Open_Orders.append(order_resp)
|
|
||||||
Just_Rejected_Or_Expired = False
|
|
||||||
|
|
||||||
utils.send_tg_alert(f'FR_ALGO - ASTER Order ({order_resp['orderId']}). Start_$: {Aster.notional_position:.4f}; {side}: {float(qty)*float(price):.4f}; Price: {float(price):.4f}')
|
|
||||||
logging.info(f'ASTER ORDER PLACED SUCCESS: {order_resp}')
|
|
||||||
# print_summary(use_logging=True)
|
|
||||||
else:
|
|
||||||
logging.critical(f'*** Aster Order Response Abnormal: {order_resp}; post_order: {post_order}')
|
|
||||||
await kill_algo()
|
|
||||||
|
|
||||||
async def cancel_extend_order(order_id: str):
|
|
||||||
r = EXTEND_CLIENT.orders.cancel_order(order_id=order_id)
|
|
||||||
|
|
||||||
r = dict(r)
|
|
||||||
if r.get('status', None) == 'OK':
|
|
||||||
logging.info(f'EXTEND ORDER CANCELLED: {order_id}')
|
|
||||||
else:
|
|
||||||
logging.warning(f'EXTEND ORDER FAILED TO CANCEL DURING CR ({order_id}): RESP {r}')
|
|
||||||
|
|
||||||
async def post_extend_order(
|
|
||||||
symbol: str,
|
|
||||||
side: str,
|
|
||||||
qty: Decimal,
|
|
||||||
price: Decimal,
|
|
||||||
reduceOnly: bool,
|
|
||||||
postOnly: bool,
|
|
||||||
cxl_prev_order_id: str | None = None,
|
|
||||||
):
|
|
||||||
global Extend_Open_Orders
|
|
||||||
global Just_Rejected_Or_Expired
|
|
||||||
|
|
||||||
side = OrderSide.BUY if side == 'BUY' else OrderSide.SELL
|
|
||||||
taker_fee = Decimal("0.00025")
|
|
||||||
try:
|
|
||||||
order_resp: WrappedApiResponse[PlacedOrderModel] = await EXTEND_CLIENT.place_order(
|
|
||||||
market_name=symbol,
|
|
||||||
amount_of_synthetic=qty,
|
|
||||||
price=price,
|
|
||||||
side=side,
|
|
||||||
taker_fee=taker_fee,
|
|
||||||
previous_order_id=cxl_prev_order_id,
|
|
||||||
post_only=postOnly,
|
|
||||||
reduce_only=reduceOnly
|
|
||||||
)
|
|
||||||
except Exception as e:
|
|
||||||
logging.critical(e)
|
|
||||||
|
|
||||||
order_resp_dict = dict(order_resp)
|
|
||||||
|
|
||||||
if order_resp_dict.get('status', None) == 'ERROR':
|
|
||||||
if order_resp_dict['error']['code']==1142:
|
|
||||||
logging.info('Cant find edit order for Extend, skipping cancel.')
|
|
||||||
else:
|
|
||||||
logging.critical(f'*** Extend Order Response Abnormal: {order_resp};')
|
|
||||||
await kill_algo()
|
|
||||||
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)
|
|
||||||
order_dict['qty'] = str(qty)
|
|
||||||
order_dict['filled_qty'] = str(0)
|
|
||||||
order_dict['side'] = str(side)
|
|
||||||
|
|
||||||
Extend_Open_Orders.append(order_dict)
|
|
||||||
Just_Rejected_Or_Expired = False
|
|
||||||
utils.send_tg_alert(f'FR_ALGO - EXTEND Order ({order_dict.get('id', None)}). Start_$: {Extend.notional_position:.2f}; {str(side)}: {float(qty)*float(price):.2f}; Price: {float(price):.2f}')
|
|
||||||
logging.info(f'EXTEND ORDER PLACED SUCCESS: {order_dict}')
|
|
||||||
# print_summary(use_logging=True)
|
|
||||||
else:
|
|
||||||
logging.critical(f'*** Extend Order Response Abnormal: {order_resp};')
|
|
||||||
await kill_algo()
|
|
||||||
|
|
||||||
### OPEN ORDERS ###
|
|
||||||
async def handle_order_updates(exch: str, local_open_orders: list[dict], ws_open_orders: list[dict]) -> list[dict]: # exch = 'ASTER' | 'EXTEND'
|
|
||||||
global Just_Rejected_Or_Expired
|
|
||||||
global Last_Aster_Fill_Time_Ts
|
|
||||||
|
|
||||||
if ws_open_orders:
|
|
||||||
for idx, o in enumerate(local_open_orders):
|
|
||||||
o = dict(o)
|
|
||||||
if o.get('order_id') is not None:
|
|
||||||
ws_order_id_field = 'order_id'
|
|
||||||
elif o.get('orderId') is not None:
|
|
||||||
ws_order_id_field = 'orderId'
|
|
||||||
else:
|
|
||||||
ws_order_id_field = 'id'
|
|
||||||
|
|
||||||
order_id = o[ws_order_id_field]
|
|
||||||
order_orig_status: str = o.get('status') if o.get('status') is not None else o['order_status'] # ty:ignore[invalid-assignment]
|
|
||||||
order_update: list[dict] = [dict(ou) for ou in ws_open_orders if dict(ou).get('order_id', None) == order_id]
|
|
||||||
|
|
||||||
if len(order_update) > 0:
|
|
||||||
order_update: dict = order_update[0]
|
|
||||||
order_update_status: str = order_update.get('status') if order_update.get('status') is not None else order_update['order_status'] # ty:ignore[invalid-assignment]
|
|
||||||
order_status_changed: bool = order_orig_status.upper() != order_update_status.upper()
|
|
||||||
|
|
||||||
local_open_orders[idx]['order_id'] = order_id
|
|
||||||
local_open_orders[idx]['status'] = order_update_status
|
|
||||||
local_open_orders[idx]['price'] = order_update.get('price', 0) if order_update.get('price') is not None else order_update['original_price']
|
|
||||||
|
|
||||||
if order_status_changed:
|
|
||||||
logging.info(f'{exch} ORDER ({order_id}): {order_orig_status} -> {order_update_status}')
|
|
||||||
local_open_orders[idx] = order_update
|
|
||||||
if order_update_status in ['CANCELLED','CANCELED','EXPIRED','REJECTED']:
|
|
||||||
logging.info(f'{exch} ORDER CANCELLED or EXPIRED: {order_id}')
|
|
||||||
local_open_orders.pop(idx)
|
|
||||||
Just_Rejected_Or_Expired = True
|
|
||||||
utils.send_tg_alert(f'FR_ALGO - {exch} REJECTED ({order_id})')
|
|
||||||
elif order_update_status in ['PARTIALLY_FILLED']:
|
|
||||||
logging.info(f'{exch} ORDER PARTIALLY FILLED: {order_id}')
|
|
||||||
# await get_aster_collateral()
|
|
||||||
if exch=='ASTER':
|
|
||||||
await get_aster_notional_position(resp=ws_open_orders)
|
|
||||||
Last_Aster_Fill_Time_Ts = datetime.now().timestamp()*1000
|
|
||||||
else:
|
|
||||||
await get_extend_notional()
|
|
||||||
utils.send_tg_alert(f'FR_ALGO - {exch} PARTIALLY FILLED ({order_id})')
|
|
||||||
elif order_update_status in ['FILLED']:
|
|
||||||
logging.info(f'{exch} ORDER FILLED: {order_id}')
|
|
||||||
local_open_orders.pop(idx)
|
|
||||||
# await get_aster_collateral()
|
|
||||||
if exch=='ASTER':
|
|
||||||
await get_aster_notional_position(resp=ws_open_orders)
|
|
||||||
Last_Aster_Fill_Time_Ts = datetime.now().timestamp()*1000
|
|
||||||
else:
|
|
||||||
await get_extend_notional()
|
|
||||||
utils.send_tg_alert(f'FR_ALGO - {exch} FILLED ({order_id})')
|
|
||||||
else:
|
|
||||||
logging.critical(f'{exch} ORDER STATUS CHG TO UNEXPECTED VALUE, KILLING... ({order_id}): {order_orig_status} -> {order_update_status}')
|
|
||||||
await kill_algo()
|
|
||||||
return local_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) # ty:ignore[invalid-assignment]
|
|
||||||
|
|
||||||
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_account_open_symbols() -> list[str]:
|
|
||||||
fut_acct_positionRisk: dict = {
|
|
||||||
"url": "/fapi/v3/positionRisk",
|
|
||||||
"method": "GET",
|
|
||||||
"params": {
|
|
||||||
'symbol':''
|
|
||||||
}
|
|
||||||
}
|
|
||||||
try:
|
|
||||||
resp: list = await aster_auth.post_authenticated_url(req=fut_acct_positionRisk) # ty:ignore[invalid-assignment]
|
|
||||||
except Exception as e:
|
|
||||||
logging.critical(f'JSONDecodeError trying to get Aster open orders: {e}; resp: {resp}')
|
|
||||||
await kill_algo()
|
|
||||||
resp: list = []
|
|
||||||
ld = [ utils.symbol_to_extend_fmt(x['symbol']) for x in resp if abs(float(x.get('positionAmt', 0))) > 0]
|
|
||||||
return ld
|
|
||||||
|
|
||||||
async def get_aster_notional_position(resp: list | None = None):
|
|
||||||
global Aster
|
|
||||||
|
|
||||||
previous_notional_obj = Aster.notional_obj
|
|
||||||
previous_notional_position = Aster.notional_position
|
|
||||||
|
|
||||||
if resp:
|
|
||||||
pos_dict = [x for x in resp if x.get('symbol', None) == Aster.symbol]
|
|
||||||
if pos_dict:
|
|
||||||
pos_dict = pos_dict[0]
|
|
||||||
else:
|
|
||||||
pos_dict = {}
|
|
||||||
pos_dict['side'] = 'LONG'
|
|
||||||
pos_dict['entry_price'] = 0.00
|
|
||||||
pos_dict['position_amount'] = 0.00
|
|
||||||
pos_dict['unrealized_pnl'] = 0.00
|
|
||||||
pos_dict['timestamp_arrival'] = round(datetime.now().timestamp()*1000)
|
|
||||||
# logging.info('get_aster_notional - No Positions')
|
|
||||||
else:
|
|
||||||
logging.info('Getting Aster Notionals from API')
|
|
||||||
fut_acct_positionRisk: dict = {
|
|
||||||
"url": "/fapi/v3/positionRisk",
|
|
||||||
"method": "GET",
|
|
||||||
"params": {
|
|
||||||
'symbol': Aster.symbol,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
try:
|
|
||||||
resp: list = await aster_auth.post_authenticated_url(req=fut_acct_positionRisk) # ty:ignore[invalid-assignment]
|
|
||||||
except Exception as e:
|
|
||||||
logging.critical(f'JSONDecodeError trying to get Aster notional: {e}; resp: {resp}')
|
|
||||||
await kill_algo()
|
|
||||||
resp: list = []
|
|
||||||
pos_dict = [x for x in resp if x.get('symbol', None) == Aster.symbol]
|
|
||||||
if pos_dict:
|
|
||||||
pos_dict = pos_dict[0]
|
|
||||||
else:
|
|
||||||
pos_dict = {}
|
|
||||||
pos_dict['side'] = 'LONG'
|
|
||||||
pos_dict['entry_price'] = 0.00
|
|
||||||
pos_dict['position_amount'] = 0.00
|
|
||||||
pos_dict['unrealized_pnl'] = 0.00
|
|
||||||
logging.info('get_aster_notional - No Positions')
|
|
||||||
pos_dict['timestamp_arrival'] = round(datetime.now().timestamp()*1000)
|
|
||||||
|
|
||||||
if previous_notional_obj:
|
|
||||||
if previous_notional_obj['timestamp_arrival'] > pos_dict['timestamp_arrival']:
|
|
||||||
# logging.info(f'ASTER NOTIONAL: prev timestamp ({pd.to_datetime(previous_notional_obj['timestamp_arrival'], unit='ms')}) > new timestamp ({pd.to_datetime(pos_dict['timestamp_arrival'], unit='ms')}); skipping')
|
|
||||||
return
|
|
||||||
|
|
||||||
Aster.notional_obj = pos_dict
|
|
||||||
|
|
||||||
if len(pos_dict) < 1:
|
|
||||||
logging.info(f'BAD NOTIONAL - ASTER CHANGE: Empty pos_dict: {pos_dict}; resp: {resp}')
|
|
||||||
await kill_algo()
|
|
||||||
|
|
||||||
Aster.unrealized_pnl = float(pos_dict['unrealized_pnl']) if pos_dict.get('unrealized_pnl') is not None else float(pos_dict['unRealizedProfit'])
|
|
||||||
|
|
||||||
if pos_dict.get('notional') is not None:
|
|
||||||
Aster.notional_position = float(pos_dict['notional']) - Aster.unrealized_pnl
|
|
||||||
else:
|
|
||||||
Aster.notional_position = float(pos_dict['position_amount'])*float(pos_dict['entry_price'])
|
|
||||||
if pos_dict.get('leverage') is not None:
|
|
||||||
Aster.mult = int(pos_dict['leverage'])
|
|
||||||
if abs(Aster.notional_position) > Config.Config.Max_Target_Notional*Config.Config.Max_Order_Over_Notional_Ratio:
|
|
||||||
logging.info(f'BAD NOTIONAL - ASTER CHANGE: {previous_notional_position} -> {Aster.notional_position}; UR PNL: {Aster.unrealized_pnl}; MULT: {Aster.mult}; pos_dict: {pos_dict}; resp: {resp}; max_tgt_notional: {Config.Config.Max_Target_Notional}')
|
|
||||||
await kill_algo()
|
|
||||||
if Aster.notional_position != previous_notional_position:
|
|
||||||
logging.info(f'ASTER NOTIONAL CHANGE: {previous_notional_position:.2f} -> {Aster.notional_position:.2f}; UR PNL: {Aster.unrealized_pnl:.2f}; MULT: {Aster.mult:.0f}; resp: {bool(resp)}')
|
|
||||||
|
|
||||||
async def get_extend_account_open_symbols() -> list[str]:
|
|
||||||
resp = dict(await EXTEND_CLIENT.account.get_positions()).get('data', [])
|
|
||||||
ld = [x.market for x in list(resp) if abs(float(x.size)) > 0]
|
|
||||||
return ld
|
|
||||||
|
|
||||||
async def set_comb_open_symbols() -> None:
|
|
||||||
global Open_Symbols
|
|
||||||
|
|
||||||
open_aster_symbols = await get_aster_account_open_symbols()
|
|
||||||
open_extend_symbols = await get_extend_account_open_symbols()
|
|
||||||
|
|
||||||
Open_Symbols = list(set(open_aster_symbols + open_extend_symbols))
|
|
||||||
|
|
||||||
async def get_extend_notional(resp: list | None = None):
|
|
||||||
global Extend
|
|
||||||
|
|
||||||
previous_notional_obj = Extend.notional_obj
|
|
||||||
previous_notional_position = Extend.notional_position
|
|
||||||
|
|
||||||
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.symbol]
|
|
||||||
if pos_dict:
|
|
||||||
pos_dict = pos_dict[0]
|
|
||||||
pos_dict['timestamp_arrival'] = round(datetime.now().timestamp()*1000)
|
|
||||||
else:
|
|
||||||
pos_dict = {}
|
|
||||||
pos_dict['side'] = 'LONG'
|
|
||||||
pos_dict['value'] = 0.00
|
|
||||||
pos_dict['unrealised_pnl'] = 0.00
|
|
||||||
pos_dict['timestamp_arrival'] = round(datetime.now().timestamp()*1000)
|
|
||||||
logging.info('get_extend_notional - No Positions')
|
|
||||||
else:
|
|
||||||
pos_dict = [dict(d) for d in resp if dict(d).get('market') == Extend.symbol]
|
|
||||||
if pos_dict:
|
|
||||||
pos_dict = pos_dict[0]
|
|
||||||
else:
|
|
||||||
pos_dict = {}
|
|
||||||
pos_dict['side'] = 'LONG'
|
|
||||||
pos_dict['value'] = 0.00
|
|
||||||
pos_dict['unrealised_pnl'] = 0.00
|
|
||||||
pos_dict['timestamp_arrival'] = round(datetime.now().timestamp()*1000)
|
|
||||||
# logging.info('get_extend_notional - No Positions')
|
|
||||||
|
|
||||||
# pos_dict['timestamp_arrival'] = round(datetime.now().timestamp()*1000)
|
|
||||||
|
|
||||||
if previous_notional_obj:
|
|
||||||
if previous_notional_obj['timestamp_arrival'] > pos_dict['timestamp_arrival']:
|
|
||||||
# logging.info(f'EXTEND NOTIONAL: prev timestamp ({pd.to_datetime(previous_notional_obj['timestamp_arrival'], unit='ms')}) > new timestamp ({pd.to_datetime(pos_dict['timestamp_arrival'], unit='ms')}); skipping')
|
|
||||||
return
|
|
||||||
else:
|
|
||||||
previous_notional_obj = {}
|
|
||||||
|
|
||||||
Extend.notional_obj = pos_dict
|
|
||||||
|
|
||||||
Extend.unrealized_pnl = pos_dict.get('unrealised_pnl', 0)
|
|
||||||
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(Extend.unrealized_pnl)
|
|
||||||
Extend.mult = pos_dict.get('leverage', Extend.mult)
|
|
||||||
if abs(Extend.notional_position) > Config.Config.Max_Target_Notional*Config.Config.Max_Order_Over_Notional_Ratio:
|
|
||||||
logging.info(f'BAD NOTIONAL - EXTEND CHANGE: {previous_notional_position} -> {Extend.notional_position}; UR PNL: {Extend.unrealized_pnl}; MULT: {Extend.mult}; pos_dict: {pos_dict}; resp: {resp}')
|
|
||||||
await kill_algo()
|
|
||||||
if Extend.notional_position != previous_notional_position:
|
|
||||||
logging.info(f'EXTEND NOTIONAL CHANGE: {previous_notional_position} [{previous_notional_obj.get('timestamp_arrival')}] -> {Extend.notional_position:.2f} [{Extend.notional_obj['timestamp_arrival']}]; UR PNL: {Extend.unrealized_pnl:.2f}; MULT: {Extend.mult}; resp: {bool(resp)}')
|
|
||||||
|
|
||||||
### EXCHANGE INFO ###
|
|
||||||
async def get_aster_exch_info(symbol_override: str | None = None):
|
|
||||||
global Aster
|
|
||||||
|
|
||||||
if symbol_override:
|
|
||||||
Aster.symbol = utils.symbol_to_aster_fmt(symbol_override)
|
|
||||||
|
|
||||||
fut_acct_exchangeInfo: dict = {
|
|
||||||
"url": "/fapi/v3/exchangeInfo",
|
|
||||||
"method": "GET",
|
|
||||||
"params": {}
|
|
||||||
}
|
|
||||||
r: dict = await aster_auth.post_authenticated_url(fut_acct_exchangeInfo) # ty:ignore[invalid-assignment]
|
|
||||||
s: list = r['symbols']
|
|
||||||
d: dict = [d for d in s if d.get('symbol', None) == Aster.symbol][0]
|
|
||||||
f: dict = [f for f in d['filters'] if f.get('filterType', None) == 'LOT_SIZE'][0]
|
|
||||||
q: dict = [f for f in d['filters'] if f.get('filterType', None) == 'PRICE_FILTER'][0]
|
|
||||||
n: dict = [f for f in d['filters'] if f.get('filterType', None) == 'MIN_NOTIONAL'][0]
|
|
||||||
|
|
||||||
min_qty = float(f['minQty'])
|
|
||||||
min_qty = int(min_qty) if min_qty == int(min_qty) else min_qty
|
|
||||||
|
|
||||||
min_price = float(q['minPrice'])
|
|
||||||
min_price = int(min_price) if min_price == int(min_price) else min_price
|
|
||||||
Aster.min_order_size = min_qty
|
|
||||||
Aster.min_price = min_price
|
|
||||||
Aster.min_notional = float(n['notional'])
|
|
||||||
|
|
||||||
async def get_extend_exch_info(symbol_override: str | None = None):
|
|
||||||
global Extend
|
|
||||||
|
|
||||||
if symbol_override:
|
|
||||||
Extend.symbol = utils.symbol_to_extend_fmt(symbol_override)
|
|
||||||
|
|
||||||
r = await EXTEND_CLIENT.markets_info.get_markets_dict()
|
|
||||||
Extend.min_order_size = float(r[Extend.symbol].trading_config.min_order_size)
|
|
||||||
Extend.min_price = float(r[Extend.symbol].trading_config.min_price_change)
|
|
||||||
|
|
||||||
### CANCEL ORDERS ###
|
|
||||||
async def aster_cancel_all_orders():
|
|
||||||
cancel_all_open_orders = {
|
|
||||||
"url": "/fapi/v3/allOpenOrders",
|
|
||||||
"method": "DELETE",
|
|
||||||
"params": {
|
|
||||||
'symbol': Aster.symbol,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
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.symbol])
|
|
||||||
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 Config
|
|
||||||
|
|
||||||
global Aster
|
|
||||||
global Extend
|
|
||||||
|
|
||||||
global Open_Symbols
|
|
||||||
global Last_Aster_Fill_Time_Ts
|
|
||||||
global Just_Rejected_Or_Expired
|
|
||||||
|
|
||||||
global Aster_Open_Orders
|
|
||||||
global Extend_Open_Orders
|
|
||||||
|
|
||||||
# global Flags
|
|
||||||
|
|
||||||
try:
|
|
||||||
while True:
|
|
||||||
loop_start = time.time()
|
|
||||||
# print('__________Start___________')
|
|
||||||
|
|
||||||
### Load Algo Config ###
|
|
||||||
Config = json.loads(VAL_KEY.get('fr_orchestrator_output')) # ty:ignore[invalid-argument-type]
|
|
||||||
Config = structs.Algo_Config(**Config)
|
|
||||||
Config.Config.Max_Target_Notional = float(min([Aster.mult, Extend.mult]) * Config.Config.Target_Open_Cash_Position)
|
|
||||||
|
|
||||||
### Load Data from Feedhandlers ###
|
|
||||||
best_symbol_by_exchange: dict = json.loads(s=VAL_KEY.get(name='fr_engine_best_fund_rate_output')) # ty:ignore[invalid-argument-type]
|
|
||||||
best_symbol_by_exchange_aster = structs.Perpetual_Exchange(**best_symbol_by_exchange['ASTER'])
|
|
||||||
best_symbol_by_exchange_extend = structs.Perpetual_Exchange(**best_symbol_by_exchange['EXTEND'])
|
|
||||||
|
|
||||||
# Fund Rates
|
|
||||||
# aster_fund_rate_dict: Any = VAL_KEY.get('fund_rate_aster')
|
|
||||||
# aster_fund_rate_dict: dict = json.loads(s=aster_fund_rate_dict) if aster_fund_rate_dict is not None else {}
|
|
||||||
# if aster_fund_rate_dict.get('symbol', None) != Aster.symbol:
|
|
||||||
# aster_fund_rate: float = Aster.initial_funding_rate
|
|
||||||
# # logging.info(f'ASTER Symbol mismatch: {ASTER_FUND_RATE_DICT}; expected symbol: {ASTER.symbol}')
|
|
||||||
# # raise ValueError(f'ASTER Symbol mismatch: {ASTER_FUND_RATE_DICT}; expected symbol: {ASTER.symbol}')
|
|
||||||
# else:
|
|
||||||
# aster_fund_rate: float = float(aster_fund_rate_dict.get('funding_rate', 0))
|
|
||||||
|
|
||||||
# extend_fund_rate_dict: Any = VAL_KEY.get('fund_rate_extended')
|
|
||||||
# extend_fund_rate_dict: dict = json.loads(s=extend_fund_rate_dict) if extend_fund_rate_dict is not None else {}
|
|
||||||
# if extend_fund_rate_dict.get('symbol', None) != Extend.symbol:
|
|
||||||
# extend_fund_rate: float = Extend.initial_funding_rate
|
|
||||||
# # logging.info(f'ASTER Symbol mismatch: {EXTENDED_FUND_RATE_DICT}; expected symbol: {EXTEND.symbol}')
|
|
||||||
# # raise ValueError(f'ASTER Symbol mismatch: {EXTENDED_FUND_RATE_DICT}; expected symbol: {EXTEND.symbol}')
|
|
||||||
# else:
|
|
||||||
# extend_fund_rate: float = float(extend_fund_rate_dict.get('funding_rate', 0))
|
|
||||||
|
|
||||||
# if Config.Overrides.Flip_Side_For_Testing:
|
|
||||||
# aster_fund_rate = aster_fund_rate * -1
|
|
||||||
# extend_fund_rate = extend_fund_rate * -1
|
|
||||||
|
|
||||||
# aster_fund_rate_time = float(aster_fund_rate_dict.get('next_funding_time_ts_ms', 0))
|
|
||||||
# aster_fund_rate_time = aster_fund_rate_time+(60*60*1000) if aster_fund_rate_time < (datetime.now().timestamp()*1000) else aster_fund_rate_time
|
|
||||||
|
|
||||||
# extend_fund_rate_time = max([float(extend_fund_rate_dict.get('next_funding_time_ts_ms', 0)), 0])
|
|
||||||
# extend_fund_rate_time = extend_fund_rate_time+(60*60*1000) if extend_fund_rate_time < (datetime.now().timestamp()*1000) else extend_fund_rate_time
|
|
||||||
|
|
||||||
# Tickers
|
|
||||||
aster_ticker_dict: Any = VAL_KEY.get('fut_ticker_aster')
|
|
||||||
aster_ticker_dict: dict = json.loads(s=aster_ticker_dict) if aster_ticker_dict is not None else {}
|
|
||||||
if ( aster_ticker_dict.get('symbol', None) != Aster.symbol ) and not(Config.Overrides.Flatten_Open_Positions):
|
|
||||||
logging.warning(f'ASTER Symbol mismatch: {aster_ticker_dict}; expected symbol: {Aster.symbol}')
|
|
||||||
VAL_KEY.set(name='fr_algo_working_symbol', value=json.dumps(obj={'ASTER': asdict(obj=Aster), 'EXTEND': asdict(obj=Extend)}))
|
|
||||||
time.sleep(5)
|
|
||||||
continue
|
|
||||||
# raise ValueError(f'ASTER Symbol mismatch: {ASTER_TICKER_DICT}; expected symbol: {ASTER.symbol}')
|
|
||||||
|
|
||||||
extend_ticker_dict: Any = VAL_KEY.get('fut_ticker_extended')
|
|
||||||
extend_ticker_dict: dict = json.loads(s=extend_ticker_dict) if extend_ticker_dict is not None else {}
|
|
||||||
if ( extend_ticker_dict.get('symbol', None) != Extend.symbol) and not(Config.Overrides.Flatten_Open_Positions):
|
|
||||||
logging.warning(f'EXTEND Symbol mismatch: {extend_ticker_dict}; expected symbol: {Extend.symbol}')
|
|
||||||
VAL_KEY.set(name='fr_algo_working_symbol', value=json.dumps(obj={'ASTER': asdict(obj=Aster), 'EXTEND': asdict(obj=Extend)}))
|
|
||||||
time.sleep(5)
|
|
||||||
continue
|
|
||||||
# raise ValueError(f'EXTEND Symbol mismatch: {EXTENDED_TICKER_DICT}; expected symbol: {EXTEND.symbol}')
|
|
||||||
|
|
||||||
### Load Local Notional Updates from WS ###
|
|
||||||
aster_ws_pos_updates: Any = VAL_KEY.get(name='fr_aster_user_positions')
|
|
||||||
aster_ws_pos_updates: list = json.loads(s=aster_ws_pos_updates) if aster_ws_pos_updates is not None else []
|
|
||||||
extend_ws_pos_updates: Any = VAL_KEY.get('fr_extended_user_positions')
|
|
||||||
extend_ws_pos_updates: list = json.loads(extend_ws_pos_updates) if extend_ws_pos_updates is not None else []
|
|
||||||
|
|
||||||
if len(aster_ws_pos_updates) > 0:
|
|
||||||
await get_aster_notional_position(resp=aster_ws_pos_updates)
|
|
||||||
|
|
||||||
if len(extend_ws_pos_updates) > 0:
|
|
||||||
await get_extend_notional(resp=extend_ws_pos_updates)
|
|
||||||
|
|
||||||
### Load Local Order Updates from WS ###
|
|
||||||
aster_ws_order_updates: Any = VAL_KEY.get('fr_aster_user_orders')
|
|
||||||
aster_ws_order_updates: list = json.loads(aster_ws_order_updates) if aster_ws_order_updates is not None else []
|
|
||||||
extend_ws_order_updates: Any = VAL_KEY.get('fr_extended_user_orders')
|
|
||||||
extend_ws_order_updates: list = json.loads(extend_ws_order_updates) if extend_ws_order_updates is not None else []
|
|
||||||
|
|
||||||
### CHECK NO MORE THAN 1 OPEN ORDER ON EITHER EXCHANGE ###
|
|
||||||
if len(Aster_Open_Orders) > 1 or len(Extend_Open_Orders) > 1:
|
|
||||||
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')
|
|
||||||
|
|
||||||
### Update Local Open Orders w Changes from WS ###
|
|
||||||
Aster_Open_Orders = await handle_order_updates(exch='ASTER', local_open_orders=Aster_Open_Orders, ws_open_orders=aster_ws_order_updates)
|
|
||||||
Extend_Open_Orders = await handle_order_updates(exch='EXTEND', local_open_orders=Extend_Open_Orders, ws_open_orders=extend_ws_order_updates)
|
|
||||||
|
|
||||||
### Decisions ###
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
### Logging ###
|
|
||||||
def print_summary(use_logging: bool = False):
|
|
||||||
OUT: Any = logging.info if use_logging else print
|
|
||||||
|
|
||||||
# ASTER: [ Available Collateral: {ASTER_AVAIL_COLLATERAL:.4f} ] | EXTEND: [ Available Collateral: {EXTEND_AVAIL_COLLATERAL:.4f} ]
|
|
||||||
OUT(f'''
|
|
||||||
FLIP SIDES FOR TESTING?: {Config.Overrides.Flip_Side_For_Testing}; ASTER ORDER ENABLED? {Config.Overrides.Allow_Ordering_Aster}; EXTEND ORDER ENABLED? {Config.Overrides.Allow_Ordering_Extend}
|
|
||||||
|
|
||||||
MKT : Aster: {Aster.symbol:<10} (best: {best_symbol_by_exchange_aster.symbol}) | Extend: {Extend.symbol:<10} (best: {best_symbol_by_exchange_extend.symbol})
|
|
||||||
{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: {'LONG PAYS SHORT' if aster_fund_rate > 0 else 'SHORT PAYS LONG'} | EXTEND: {'LONG PAYS SHORT' if extend_fund_rate > 0 else 'SHORT PAYS LONG'}
|
|
||||||
ASTER: [ Notional Position $ : {Aster.notional_position:.4f} ] | EXTEND: [ Notional Position $ : {Extend.notional_position:.4f} ]
|
|
||||||
|
|
||||||
ALPHA SIDE : {ALPHA_EXCH} [{ALPHA_CARRY_SIDE}]
|
|
||||||
ALPHA SIGNAL: {alpha_signal}; Current {current_ratio:.4f} [{current_ratio*10_000:.2f}scl] {">" if ALPHA_CARRY_SIDE=='BUY' else "<"} Model {alpha_model_ratio:.4f} [{alpha_model_ratio*10_000:.2f}scl]
|
|
||||||
|
|
||||||
TGT NOTIONAL: $ {abs(ALPHA_TGT_NOTIONAL_FINAL):.2f}; Flatten Open Positions Flag? {Config.Overrides.Flatten_Open_Positions}; Opportunistic? {Config.Overrides.Flatten_Open_Positions_Opportunistic}
|
|
||||||
AT TARGET? : {at_notional_target.value}; is_locked?: {at_notional_target.is_locked}
|
|
||||||
|
|
||||||
|
|
||||||
--- ASTER OPEN ORDERS ---
|
|
||||||
{Aster_Open_Orders}
|
|
||||||
|
|
||||||
--- EXTEND OPEN ORDERS ---
|
|
||||||
{Extend_Open_Orders}
|
|
||||||
''')
|
|
||||||
if Config.Logging.Log_Summary_Each_Loop:
|
|
||||||
print_summary(use_logging=True)
|
|
||||||
if Config.Logging.Print_Summary_Each_Loop:
|
|
||||||
print_summary(use_logging=False)
|
|
||||||
|
|
||||||
### ASTER
|
|
||||||
if Config.Overrides.Allow_Ordering_Aster and ASTER_TGT_TAIL_ORDERABLE: # Tier 1 Overrides
|
|
||||||
if alpha_signal or Config.Overrides.Flatten_Open_Positions: # Tier 2 Overrides / Alpha
|
|
||||||
skip = False
|
|
||||||
side = 'BUY' if ASTER_TGT_TAIL_BASE_QTY > 0.00 else 'SELL'
|
|
||||||
qty = Decimal(value=str(abs(ASTER_TGT_TAIL_BASE_QTY)))
|
|
||||||
price = ASTER_TOB_PX - ( float(Aster.min_price)*int(Config.Config.Price_Worsener_Aster) ) if side == 'BUY' else ASTER_TOB_PX + ( float(Aster.min_price)*int(Config.Config.Price_Worsener_Aster) )
|
|
||||||
|
|
||||||
if abs( ( float(ASTER_TGT_TAIL_BASE_QTY)*float(price) ) + Aster.notional_position ) > Config.Config.Max_Target_Notional*Config.Config.Max_Order_Over_Notional_Ratio:
|
|
||||||
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: # Cancel Open Order?
|
|
||||||
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',0)) if Aster_Open_Orders[0].get('price') is not None else float(Aster_Open_Orders[0]['original_price'])
|
|
||||||
|
|
||||||
open_order_dict = dict(Aster_Open_Orders[0])
|
|
||||||
open_order_id = str(open_order_dict['order_id'])
|
|
||||||
open_order_px = float(open_order_dict['price'])
|
|
||||||
|
|
||||||
min_price = Aster.min_price
|
|
||||||
min_price = int(min_price) if min_price == int(min_price) else min_price
|
|
||||||
if Decimal(str( float(open_order_px) - float(price) )).quantize(Decimal(str(min_price)), rounding=ROUND_HALF_UP) == 0.00:
|
|
||||||
if Config.Logging.Print_Summary_Each_Loop:
|
|
||||||
print('ASTER OPEN ORDER NO PX CHG; SKIPPING')
|
|
||||||
skip = True
|
|
||||||
else:
|
|
||||||
await cancel_aster_order(open_order_id) # ty:ignore[invalid-argument-type]
|
|
||||||
|
|
||||||
if ASTER_TGT_TAIL_BASE_QTY == 0.00:
|
|
||||||
logging.info('ASTER TRYNG TO ORDER 0.00 BASE QTY, SKIPPING')
|
|
||||||
skip = True
|
|
||||||
|
|
||||||
if not skip:
|
|
||||||
min_price = Aster.min_price
|
|
||||||
min_price = int(min_price) if min_price == int(min_price) else min_price
|
|
||||||
price: Decimal = Decimal(str(price)).quantize(Decimal(str(min_price)), rounding=ROUND_HALF_UP)
|
|
||||||
|
|
||||||
if price == Decimal(str(0.00)).quantize(Decimal(str(min_price)), rounding=ROUND_HALF_UP):
|
|
||||||
logging.info('ASTER TRYNG TO ORDER with A PRICE OF 0.00, SKIPPING')
|
|
||||||
continue
|
|
||||||
|
|
||||||
if ( qty < Aster.min_order_size ) or ( (qty*price) < Aster.min_notional ):
|
|
||||||
reduceOnly = True
|
|
||||||
else:
|
|
||||||
reduceOnly = False
|
|
||||||
|
|
||||||
await post_aster_order(
|
|
||||||
symbol=Aster.symbol,
|
|
||||||
side=side,
|
|
||||||
qty=qty,
|
|
||||||
price=price,
|
|
||||||
reduceOnly=reduceOnly,
|
|
||||||
postOnly=True,
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
pass
|
|
||||||
elif not(ASTER_TGT_TAIL_ORDERABLE) and Aster_Open_Orders:
|
|
||||||
logging.info('ASTER HAS NO TAIL BUT OPEN ORDERS - CANCELLING OPEN ORDERS')
|
|
||||||
await extend_cancel_all_orders()
|
|
||||||
|
|
||||||
### EXTEND ###
|
|
||||||
if Config.Overrides.Allow_Ordering_Extend and EXTEND_TGT_TAIL_ORDERABLE: # Tier 1 Overrides
|
|
||||||
if alpha_signal or Config.Overrides.Flatten_Open_Positions: # Tier 2 Overrides / Alpha
|
|
||||||
skip = False
|
|
||||||
side = 'BUY' if EXTEND_TGT_TAIL_BASE_QTY > 0.00 else 'SELL'
|
|
||||||
qty = Decimal(value=str(abs(EXTEND_TGT_TAIL_BASE_QTY)))
|
|
||||||
price = EXTEND_TOB_PX - ( float(Extend.min_price)*int(Config.Config.Price_Worsener_Extend) ) if side == 'BUY' else EXTEND_TOB_PX + ( float(Extend.min_price)*int(Config.Config.Price_Worsener_Extend) ) # ty:ignore[invalid-assignment]
|
|
||||||
|
|
||||||
if abs( ( float(EXTEND_TGT_TAIL_BASE_QTY)*float(price) ) + Extend.notional_position ) > Config.Config.Max_Target_Notional*Config.Config.Max_Order_Over_Notional_Ratio:
|
|
||||||
logging.info(f'TRYING TO ORDER OVER MAX NOTIOANL - EXTEND: {Extend.notional_position} + {float(EXTEND_TGT_TAIL_BASE_QTY)*float(price)} (qty: {float(EXTEND_TGT_TAIL_BASE_QTY):.2f}; px: {float(price):.2f})')
|
|
||||||
await kill_algo()
|
|
||||||
|
|
||||||
if Extend_Open_Orders: # Cancel Open Order?
|
|
||||||
open_order_dict = dict(Extend_Open_Orders[0])
|
|
||||||
open_order_id = str(open_order_dict['external_id'])
|
|
||||||
open_order_px = float(open_order_dict['price'])
|
|
||||||
|
|
||||||
min_price = Extend.min_price
|
|
||||||
min_price = int(min_price) if min_price == int(min_price) else min_price
|
|
||||||
if Decimal(str( float(open_order_px) - float(price) )).quantize(Decimal(str(min_price)), rounding=ROUND_HALF_UP) == 0.00:
|
|
||||||
if Config.Logging.Print_Summary_Each_Loop:
|
|
||||||
print('EXTEND OPEN ORDER NO PX CHG; SKIPPING')
|
|
||||||
skip = True
|
|
||||||
else:
|
|
||||||
open_order_id = None
|
|
||||||
|
|
||||||
if EXTEND_TGT_TAIL_BASE_QTY == 0.00:
|
|
||||||
logging.info('EXTEND TRYNG TO ORDER 0.00 BASE QTY, SKIPPING')
|
|
||||||
skip = True
|
|
||||||
|
|
||||||
if not skip:
|
|
||||||
min_price = Extend.min_price
|
|
||||||
min_price = int(min_price) if min_price == int(min_price) else min_price
|
|
||||||
price: Decimal = Decimal(str(price)).quantize(Decimal(str(min_price)), rounding=ROUND_HALF_UP)
|
|
||||||
|
|
||||||
if price == Decimal(str(0.00)).quantize(Decimal(str(min_price)), rounding=ROUND_HALF_UP):
|
|
||||||
logging.info('EXTEND TRYNG TO ORDER with A PRICE OF 0.00, SKIPPING')
|
|
||||||
continue
|
|
||||||
|
|
||||||
if ( qty < Extend.min_order_size ) or ( (qty*price) < Extend.min_notional ):
|
|
||||||
reduceOnly = True
|
|
||||||
else:
|
|
||||||
reduceOnly = False
|
|
||||||
|
|
||||||
await post_extend_order(
|
|
||||||
symbol=Extend.symbol,
|
|
||||||
side=side,
|
|
||||||
qty=qty,
|
|
||||||
price=price,
|
|
||||||
reduceOnly=reduceOnly,
|
|
||||||
postOnly=True,
|
|
||||||
cxl_prev_order_id=open_order_id
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
pass
|
|
||||||
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()
|
|
||||||
|
|
||||||
### Continue immediately or sleep ###
|
|
||||||
if Aster_Open_Orders or Extend_Open_Orders:
|
|
||||||
if Config.Logging.Print_Summary_Each_Loop:
|
|
||||||
print(f'_____ Open Orders _____ (Algo Engine ms: {(time.time() - loop_start)*1000:.2f}); Continuing...')
|
|
||||||
continue
|
|
||||||
else:
|
|
||||||
if Config.Logging.Print_Summary_Each_Loop:
|
|
||||||
print(f'_____ End No Open Orders _____ (Algo Engine ms: {(time.time() - loop_start)*1000:.2f}); Sleeping for sec: {Config.Config.Loop_Sleep_Sec:.0f}')
|
|
||||||
time.sleep(Config.Config.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
|
|
||||||
global Config
|
|
||||||
global Aster
|
|
||||||
global Extend
|
|
||||||
global Open_Symbols
|
|
||||||
|
|
||||||
|
|
||||||
_, 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')
|
|
||||||
|
|
||||||
await set_comb_open_symbols()
|
|
||||||
|
|
||||||
best_symbol_by_exchange: dict = json.loads(s=VAL_KEY.get(name='fr_engine_best_fund_rate_output')) # ty:ignore[invalid-argument-type]
|
|
||||||
if Open_Symbols:
|
|
||||||
logging.info(f'OPEN SYMBOLS: {Open_Symbols}')
|
|
||||||
master_data = json.loads(s=VAL_KEY.get(name='fr_engine_best_fund_rate_master')) # ty:ignore[invalid-argument-type]
|
|
||||||
open_symbol_to_work = Open_Symbols[0]
|
|
||||||
current_pos_master_ast = [d for d in master_data if d.get('symbol_ext') == open_symbol_to_work][0]
|
|
||||||
Aster = structs.Perpetual_Exchange(
|
|
||||||
mult = int(current_pos_master_ast['max_leverage_ast']),
|
|
||||||
lh_asset = current_pos_master_ast['lh_asset_ast'],
|
|
||||||
rh_asset = current_pos_master_ast['rh_asset_ast'],
|
|
||||||
symbol_asset_separator = '',
|
|
||||||
initial_funding_rate=float(current_pos_master_ast['funding_rate_ast']),
|
|
||||||
min_price=float(current_pos_master_ast['min_price_ast']),
|
|
||||||
min_order_size=float(current_pos_master_ast['min_order_size_ast']),
|
|
||||||
min_lot_size=float(current_pos_master_ast['min_lot_size_ast']),
|
|
||||||
min_notional=float(current_pos_master_ast['min_notional_ast']),
|
|
||||||
buy_ratio=float(current_pos_master_ast['buy_ratio_ast']),
|
|
||||||
)
|
|
||||||
Extend = structs.Perpetual_Exchange(
|
|
||||||
mult = int(current_pos_master_ast['max_leverage_ext']),
|
|
||||||
lh_asset = current_pos_master_ast['lh_asset_ext'],
|
|
||||||
rh_asset = current_pos_master_ast['rh_asset_ext'],
|
|
||||||
symbol_asset_separator = '-',
|
|
||||||
initial_funding_rate=float(current_pos_master_ast['funding_rate_ext']),
|
|
||||||
min_price=float(current_pos_master_ast['min_price_ext']),
|
|
||||||
min_order_size=float(current_pos_master_ast['min_order_size_ext']),
|
|
||||||
min_lot_size=float(current_pos_master_ast['min_lot_size_ext']),
|
|
||||||
min_notional=float(current_pos_master_ast['min_notional_ext']),
|
|
||||||
buy_ratio=float(current_pos_master_ast['buy_ratio_ext']),
|
|
||||||
)
|
|
||||||
Open_Symbols.pop(0)
|
|
||||||
else:
|
|
||||||
Aster = structs.Perpetual_Exchange(**best_symbol_by_exchange['ASTER'])
|
|
||||||
Extend = structs.Perpetual_Exchange(**best_symbol_by_exchange['EXTEND'])
|
|
||||||
|
|
||||||
# await get_aster_exch_info(symbol_override=Open_Symbols[0])
|
|
||||||
# await get_extend_exch_info(symbol_override=Open_Symbols[0])
|
|
||||||
|
|
||||||
with open('algo_config.json', mode='r', encoding='utf-8') as file:
|
|
||||||
Config = json.load(file)
|
|
||||||
Config = structs.Algo_Config(**Config)
|
|
||||||
|
|
||||||
Config.Config.Max_Target_Notional = float(min([Aster.mult, Extend.mult]) * Config.Config.Target_Open_Cash_Position)
|
|
||||||
# logging.info(f'Initial Algo Config: {ALGO_CONFIG}')
|
|
||||||
|
|
||||||
VAL_KEY.set(name='fr_orchestrator_output', value=json.dumps(obj=Config.model_dump()))
|
|
||||||
VAL_KEY.set(name='fr_algo_working_symbol', value=json.dumps(obj={'ASTER': asdict(obj=Aster), 'EXTEND': asdict(obj=Extend)}))
|
|
||||||
|
|
||||||
async with engine.connect() as CON:
|
|
||||||
### ASTER SETUP ###
|
|
||||||
# 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())
|
|
||||||
1017
main_v1.1.py
Normal file
1017
main_v1.1.py
Normal file
File diff suppressed because it is too large
Load Diff
@@ -261,6 +261,8 @@ class Perpetual_Exchange:
|
|||||||
notional_obj: dict = field(default_factory=dict)
|
notional_obj: dict = field(default_factory=dict)
|
||||||
notional_position: float = 0
|
notional_position: float = 0
|
||||||
unrealized_pnl: float = 0
|
unrealized_pnl: float = 0
|
||||||
|
buy_ratio_std: float = 0
|
||||||
|
just_rejected_count: int = 0
|
||||||
|
|
||||||
# async def update(self):
|
# async def update(self):
|
||||||
# await self.Collateral_Updates.update()
|
# await self.Collateral_Updates.update()
|
||||||
|
|||||||
93
ng.py
93
ng.py
@@ -1,13 +1,91 @@
|
|||||||
import os
|
import os
|
||||||
from nicegui import ui, app
|
from nicegui import ui, app
|
||||||
from sqlalchemy import create_engine
|
from sqlalchemy import create_engine
|
||||||
|
# import requests
|
||||||
import json
|
import json
|
||||||
|
# import time
|
||||||
|
# import re
|
||||||
import valkey
|
import valkey
|
||||||
|
# import asyncio
|
||||||
|
# import datetime as dt
|
||||||
|
# from random import random
|
||||||
|
# from nicegui_modules import data
|
||||||
|
# from nicegui_modules import ui_components
|
||||||
|
# from glide import GlideClient, NodeAddress, GlideClientConfiguration
|
||||||
|
|
||||||
|
|
||||||
|
LISTENING_CLIENT = None
|
||||||
|
LH_PAIR = 'BTC'
|
||||||
|
RH_PAIR = 'USD'
|
||||||
|
|
||||||
|
DEFAULT_TO_DARKMODE: bool = True
|
||||||
|
ALLOW_BODY_SCROLL: bool = True
|
||||||
|
LOOKBACK: int = 60
|
||||||
|
LOOKBACK_RT_TV_MAX_POINTS: int = 3000
|
||||||
|
REFRESH_INTERVAL_SEC: float = 10
|
||||||
|
REFRESH_INTERVAL_RT_SEC: float = 1/30
|
||||||
|
|
||||||
|
ENGINE = create_engine('mysql+pymysql://root:pwd@localhost/fund_rate')
|
||||||
VALKEY_R = valkey.Valkey(host='localhost', port=6379, db=0, decode_responses=True)
|
VALKEY_R = valkey.Valkey(host='localhost', port=6379, db=0, decode_responses=True)
|
||||||
|
|
||||||
|
### Utils ###
|
||||||
|
def update_body_scroll(e=None, bool_override=False):
|
||||||
|
if e is None:
|
||||||
|
if bool_override:
|
||||||
|
ui.query('body').style('height: 100%; overflow-y: auto;')
|
||||||
|
else:
|
||||||
|
ui.query('body').style('height: 100%; overflow-y: hidden;')
|
||||||
|
else:
|
||||||
|
if e.value:
|
||||||
|
ui.query('body').style('height: 100%; overflow-y: auto;')
|
||||||
|
else:
|
||||||
|
ui.query('body').style('height: 100%; overflow-y: hidden;')
|
||||||
|
|
||||||
|
### Callbacks ###
|
||||||
|
async def update_tv():
|
||||||
|
series_update = json.loads(VALKEY_R.get('poly_rtds_cl_btcusd')) # ty:ignore[invalid-argument-type]
|
||||||
|
series_update_b = json.loads(VALKEY_R.get('poly_binance_btcusd')) # ty:ignore[invalid-argument-type]
|
||||||
|
series_update_c = json.loads(VALKEY_R.get('poly_5min_btcusd')) # ty:ignore[invalid-argument-type]
|
||||||
|
timestamp = round( ( series_update['timestamp_arrival'] / 1000 ) , 2)
|
||||||
|
timestamp_b = round( ( series_update_b['timestamp_arrival'] / 1000 ) , 2)
|
||||||
|
timestamp_c = round( ( series_update_c['timestamp_arrival'] / 1000 ) , 2)
|
||||||
|
value = float(series_update['value'])
|
||||||
|
value_b = float(series_update_b['value'])
|
||||||
|
value_c = float(series_update_c['price'])
|
||||||
|
|
||||||
|
data_dict = {
|
||||||
|
'timestamp': timestamp,
|
||||||
|
'timestamp_b': timestamp_b,
|
||||||
|
'timestamp_c': timestamp_c,
|
||||||
|
'value': value,
|
||||||
|
'value_b': value_b,
|
||||||
|
'value_c': value_c,
|
||||||
|
'target': series_update_c['target_price'],
|
||||||
|
'LOOKBACK_RT_TV_MAX_POINTS': LOOKBACK_RT_TV_MAX_POINTS,
|
||||||
|
}
|
||||||
|
|
||||||
|
ui.run_javascript(f'await update_tv(data_dict={data_dict});')
|
||||||
|
|
||||||
|
|
||||||
|
### Pages ###
|
||||||
|
async def rt_chart_page():
|
||||||
|
global LOOKBACK
|
||||||
|
|
||||||
|
LOOKBACK = app.storage.user.get('lookback', LOOKBACK)
|
||||||
|
timer = ui.timer(REFRESH_INTERVAL_RT_SEC, update_tv)
|
||||||
|
|
||||||
|
with ui.row():
|
||||||
|
with ui.column():
|
||||||
|
ui.switch('☸︎', value=ALLOW_BODY_SCROLL, on_change=lambda e: update_body_scroll(e))
|
||||||
|
with ui.column():
|
||||||
|
ui.switch('▶️', value=True).bind_value_to(timer, 'active')
|
||||||
|
with ui.column().style('position: absolute; right: 20px; font-family: monospace; align-self: center;'):
|
||||||
|
ui.label('Atwater Trading: Orderbook')
|
||||||
|
|
||||||
|
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();')
|
||||||
|
|
||||||
def root():
|
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'))
|
app.add_static_files(max_cache_age=0, url_path='/static', local_directory=os.path.join(os.path.dirname(__file__), 'nicegui_modules/static'))
|
||||||
@@ -18,19 +96,12 @@ def root():
|
|||||||
<script src="/static/script.js"></script>
|
<script src="/static/script.js"></script>
|
||||||
'''
|
'''
|
||||||
)
|
)
|
||||||
|
|
||||||
# ui.add_head_html('<meta name="darkreader-lock">')
|
# ui.add_head_html('<meta name="darkreader-lock">')
|
||||||
# update_body_scroll(bool_override=ALLOW_BODY_SCROLL)
|
update_body_scroll(bool_override=ALLOW_BODY_SCROLL)
|
||||||
|
|
||||||
ui.sub_pages({
|
ui.sub_pages({
|
||||||
'/': controls_grid,
|
'/': rt_chart_page,
|
||||||
}).classes('w-full')
|
}).classes('w-full')
|
||||||
|
|
||||||
|
ui.run(root, storage_secret="123ABC", reload=True, dark=True, title='Atwater Trading')
|
||||||
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')
|
|
||||||
223
ng_modules/static/script.js
Normal file
223
ng_modules/static/script.js
Normal file
@@ -0,0 +1,223 @@
|
|||||||
|
async function waitForVariable(variableName, timeout = 5000) {
|
||||||
|
const startTime = Date.now();
|
||||||
|
while (typeof window[variableName] === 'undefined') {
|
||||||
|
if (Date.now() - startTime > timeout) {
|
||||||
|
throw new Error(`Variable '${variableName}' not defined within ${timeout}ms`);
|
||||||
|
}
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 100));
|
||||||
|
}
|
||||||
|
console.log(`Variable '${variableName}' is now defined.`);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function update_tv(data_dict) {
|
||||||
|
|
||||||
|
window.data.push({ time: data_dict.timestamp, value: data_dict.value });
|
||||||
|
window.data_b.push({ time: data_dict.timestamp_b, value: data_dict.value_b });
|
||||||
|
window.data_c.push({ time: data_dict.timestamp_c, value: data_dict.value_c });
|
||||||
|
window.data_tgt.push({ time: data_dict.timestamp_c, value: data_dict.target });
|
||||||
|
window.lineSeries.update({ time: data_dict.timestamp, value: data_dict.value });
|
||||||
|
window.lineSeries_b.update({ time: data_dict.timestamp_b, value: data_dict.value_b });
|
||||||
|
window.lineSeries_c.update({ time: data_dict.timestamp_c, value: data_dict.value_c });
|
||||||
|
window.lineSeries_tgt.update({ time: data_dict.timestamp_c, value: data_dict.target });
|
||||||
|
|
||||||
|
// midPriceLine.applyOptions({
|
||||||
|
// price: data_dict.mid_px,
|
||||||
|
// color: '#c78228',
|
||||||
|
// lineWidth: 3,
|
||||||
|
// lineStyle: LightweightCharts.LineStyle.Dashed,
|
||||||
|
// axisLabelVisible: true,
|
||||||
|
// });
|
||||||
|
|
||||||
|
window.chart.timeScale().scrollToRealTime();
|
||||||
|
// const currentRange = window.chart.timeScale().getVisibleLogicalRange();
|
||||||
|
// window.chart.timeScale().fitContent();
|
||||||
|
// window.chart.timeScale().setVisibleLogicalRange(currentRange);
|
||||||
|
|
||||||
|
const MAX_DATA_POINTS = data_dict.LOOKBACK_RT_TV_MAX_POINTS;
|
||||||
|
if (window.lineSeries.data().length > MAX_DATA_POINTS) {
|
||||||
|
window.lineSeries.setData(lineSeries.data().slice(-MAX_DATA_POINTS));
|
||||||
|
}
|
||||||
|
if (window.lineSeries_b.data().length > MAX_DATA_POINTS) {
|
||||||
|
window.lineSeries_b.setData(lineSeries_b.data().slice(-MAX_DATA_POINTS));
|
||||||
|
}
|
||||||
|
if (window.lineSeries_c.data().length > MAX_DATA_POINTS) {
|
||||||
|
window.lineSeries_c.setData(lineSeries_c.data().slice(-MAX_DATA_POINTS));
|
||||||
|
}
|
||||||
|
if (window.lineSeries_tgt.data().length > MAX_DATA_POINTS) {
|
||||||
|
window.lineSeries_tgt.setData(lineSeries_tgt.data().slice(-MAX_DATA_POINTS));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
async function create_tv() {
|
||||||
|
window.chart = LightweightCharts.createChart(document.getElementById('tv'),
|
||||||
|
{
|
||||||
|
autoSize: true,
|
||||||
|
toolbox: true,
|
||||||
|
timeScale: {
|
||||||
|
timeVisible: true, // Shows HH:mm on x-axis
|
||||||
|
secondsVisible: true // Optional: show seconds
|
||||||
|
},
|
||||||
|
rightPriceScale: {
|
||||||
|
visible: true,
|
||||||
|
autoScale: true
|
||||||
|
},
|
||||||
|
leftPriceScale: {
|
||||||
|
visible: true
|
||||||
|
},
|
||||||
|
|
||||||
|
layout: {
|
||||||
|
background: { type: 'solid', color: '#222' },
|
||||||
|
textColor: '#DDD',
|
||||||
|
},
|
||||||
|
grid: {
|
||||||
|
vertLines: {
|
||||||
|
color: '#e1e1e1', // Set vertical line color
|
||||||
|
visible: true,
|
||||||
|
style: 2, // 0: Solid, 1: Dashed, 2: Dotted, 3: LargeDashed, 4: SparseDotted
|
||||||
|
},
|
||||||
|
horzLines: {
|
||||||
|
color: '#e1e1e1', // Set horizontal line color
|
||||||
|
visible: true,
|
||||||
|
style: 2,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
crosshair: { mode: LightweightCharts.CrosshairMode.Normal },
|
||||||
|
}
|
||||||
|
);
|
||||||
|
window.lineSeries = chart.addSeries(LightweightCharts.LineSeries, {
|
||||||
|
color: '#94fcdf',
|
||||||
|
priceScaleId: 'right'
|
||||||
|
// topColor: '#94fcdf',
|
||||||
|
// bottomColor: 'rgba(112, 171, 249, 0.28)',
|
||||||
|
// invertFilledArea: false
|
||||||
|
});
|
||||||
|
window.lineSeries_b = chart.addSeries(LightweightCharts.LineSeries, {
|
||||||
|
color: '#dd7525',
|
||||||
|
priceScaleId: 'right'
|
||||||
|
// topColor: '#94fcdf',
|
||||||
|
// bottomColor: 'rgba(112, 171, 249, 0.28)',
|
||||||
|
// invertFilledArea: false
|
||||||
|
});
|
||||||
|
window.lineSeries_c = chart.addSeries(LightweightCharts.LineSeries, {
|
||||||
|
color: '#ea0707',
|
||||||
|
priceScaleId: 'left',
|
||||||
|
autoscaleInfoProvider: () => ({
|
||||||
|
priceRange: {
|
||||||
|
minValue: 0.0,
|
||||||
|
maxValue: 1.0
|
||||||
|
}
|
||||||
|
})
|
||||||
|
// topColor: '#94fcdf',
|
||||||
|
// bottomColor: 'rgba(112, 171, 249, 0.28)',
|
||||||
|
// invertFilledArea: false
|
||||||
|
});
|
||||||
|
window.lineSeries_tgt = chart.addSeries(LightweightCharts.LineSeries, {
|
||||||
|
color: '#ffffff',
|
||||||
|
priceScaleId: 'right',
|
||||||
|
lineStyle: LightweightCharts.LineStyle.Dashed
|
||||||
|
// topColor: '#94fcdf',
|
||||||
|
// bottomColor: 'rgba(112, 171, 249, 0.28)',
|
||||||
|
// invertFilledArea: false
|
||||||
|
});
|
||||||
|
// window.midPriceLine_Config = {
|
||||||
|
// price: 0,
|
||||||
|
// color: '#c78228',
|
||||||
|
// lineWidth: 3,
|
||||||
|
// lineStyle: LightweightCharts.LineStyle.Dashed,
|
||||||
|
// axisLabelVisible: false,
|
||||||
|
// };
|
||||||
|
// window.midPriceLine = window.lineSeries.createPriceLine(midPriceLine_Config);
|
||||||
|
window.data = [];
|
||||||
|
window.data_b = [];
|
||||||
|
window.data_c = [];
|
||||||
|
window.data_tgt = [];
|
||||||
|
window.lineSeries.setData(window.data);
|
||||||
|
window.lineSeries_b.setData(window.data_b);
|
||||||
|
window.lineSeries_c.setData(window.data_c);
|
||||||
|
window.lineSeries_tgt.setData(window.data_tgt);
|
||||||
|
|
||||||
|
// Create and style the tooltip html element
|
||||||
|
const container = document.getElementById('tv');
|
||||||
|
|
||||||
|
window.toolTipWidth = 200;
|
||||||
|
|
||||||
|
const toolTip = document.createElement('div');
|
||||||
|
toolTip.style = `width: ${window.toolTipWidth}px; height: 100%; position: absolute; display: none; padding: 8px; box-sizing: border-box; font-size: 12px; text-align: left; z-index: 1000; top: 12px; left: 12px; pointer-events: none; border-radius: 4px 4px 0px 0px; border-bottom: none; box-shadow: 0 2px 5px 0 rgba(117, 134, 150, 0.45);font-family: -apple-system, BlinkMacSystemFont, 'Trebuchet MS', Roboto, Ubuntu, sans-serif; -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;`;
|
||||||
|
toolTip.style.background = `rgba(${'0, 0, 0'}, 0.25)`;
|
||||||
|
toolTip.style.color = 'white';
|
||||||
|
toolTip.style.borderColor = 'rgba( 239, 83, 80, 1)';
|
||||||
|
container.appendChild(toolTip);
|
||||||
|
|
||||||
|
// update tooltip
|
||||||
|
window.chart.subscribeCrosshairMove(async param => {
|
||||||
|
|
||||||
|
if (
|
||||||
|
param.point === undefined ||
|
||||||
|
!param.time ||
|
||||||
|
param.point.x < 0 ||
|
||||||
|
param.point.x > container.clientWidth ||
|
||||||
|
param.point.y < 0 ||
|
||||||
|
param.point.y > container.clientHeight
|
||||||
|
) {
|
||||||
|
toolTip.style.display = 'none';
|
||||||
|
} else {
|
||||||
|
|
||||||
|
// toolTip.style.height = '100%';
|
||||||
|
toolTip.style.alignContent = 'center';
|
||||||
|
|
||||||
|
const dateStr = new Date(param.time*1000).toISOString();
|
||||||
|
|
||||||
|
let data = await param.seriesData.get(window.lineSeries);
|
||||||
|
if (data === undefined) {
|
||||||
|
data = {}
|
||||||
|
data.value = 0
|
||||||
|
console.log('data is UNDEFINED, SETTING TO 0')
|
||||||
|
};
|
||||||
|
|
||||||
|
let data_b = await param.seriesData.get(window.lineSeries_b);
|
||||||
|
if (data_b === undefined) {
|
||||||
|
data_b = {}
|
||||||
|
data_b.value = 0
|
||||||
|
console.log('data is UNDEFINED, SETTING TO 0')
|
||||||
|
};
|
||||||
|
|
||||||
|
const value_px = data.value
|
||||||
|
const value_px_b = window.data_b.value
|
||||||
|
const value_px_c = window.data_c.value
|
||||||
|
const value_px_tgt = window.data_tgt.value
|
||||||
|
|
||||||
|
toolTip.style.display = 'block';
|
||||||
|
// <div style="color: ${'rgba( 239, 83, 80, 1)'}">
|
||||||
|
// Atwater Trading
|
||||||
|
// </div>
|
||||||
|
toolTip.innerHTML = `
|
||||||
|
<div style="font-size: 24px; margin: 4px 0px; color: ${'white'}">
|
||||||
|
Chainlink: ${Math.round(100 * value_px) / 100}
|
||||||
|
Binance: ${Math.round(100 * value_px_b) / 100}
|
||||||
|
</div>
|
||||||
|
<div style="color: ${'white'}">
|
||||||
|
${dateStr}
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
let left = param.point.x; // relative to timeScale
|
||||||
|
const timeScaleWidth = chart.timeScale().width();
|
||||||
|
const priceScaleWidth = chart.priceScale('left').width();
|
||||||
|
const halfTooltipWidth = toolTipWidth / 2;
|
||||||
|
left += priceScaleWidth - halfTooltipWidth;
|
||||||
|
left = Math.min(left, priceScaleWidth + timeScaleWidth - toolTipWidth);
|
||||||
|
left = Math.max(left, priceScaleWidth);
|
||||||
|
|
||||||
|
toolTip.style.left = left + 'px';
|
||||||
|
toolTip.style.top = 0 + 'px';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
window.chart.timeScale().fitContent();
|
||||||
|
|
||||||
|
console.log("TV Created!")
|
||||||
|
};
|
||||||
33
ng_modules/static/styles.css
Normal file
33
ng_modules/static/styles.css
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
/* Sticky Quasar Table for Dark Mode */
|
||||||
|
.table-sticky-dark .q-table__top,
|
||||||
|
.table-sticky-dark .q-table__bottom,
|
||||||
|
.table-sticky-dark thead tr:first-child th {
|
||||||
|
background-color: black;
|
||||||
|
}
|
||||||
|
.table-sticky-dark thead tr th {
|
||||||
|
position: sticky;
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
.table-sticky-dark thead tr:first-child th {
|
||||||
|
top: 0;
|
||||||
|
}
|
||||||
|
.table-sticky-dark tbody {
|
||||||
|
scroll-margin-top: 48px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Sticky Quasar Table for Light Mode */
|
||||||
|
/* .table-sticky-light .q-table__top,
|
||||||
|
.table-sticky-light .q-table__bottom,
|
||||||
|
.table-sticky-light thead tr:first-child th {
|
||||||
|
background-color: rgb(229, 223, 223);
|
||||||
|
}
|
||||||
|
.table-sticky-light thead tr th {
|
||||||
|
position: sticky;
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
.table-sticky-light thead tr:first-child th {
|
||||||
|
top: 0;
|
||||||
|
}
|
||||||
|
.table-sticky-light tbody {
|
||||||
|
scroll-margin-top: 48px;
|
||||||
|
} */
|
||||||
Reference in New Issue
Block a user