918 lines
33 KiB
Python
918 lines
33 KiB
Python
import asyncio
|
|
import json
|
|
from dataclasses import dataclass
|
|
import logging
|
|
import math
|
|
import os
|
|
import time
|
|
from datetime import datetime, timezone
|
|
from typing import AsyncContextManager
|
|
import traceback
|
|
import numpy as np
|
|
import pandas as pd
|
|
import requests
|
|
import talib
|
|
import valkey
|
|
from dotenv import load_dotenv
|
|
from py_clob_client.clob_types import (
|
|
OrderArgs,
|
|
OrderType,
|
|
PartialCreateOrderOptions,
|
|
PostOrdersArgs,
|
|
BalanceAllowanceParams,
|
|
OpenOrderParams
|
|
)
|
|
from py_clob_client.order_builder.constants import BUY, SELL
|
|
from sqlalchemy import text
|
|
from sqlalchemy.ext.asyncio import create_async_engine
|
|
from functools import wraps
|
|
import modules.api as api
|
|
|
|
### Custom Order Args ###
|
|
@dataclass
|
|
class Custom_OrderArgs(OrderArgs):
|
|
max_price: float = 0.00
|
|
post_only: bool = False
|
|
|
|
|
|
### Database ###
|
|
CLIENT = None
|
|
CON: AsyncContextManager | None = None
|
|
VAL_KEY = None
|
|
|
|
### Logging ###
|
|
load_dotenv()
|
|
LOG_FILEPATH: str = os.getenv("LOGS_PATH") + '/Polymarket_5min_Algo.log'
|
|
|
|
### ALGO CONFIG / CONSTANTS ###
|
|
SLOPE_YES_THRESH = 0.01 # In Percent % Chg (e.g. 0.02 == 0.02%)
|
|
ENDTIME_BUFFER_SEC = 30 # Stop trading, cancel all open orders and exit positions this many seconds before mkt settles.
|
|
TGT_PX_INDEX_DIFF_THRESH = 0.05 # In Percent % Chg (e.g. 0.02 == 0.02%)
|
|
DEFAULT_ORDER_SIZE = 10 # In USDe
|
|
MIN_ORDER_SIZE = 5 # In USDe
|
|
TGT_PROFIT_CENTS = 0.04
|
|
CHASE_TO_BUY_CENTS = 0.05
|
|
MAX_ALLOWED_POLY_PX = 0.90
|
|
|
|
### GLOBALS ###
|
|
ORDER_LOCK = 0
|
|
|
|
SLUG_END_TIME = 0
|
|
|
|
FREE_CASH: float = 0
|
|
|
|
POLY_BINANCE = {}
|
|
POLY_REF = {}
|
|
POLY_CLOB = {}
|
|
POLY_CLOB_DOWN = {}
|
|
USER_TRADES = {}
|
|
USER_ORDERS = {}
|
|
SLOPE_HIST = []
|
|
|
|
LOCAL_ACTIVE_ORDERS = []
|
|
LOCAL_TOKEN_BALANCES = {}
|
|
# LOCAL_ACTIVE_POSITIONS = []
|
|
|
|
ACTIVE_BALANCES_EXIST = False ### REMOVE
|
|
|
|
|
|
|
|
### Decorators ###
|
|
def async_timeit(func):
|
|
@wraps(func)
|
|
async def wrapper(*args, **kwargs):
|
|
start_time = time.perf_counter()
|
|
try:
|
|
return await func(*args, **kwargs)
|
|
finally:
|
|
end_time = time.perf_counter()
|
|
total_time = (end_time - start_time)*1000
|
|
print(f"Function '{func.__name__}' executed in {total_time:.4f} ms")
|
|
|
|
return wrapper
|
|
|
|
### Database Funcs ###
|
|
# @async_timeit
|
|
async def create_executions_orders_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: executions_orders')
|
|
await CON.execute(text("""
|
|
CREATE TABLE IF NOT EXISTS executions_orders (
|
|
timestamp_sent BIGINT,
|
|
token_id VARCHAR(100),
|
|
limit_price DOUBLE,
|
|
size DOUBLE,
|
|
side VARCHAR(8),
|
|
order_type VARCHAR(8),
|
|
post_only BOOL,
|
|
resp_errorMsg VARCHAR(100),
|
|
resp_orderID VARCHAR(100),
|
|
resp_takingAmount DOUBLE,
|
|
resp_makingAmount DOUBLE,
|
|
resp_status VARCHAR(20),
|
|
resp_success BOOL
|
|
);
|
|
"""))
|
|
await CON.commit()
|
|
else:
|
|
raise ValueError('Only MySQL engine is implemented')
|
|
|
|
# @async_timeit
|
|
async def insert_executions_orders_table(
|
|
timestamp_sent: int,
|
|
token_id: str,
|
|
limit_price: float,
|
|
size: float,
|
|
side: str,
|
|
order_type: str,
|
|
post_only: bool,
|
|
resp_errorMsg: str,
|
|
resp_orderID: str,
|
|
resp_takingAmount: float,
|
|
resp_makingAmount: float,
|
|
resp_status: str,
|
|
resp_success: bool,
|
|
CON: AsyncContextManager,
|
|
engine: str = 'mysql', # mysql | duckdb
|
|
) -> None:
|
|
params={
|
|
'timestamp_sent': timestamp_sent,
|
|
'token_id': token_id,
|
|
'limit_price': limit_price,
|
|
'size': size,
|
|
'side': side,
|
|
'order_type': order_type,
|
|
'post_only': post_only,
|
|
'resp_errorMsg': resp_errorMsg,
|
|
'resp_orderID': resp_orderID,
|
|
'resp_takingAmount': resp_takingAmount,
|
|
'resp_makingAmount': resp_makingAmount,
|
|
'resp_status': resp_status,
|
|
'resp_success': resp_success,
|
|
}
|
|
if CON is None:
|
|
logging.info("NO DB CONNECTION, SKIPPING Insert Statements")
|
|
else:
|
|
if engine == 'mysql':
|
|
await CON.execute(text("""
|
|
INSERT INTO executions_orders
|
|
(
|
|
timestamp_sent,
|
|
token_id,
|
|
limit_price,
|
|
size,
|
|
side,
|
|
order_type,
|
|
post_only,
|
|
resp_errorMsg,
|
|
resp_orderID,
|
|
resp_takingAmount,
|
|
resp_makingAmount,
|
|
resp_status,
|
|
resp_success
|
|
)
|
|
VALUES
|
|
(
|
|
:timestamp_sent,
|
|
:token_id,
|
|
:limit_price,
|
|
:size,
|
|
:side,
|
|
:order_type,
|
|
:post_only,
|
|
:resp_errorMsg,
|
|
:resp_orderID,
|
|
:resp_takingAmount,
|
|
:resp_makingAmount,
|
|
:resp_status,
|
|
:resp_success
|
|
)
|
|
"""),
|
|
parameters=params
|
|
)
|
|
await CON.commit()
|
|
else:
|
|
raise ValueError('Only MySQL engine is implemented')
|
|
|
|
### Functions ###
|
|
# @async_timeit
|
|
async def slope_decision() -> list[bool, str]:
|
|
hist_trades = np.array(POLY_BINANCE.get('hist_trades', []))
|
|
|
|
if ( np.max(hist_trades[:, 0] )*1000 ) - ( np.min(hist_trades[:, 0])*1000 ) < 5:
|
|
logging.info('Max - Min Trade In History is < 5 Seconds Apart')
|
|
return False, ''
|
|
|
|
last_px = POLY_BINANCE['value']
|
|
last_px_ts = POLY_BINANCE['timestamp_value']
|
|
|
|
ts_min_1_sec = last_px_ts - 1000
|
|
price_min_1_sec_index = (np.abs(hist_trades[:, 0] - ts_min_1_sec)).argmin()
|
|
price_min_1_sec = hist_trades[:, 1][price_min_1_sec_index]
|
|
|
|
ts_min_5_sec = last_px_ts - 5000
|
|
price_min_5_sec_index = (np.abs(hist_trades[:, 0] - ts_min_5_sec)).argmin()
|
|
price_min_5_sec = hist_trades[:, 1][price_min_5_sec_index]
|
|
|
|
slope = (last_px - price_min_1_sec) / price_min_1_sec
|
|
slope_5 = (last_px - price_min_5_sec) / price_min_5_sec
|
|
SLOPE_HIST.append(slope)
|
|
|
|
# print(f'Avg Binance: {np.mean(hist_trades[:, 1])}')
|
|
# print(f'Len Hist : {len(hist_trades[:, 1])}')
|
|
# print(f'First Hist : {pd.to_datetime(np.min(hist_trades[:, 0]), unit='ms')}')
|
|
# print(f'Latest Hist: {pd.to_datetime(np.max(hist_trades[:, 0]), unit='ms')}')
|
|
# print(f'Slope Hist Avg: {np.mean(SLOPE_HIST):.4%}')
|
|
# print(f'Slope Hist Max: {np.max(SLOPE_HIST):.4%}')
|
|
# print(f'Slope Hist Std: {np.std(SLOPE_HIST):.4%}')
|
|
slope_1_buy = abs(slope) >= ( SLOPE_YES_THRESH / 100)
|
|
slope_5_buy = abs(slope_5) >= ( SLOPE_YES_THRESH / 100)
|
|
|
|
print(f'SLOPE_1: {slope:.4%} == {slope_1_buy}; SLOPE_5: {slope_5:.4%} == {slope_5_buy};')
|
|
|
|
### DECISION ###
|
|
if slope_1_buy and slope_5_buy:
|
|
print(f'🤑🤑🤑🤑🤑🤑🤑🤑🤑🤑 Slope: {slope_5:.4%};')
|
|
side = 'UP' if slope > 0.00 else 'DOWN'
|
|
return True, side
|
|
else:
|
|
return False, ''
|
|
|
|
# @async_timeit
|
|
async def cancel_all_orders(CLIENT):
|
|
logging.info('Attempting to Cancel All Orders')
|
|
cxl_resp = CLIENT.cancel_all()
|
|
if bool(cxl_resp.get('not_canceled', True)):
|
|
logging.warning(f'*** Cancel Request FAILED, trying again and shutting down: {cxl_resp}')
|
|
cxl_resp = CLIENT.cancel_all()
|
|
raise Exception('*** Cancel Request FAILED')
|
|
logging.info(f'Cancel Successful: {cxl_resp}')
|
|
|
|
# @async_timeit
|
|
async def cancel_single_order_by_id(CLIENT, order_id):
|
|
global LOCAL_ACTIVE_ORDERS
|
|
|
|
logging.info(f'Attempting to Cancel Single Order: {order_id}')
|
|
cxl_resp = CLIENT.cancel(order_id=order_id)
|
|
|
|
for idx, o in enumerate(LOCAL_ACTIVE_ORDERS):
|
|
if o.get('orderID') == order_id:
|
|
if bool(cxl_resp.get('not_canceled', True)):
|
|
if cxl_resp.get('not_canceled', {}).get(order_id, None) == "matched orders can't be canceled":
|
|
LOCAL_ACTIVE_ORDERS[idx]['status'] = 'MATCHED'
|
|
logging.info(f'Cancel request failed b/c already matched: {cxl_resp}')
|
|
return True
|
|
elif cxl_resp.get('not_canceled', {}).get(order_id, None) == "order can't be found - already canceled or matched":
|
|
logging.info(f'Cancel request failed b/c already matched or cancelled: {cxl_resp}')
|
|
# GET ORDER STATUS
|
|
order_status = CLIENT.get_orders(
|
|
OpenOrderParams(id=o['orderID'])
|
|
)[0]['status'].upper()
|
|
logging.info(f'Fetched status from CLOB: {order_status} for order: {o['orderID']}')
|
|
if order_status == 'MATCHED':
|
|
logging.info('Order is MATCHED')
|
|
return True
|
|
elif order_status == 'CANCELED':
|
|
logging.info('Order is CANCELED')
|
|
LOCAL_ACTIVE_ORDERS.pop(idx)
|
|
return False
|
|
else:
|
|
raise ValueError(f'ORDER CXL FAILED AND ORDER STILL SHOWS AS LIVE: {cxl_resp}; STATUS: {order_status}; ID: {o.get('orderID')}')
|
|
else:
|
|
logging.warning(f'*** Cancel Request FAILED, shutting down: {cxl_resp}')
|
|
raise Exception('*** Cancel Request FAILED - SHUTDONW')
|
|
else:
|
|
LOCAL_ACTIVE_ORDERS.pop(idx)
|
|
logging.info(f'Cancel Successful: {cxl_resp}')
|
|
return False
|
|
|
|
# @async_timeit
|
|
async def flatten_open_positions(CLIENT, token_id_up, token_id_down):
|
|
up = await get_balance_by_token_id(CLIENT=CLIENT, token_id=token_id_up)
|
|
down = await get_balance_by_token_id(CLIENT=CLIENT, token_id=token_id_down)
|
|
|
|
logging.info('*********FLATTENING*********')
|
|
logging.info(f'UP BALANCE = {up}')
|
|
logging.info(f'DOWN BALANCE = {down}')
|
|
|
|
### Submit orders to flatten outstanding balances ###
|
|
if abs(up) > MIN_ORDER_SIZE:
|
|
logging.info(f'Flattening Up Position: {up}')
|
|
await post_order(
|
|
CLIENT = CLIENT,
|
|
tick_size = POLY_CLOB['tick_size'],
|
|
neg_risk = POLY_CLOB['neg_risk'],
|
|
OrderArgs_list = [Custom_OrderArgs(
|
|
token_id=token_id_up,
|
|
price=float(POLY_CLOB['price'])-0.05,
|
|
size=up,
|
|
side=SELL,
|
|
)]
|
|
)
|
|
if abs(down) > MIN_ORDER_SIZE:
|
|
logging.info(f'Flattening Down Position: {down}')
|
|
await post_order(
|
|
CLIENT = CLIENT,
|
|
tick_size = POLY_CLOB['tick_size'],
|
|
neg_risk = POLY_CLOB['neg_risk'],
|
|
OrderArgs_list = [Custom_OrderArgs(
|
|
token_id=token_id_down,
|
|
price=float(POLY_CLOB_DOWN['price'])-0.05,
|
|
size=down,
|
|
side=SELL,
|
|
|
|
)]
|
|
)
|
|
|
|
# @async_timeit
|
|
async def get_balance_by_token_id(CLIENT, token_id):
|
|
collateral = CLIENT.get_balance_allowance(
|
|
BalanceAllowanceParams(
|
|
asset_type='CONDITIONAL',
|
|
token_id=token_id,
|
|
)
|
|
)
|
|
return int(collateral['balance']) / 1_000_000
|
|
|
|
# @async_timeit
|
|
async def get_usde_balance(CLIENT):
|
|
collateral = CLIENT.get_balance_allowance(
|
|
BalanceAllowanceParams(
|
|
asset_type='COLLATERAL'
|
|
)
|
|
)
|
|
return int(collateral['balance']) / 1_000_000
|
|
|
|
@async_timeit
|
|
async def check_for_open_positions(CLIENT, token_id_up, token_id_down):
|
|
global LOCAL_TOKEN_BALANCES
|
|
|
|
if token_id_up is None or token_id_down is None:
|
|
logging.critical('Token Id is None, Exiting')
|
|
raise ValueError('Token Id is None, Exiting')
|
|
# return False
|
|
up = await get_balance_by_token_id(CLIENT=CLIENT, token_id=token_id_up)
|
|
down = await get_balance_by_token_id(CLIENT=CLIENT, token_id=token_id_down)
|
|
|
|
LOCAL_TOKEN_BALANCES = {
|
|
token_id_up: up if up else 0,
|
|
token_id_down: down if down else 0,
|
|
}
|
|
|
|
logging.info(f'LOCAL_TOKEN_BALANCES: {LOCAL_TOKEN_BALANCES}')
|
|
|
|
if ( abs(up) > 0 ) or ( abs(down) > 0 ):
|
|
return True
|
|
else:
|
|
return False
|
|
|
|
@async_timeit
|
|
async def post_order(CLIENT, OrderArgs_list: list[Custom_OrderArgs], tick_size: float | str, neg_risk: bool) -> list[dict]: # Returns order response dict
|
|
global LOCAL_ACTIVE_ORDERS
|
|
global LOCAL_TOKEN_BALANCES
|
|
|
|
orders = []
|
|
for oa in OrderArgs_list:
|
|
orders.append(
|
|
PostOrdersArgs(
|
|
order=CLIENT.create_order(
|
|
order_args=oa,
|
|
options=PartialCreateOrderOptions(
|
|
tick_size=str(tick_size),
|
|
neg_risk=neg_risk
|
|
),
|
|
),
|
|
orderType=OrderType.GTC,
|
|
postOnly=oa.post_only,
|
|
),
|
|
)
|
|
|
|
### POST
|
|
response = CLIENT.post_orders(orders)
|
|
for idx, d in enumerate(response):
|
|
if d['errorMsg'] == '':
|
|
d['token_id'] = OrderArgs_list[idx].token_id
|
|
if d['token_id'] == POLY_CLOB['token_id_up']:
|
|
d['outcome'] = "UP"
|
|
elif d['token_id'] == POLY_CLOB['token_id_down']:
|
|
d['outcome'] = "DOWN"
|
|
else:
|
|
d['outcome'] = "UNKNOWN"
|
|
|
|
d['price'] = OrderArgs_list[idx].price
|
|
d['max_price'] = OrderArgs_list[idx].max_price
|
|
d['size'] = OrderArgs_list[idx].size
|
|
d['side'] = str(OrderArgs_list[idx].side).upper()
|
|
|
|
if d['status'].upper() =='MATCHED':
|
|
### Order Immediately Matched, Can Put in Offsetting Order Depending on State ###
|
|
print('******** ORDER APPEND TO LOCAL - MATCHED ********* ')
|
|
LOCAL_ACTIVE_ORDERS.append(d)
|
|
elif d['status'].upper() == 'CONFIRMED':
|
|
current_balance = float(LOCAL_TOKEN_BALANCES.get(d['token_id'], 0.00))
|
|
if d['side'] == 'BUY':
|
|
size = float(d['size'])
|
|
else:
|
|
size = float(d['size']) * -1
|
|
|
|
LOCAL_TOKEN_BALANCES[d['token_id']] = current_balance + size
|
|
print('******** TRADE FILLED, BAL UPDATED ********* ')
|
|
else:
|
|
print('******** ORDER APPEND TO LOCAL - LIVE ********* ')
|
|
LOCAL_ACTIVE_ORDERS.append(d)
|
|
elif d['errorMsg'] == "invalid post-only order: order crosses book":
|
|
logging.info(f'invalid post-only order: order crosses book. posted: {OrderArgs_list[idx].price}')
|
|
else:
|
|
raise ValueError(f'Order entry failed: {d}')
|
|
|
|
logging.info(f'Order Posted Resp: {response}')
|
|
print(f'Order Posted Resp: {response}')
|
|
return response
|
|
|
|
### Routes ###
|
|
async def no_orders(entry_or_exit: str = 'ENTRY'):
|
|
global ORDER_LOCK
|
|
|
|
### Check for Price Bands ###
|
|
up_px = float(POLY_CLOB.get('price', 0))
|
|
down_px = float(POLY_CLOB_DOWN.get('price', 0))
|
|
|
|
if (up_px > MAX_ALLOWED_POLY_PX) or (down_px > MAX_ALLOWED_POLY_PX):
|
|
logging.info(f'Outside max allowed px: {MAX_ALLOWED_POLY_PX}')
|
|
return False
|
|
|
|
### Check for Index vs. Target Px ###
|
|
tgt_px = float(POLY_CLOB.get('target_price', 0))
|
|
ref_px = float(POLY_REF.get('value'))
|
|
tgt_px_diff_to_index = ( abs( tgt_px - ref_px ) / tgt_px)
|
|
if tgt_px_diff_to_index > (TGT_PX_INDEX_DIFF_THRESH / 100):
|
|
logging.info(f'Tgt Diff to Index Outside Limit ({TGT_PX_INDEX_DIFF_THRESH}%); Diff {tgt_px_diff_to_index:.4%}; Index: {ref_px:.2f}; Tgt: {tgt_px:.2f}')
|
|
return False
|
|
|
|
### Check Slope ###
|
|
slope_bool, slope_side = await slope_decision()
|
|
if not slope_bool:
|
|
logging.info('Failed Slope Check')
|
|
return False
|
|
|
|
token_id_up = POLY_CLOB.get('token_id_up', None)
|
|
token_id_down = POLY_CLOB.get('token_id_down', None)
|
|
|
|
### Order Entry ###
|
|
if slope_side == 'UP':
|
|
if entry_or_exit == 'ENTRY':
|
|
side = BUY
|
|
size = DEFAULT_ORDER_SIZE
|
|
up_px = up_px + 0.01
|
|
down_px = down_px - TGT_PROFIT_CENTS
|
|
up_post_only = False
|
|
down_post_only = False # T
|
|
else: # entry_or_exit == 'EXIT'
|
|
side = SELL
|
|
size = await get_balance_by_token_id(CLIENT=CLIENT, token_id=token_id_up)
|
|
up_px = up_px + TGT_PROFIT_CENTS
|
|
down_px = down_px - 0.01
|
|
up_post_only = False # T
|
|
down_post_only = False
|
|
else: # slope_side == 'DOWN'
|
|
if entry_or_exit == 'ENTRY':
|
|
side = BUY
|
|
size = DEFAULT_ORDER_SIZE
|
|
up_px = up_px - TGT_PROFIT_CENTS
|
|
down_px = down_px + 0.01
|
|
up_post_only = False # T
|
|
down_post_only = False
|
|
else:
|
|
side = SELL
|
|
size = await get_balance_by_token_id(CLIENT=CLIENT, token_id=token_id_up)
|
|
up_px = up_px - 0.01
|
|
down_px = down_px + TGT_PROFIT_CENTS
|
|
up_post_only = False
|
|
down_post_only = False # T
|
|
|
|
buy_up_leg = Custom_OrderArgs(
|
|
token_id=token_id_up,
|
|
price=up_px,
|
|
size=size,
|
|
side=side,
|
|
max_price = 0.99,
|
|
post_only=up_post_only
|
|
)
|
|
buy_down_leg = Custom_OrderArgs(
|
|
token_id=token_id_down,
|
|
price=down_px,
|
|
size=size,
|
|
side=side,
|
|
max_price = 0.99,
|
|
post_only=down_post_only
|
|
)
|
|
order_list = [buy_up_leg, buy_down_leg]
|
|
|
|
### ADD CHECK FOR MKT MOVED AWAY FROM OPPORTNITY ###
|
|
|
|
if ORDER_LOCK:
|
|
logging.info(f'BUY ORDER BLOCKED BY LOCK: {order_list}')
|
|
|
|
else:
|
|
logging.info(f'Attempting BUY Order {order_list}')
|
|
await post_order(
|
|
CLIENT = CLIENT,
|
|
tick_size = POLY_CLOB['tick_size'],
|
|
neg_risk = POLY_CLOB['neg_risk'],
|
|
OrderArgs_list = order_list
|
|
)
|
|
# ORDER_LOCK = ORDER_LOCK + 1
|
|
|
|
async def active_orders_no_positions_route():
|
|
global LOCAL_ACTIVE_ORDERS
|
|
|
|
if len(LOCAL_ACTIVE_ORDERS) > 2:
|
|
logging.critical('More than two active orders, shutting down')
|
|
await kill_algo()
|
|
b_c = 0
|
|
s_c = 0
|
|
|
|
active_buy_up = False
|
|
active_buy_down = False
|
|
|
|
active_sell_up = False
|
|
active_sell_down = False
|
|
|
|
for o in LOCAL_ACTIVE_ORDERS:
|
|
if o['side'] == 'BUY':
|
|
if o['token_id']==POLY_CLOB['token_id_up']:
|
|
active_buy_up = True
|
|
else:
|
|
active_buy_down = True
|
|
|
|
b_c = b_c + 1
|
|
elif o['side'] == 'SELL':
|
|
if o['token_id']==POLY_CLOB['token_id_up']:
|
|
active_sell_up = True
|
|
else:
|
|
active_sell_down = True
|
|
|
|
s_c = s_c + 1
|
|
|
|
if (b_c > 2) or (s_c > 2):
|
|
logging.critical(f'More than two active buys or more than two active sells: b_c {b_c}; s_c{s_c}')
|
|
await kill_algo()
|
|
|
|
for o in LOCAL_ACTIVE_ORDERS:
|
|
logging.info(f'Working on order ({o['side']}): {o['orderID']}')
|
|
|
|
if o.get('status').upper() == 'MATCHED':
|
|
# logging.info('Order is matched, awaiting confirm or kickback')
|
|
if active_buy_up and active_buy_down:
|
|
logging.info('BUY UP AND BUY DOWN ACTIVE/MATCHED - WAITING FOR CONFIRMS')
|
|
continue
|
|
logging.info('Order is matched, ordering inverse side')
|
|
order_matched=True
|
|
elif o.get('status').upper() == 'FAILED':
|
|
order_matched=True
|
|
raise ValueError(f'Trade FAILED after matching: {o}')
|
|
elif o.get('status').upper() == 'RETRYING':
|
|
order_matched=True
|
|
raise ValueError(f'Trade RETRYING after matching: {o}')
|
|
else:
|
|
order_matched = False
|
|
|
|
orig_px = float(o['price'])
|
|
orig_size = float(o['size'])
|
|
|
|
### BUY
|
|
if o['side'] == 'BUY':
|
|
if POLY_CLOB['token_id_up'] == o['token_id']:
|
|
clob_px = float(POLY_CLOB['price'])
|
|
token_id_inverse = POLY_CLOB['token_id_down']
|
|
else:
|
|
clob_px = float(POLY_CLOB_DOWN['price'])
|
|
token_id_inverse = POLY_CLOB['token_id_up']
|
|
|
|
if (clob_px >= orig_px) or order_matched:
|
|
if (clob_px >= orig_px):
|
|
logging.info(f"Market px: ({clob_px} is above buy order px: {orig_px:.2f})")
|
|
|
|
if (o.get('max_price', 0) > clob_px) or order_matched:
|
|
if (o.get('max_price', 0) > clob_px):
|
|
logging.info(f"Market px: ({clob_px} has moved too far away from original target, cancelling and resetting algo: {o.get('max_price', 0) :.2f})")
|
|
|
|
if not order_matched:
|
|
order_matched = await cancel_single_order_by_id(CLIENT=CLIENT, order_id=o['orderID'])
|
|
|
|
if order_matched:
|
|
o['status'] = 'MATCHED'
|
|
if order_matched and (not (active_buy_up and active_buy_down)):
|
|
logging.info('BUY Order Matched Immediately, Ordering Inverse BUY')
|
|
token_id = token_id_inverse
|
|
px = 1 - (orig_px+TGT_PROFIT_CENTS)
|
|
if clob_px > px:
|
|
px = clob_px + 0.01
|
|
# max_price = px + CHASE_TO_BUY_CENTS
|
|
max_price = px
|
|
post_only = False
|
|
elif order_matched and (active_buy_up and active_buy_down):
|
|
logging.info('BUY UP AND BUY DOWN MATCHED - WAITING FOR CONFIRMS (IN LOOP)')
|
|
continue
|
|
else:
|
|
token_id = o['token_id']
|
|
px = clob_px+0.01
|
|
max_price = o['max_price']
|
|
post_only = False
|
|
|
|
await post_order(
|
|
CLIENT = CLIENT,
|
|
tick_size = POLY_CLOB['tick_size'],
|
|
neg_risk = POLY_CLOB['neg_risk'],
|
|
OrderArgs_list = [Custom_OrderArgs(
|
|
token_id=token_id,
|
|
price=px,
|
|
size=orig_size,
|
|
side=BUY,
|
|
max_price=max_price,
|
|
post_only=post_only
|
|
|
|
)]
|
|
)
|
|
else:
|
|
await cancel_single_order_by_id(CLIENT=CLIENT, order_id=o['orderID'])
|
|
### SELL
|
|
elif o['side'] == 'SELL':
|
|
if POLY_CLOB['token_id_up'] == o['token_id']:
|
|
clob_px = float(POLY_CLOB['price'])
|
|
else:
|
|
clob_px = float(POLY_CLOB_DOWN['price'])
|
|
|
|
if clob_px <= orig_px:
|
|
logging.info(f"Market px: ({clob_px} is below sell order px: {orig_px:.2f})")
|
|
|
|
order_filled = await cancel_single_order_by_id(CLIENT=CLIENT, order_id=o['orderID'])
|
|
if not order_filled:
|
|
await post_order(
|
|
CLIENT = CLIENT,
|
|
tick_size = POLY_CLOB['tick_size'],
|
|
neg_risk = POLY_CLOB['neg_risk'],
|
|
OrderArgs_list = [Custom_OrderArgs(
|
|
token_id=o['token_id'],
|
|
price=orig_px-0.01,
|
|
size=orig_size,
|
|
side=SELL,
|
|
max_price = 0.00
|
|
)]
|
|
)
|
|
|
|
|
|
async def no_orders_active_positions_route():
|
|
'''
|
|
Succesful Buy, now neeed to take profit and exit
|
|
'''
|
|
global LOCAL_TOKEN_BALANCES
|
|
|
|
OrderArgs_list = []
|
|
|
|
logging.warning(f'LOCAL_TOKEN_BALANCES: {LOCAL_TOKEN_BALANCES}')
|
|
|
|
for k, v in LOCAL_TOKEN_BALANCES.items():
|
|
size = await get_balance_by_token_id(CLIENT=CLIENT, token_id=k)
|
|
if size >= MIN_ORDER_SIZE:
|
|
if POLY_CLOB['token_id_up'] == k:
|
|
clob_px = float(POLY_CLOB['price'])
|
|
else:
|
|
clob_px = float(POLY_CLOB_DOWN['price'])
|
|
|
|
OrderArgs_list.append(
|
|
Custom_OrderArgs(
|
|
token_id=k,
|
|
price=clob_px + TGT_PROFIT_CENTS,
|
|
size=size,
|
|
side='SELL',
|
|
)
|
|
)
|
|
else:
|
|
LOCAL_TOKEN_BALANCES[k] = 0.00
|
|
logging.info(f'Wants to flatten small amount, skipping: {v}')
|
|
|
|
if OrderArgs_list:
|
|
logging.info(f'Posting orders to close: {OrderArgs_list}')
|
|
await post_order(
|
|
CLIENT = CLIENT,
|
|
tick_size = POLY_CLOB['tick_size'],
|
|
neg_risk = POLY_CLOB['neg_risk'],
|
|
OrderArgs_list = OrderArgs_list
|
|
)
|
|
|
|
async def active_orders_active_positions_route():
|
|
pass
|
|
|
|
async def kill_algo(msg: str = 'No kill msg provided'):
|
|
logging.info('Killing algo...')
|
|
await cancel_all_orders(CLIENT=CLIENT)
|
|
await flatten_open_positions(
|
|
CLIENT=CLIENT,
|
|
token_id_up = POLY_CLOB.get('token_id_up', None),
|
|
token_id_down = POLY_CLOB.get('token_id_down', None),
|
|
)
|
|
logging.info(f'...algo killed: {msg}')
|
|
raise Exception(f'Algo Killed: {msg}')
|
|
|
|
async def run_algo():
|
|
global POLY_BINANCE
|
|
global POLY_REF
|
|
global POLY_CLOB
|
|
global POLY_CLOB_DOWN
|
|
global USER_TRADES
|
|
global USER_ORDERS
|
|
|
|
global SLOPE_HIST
|
|
global ACTIVE_BALANCES_EXIST
|
|
|
|
global LOCAL_ACTIVE_ORDERS
|
|
global LOCAL_TOKEN_BALANCES
|
|
# global LOCAL_ACTIVE_POSITIONS
|
|
|
|
|
|
print(f'token_id_up: {POLY_CLOB.get('token_id_up', None)}')
|
|
print(f'token_id_down: {POLY_CLOB.get('token_id_down', None)}')
|
|
|
|
|
|
POLY_CLOB = json.loads(VAL_KEY.get('poly_5min_btcusd'))
|
|
|
|
### Check for missing target px (Poly 5min Target BTC Px Target) ###
|
|
if POLY_CLOB.get('target_price', 0) <= 1.00:
|
|
kill_algo('')
|
|
|
|
ACTIVE_BALANCES_EXIST = await check_for_open_positions(
|
|
CLIENT=CLIENT,
|
|
token_id_up=POLY_CLOB.get('token_id_up', None),
|
|
token_id_down=POLY_CLOB.get('token_id_down', None),
|
|
)
|
|
|
|
try:
|
|
while True:
|
|
loop_start = time.time()
|
|
print('__________Start___________')
|
|
POLY_BINANCE = json.loads(VAL_KEY.get('poly_binance_btcusd'))
|
|
POLY_REF = json.loads(VAL_KEY.get('poly_rtds_cl_btcusd'))
|
|
POLY_CLOB = json.loads(VAL_KEY.get('poly_5min_btcusd'))
|
|
POLY_CLOB_DOWN = json.loads(VAL_KEY.get('poly_5min_btcusd_down'))
|
|
USER_TRADES = json.loads(VAL_KEY.get('poly_user_trades'))
|
|
USER_ORDERS = VAL_KEY.get('poly_user_orders')
|
|
USER_ORDERS = json.loads(USER_ORDERS) if USER_ORDERS is not None else []
|
|
|
|
### CHANGE METHOD FROM BUY-SELL TO BUY UP - BUY DOWN
|
|
### DO THIS TO AVOID DELAY WITH FILL CONFIRMS
|
|
|
|
### Manage Local vs User Stream Orders ###
|
|
# print(f'LOCAL_ACTIVE_ORDERS: {LOCAL_ACTIVE_ORDERS}')
|
|
# print(f'USER_TRADES: {USER_TRADES}')
|
|
for idx, o in enumerate(LOCAL_ACTIVE_ORDERS):
|
|
user_order = next((item for item in USER_ORDERS if item["id"] == o['orderID']), None)
|
|
user_trade = next( ( item for item in USER_TRADES if ( o['orderID'] == item['taker_order_id'] ) or ( o["orderID"] == json.loads(item['maker_orders'])[0]['order_id'] ) ), None )
|
|
|
|
print(f'*****USER TRADE: {user_trade}')
|
|
|
|
if user_trade is not None:
|
|
trade_status = str(user_trade['status']).upper()
|
|
logging.info(f'Updated Trade Status: {o['status']} --> {trade_status}; {o['orderID']}')
|
|
if trade_status == 'CONFIRMED':
|
|
LOCAL_ACTIVE_ORDERS.pop(idx)
|
|
|
|
token_id = user_trade['asset_id']
|
|
current_balance = float(LOCAL_TOKEN_BALANCES.get(token_id, 0.00))
|
|
|
|
if user_trade['side'] == 'BUY':
|
|
size = float(user_trade['size'])
|
|
else:
|
|
size = float(user_trade['size']) * -1
|
|
|
|
LOCAL_TOKEN_BALANCES[token_id] = current_balance + size
|
|
|
|
# px = user_trade['price']
|
|
# LOCAL_ACTIVE_POSITIONS.append({
|
|
# 'token_id': token_id,
|
|
# 'order_id': o['orderID'],
|
|
# 'associate_trades': user_order['associate_trades'],
|
|
# 'size_matched': user_order['size_matched'],
|
|
# 'price': px,
|
|
# 'timestamp_value': user_order['timestamp'],
|
|
# })
|
|
logging.info('Order FILLED!')
|
|
elif trade_status == 'MATCHED':
|
|
logging.info(f'Order Matched...awaiting confirm: {trade_status}')
|
|
elif trade_status == 'MINED':
|
|
logging.info(f'Order Mined ...awaiting confirm: {trade_status}')
|
|
else:
|
|
logging.info(f'Trade status but not filled: trade= {user_trade}; order={o}')
|
|
|
|
elif user_order is not None:
|
|
order_status = str(user_order['status']).upper()
|
|
o['status'] = order_status
|
|
logging.info(f'Updated Order Status: {o['status']} --> {order_status}; {o['orderID']}')
|
|
|
|
if order_status == 'MATCHED':
|
|
logging.info('Order MATCHED, awaiting confirm')
|
|
|
|
elif order_status == 'CANCELED':
|
|
LOCAL_ACTIVE_ORDERS.pop(idx)
|
|
logging.info('Order Canceled')
|
|
else:
|
|
logging.info('Order Live')
|
|
|
|
token_id_up = POLY_CLOB.get('token_id_up', 0)
|
|
token_id_down = POLY_CLOB.get('token_id_down', 0)
|
|
|
|
if (token_id_up is None) or (token_id_down is None):
|
|
print('Missing Token Ids for Market, sleeping 1 sec and retrying...')
|
|
time.sleep(1)
|
|
ACTIVE_BALANCES_EXIST = {}
|
|
continue
|
|
else:
|
|
if (LOCAL_TOKEN_BALANCES.get(token_id_up) is None):
|
|
LOCAL_TOKEN_BALANCES[token_id_up] = 0.00
|
|
if (LOCAL_TOKEN_BALANCES.get(token_id_down) is None):
|
|
LOCAL_TOKEN_BALANCES[token_id_down] = 0.00
|
|
ACTIVE_BALANCES_EXIST = (LOCAL_TOKEN_BALANCES.get(token_id_up) > 0) or (LOCAL_TOKEN_BALANCES.get(token_id_down) > 0)
|
|
|
|
### Check for Endtime Buffer ###
|
|
if ENDTIME_BUFFER_SEC > POLY_CLOB.get('sec_remaining', 0):
|
|
if LOCAL_ACTIVE_ORDERS:
|
|
print('buffer zone - orders cancel')
|
|
await cancel_all_orders(CLIENT=CLIENT)
|
|
if ACTIVE_BALANCES_EXIST:
|
|
print('buffer zone - flatten positions')
|
|
await flatten_open_positions(
|
|
CLIENT=CLIENT,
|
|
token_id_up = POLY_CLOB.get('token_id_up', None),
|
|
token_id_down = POLY_CLOB.get('token_id_down', None),
|
|
)
|
|
|
|
print('buffer zone, sleeping until next session')
|
|
time.sleep(1)
|
|
continue
|
|
|
|
### Execution Route ###
|
|
if not(LOCAL_ACTIVE_ORDERS) and not(ACTIVE_BALANCES_EXIST): # No Orders, No Positions
|
|
print('ROUTE: no_orders_no_positions_route')
|
|
await no_orders(entry_or_exit='ENTRY')
|
|
|
|
### Open Orders Route ###
|
|
elif LOCAL_ACTIVE_ORDERS and not(ACTIVE_BALANCES_EXIST): # Orders, No Positions
|
|
print('ROUTE: active_orders_no_positions_route')
|
|
await active_orders_no_positions_route()
|
|
|
|
### Open Positions Route ###
|
|
elif not(LOCAL_ACTIVE_ORDERS) and ACTIVE_BALANCES_EXIST: # No Orders, Positions
|
|
print('ROUTE: no_orders_route_active_positions_route')
|
|
await no_orders(entry_or_exit='EXIT')
|
|
|
|
### Open Orders and Open Positions Route ###
|
|
else:
|
|
print('ROUTE: active_orders_active_positions_route - OFF')
|
|
# await active_orders_no_positions_route() # Orders and Positions
|
|
|
|
print(f'__________________________ (Algo Engine ms: {(time.time() - loop_start)*1000})')
|
|
time.sleep(1)
|
|
except KeyboardInterrupt:
|
|
print('...algo stopped')
|
|
await cancel_all_orders(CLIENT=CLIENT)
|
|
except Exception as e:
|
|
logging.critical(f'*** ALGO ENGINE CRASHED: {e}')
|
|
logging.error(traceback.format_exc())
|
|
await cancel_all_orders(CLIENT=CLIENT)
|
|
|
|
|
|
async def main():
|
|
global CLIENT
|
|
global VAL_KEY
|
|
global CON
|
|
|
|
CLIENT = api.create_client()
|
|
VAL_KEY = valkey.Valkey(host='localhost', port=6379, db=0, decode_responses=True)
|
|
engine = create_async_engine('mysql+asyncmy://root:pwd@localhost/polymarket')
|
|
|
|
async with engine.connect() as CON:
|
|
await create_executions_orders_table(CON=CON)
|
|
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())
|
|
|