From c05113086709ba58f20607360ae934ccfc33758f Mon Sep 17 00:00:00 2001 From: stevekeyharvey Date: Fri, 27 Mar 2026 17:57:12 +0000 Subject: [PATCH 1/5] ionos_last_commit --- database.ipynb | 77 ++++++++++++++++++++++++++++ ws_rtds.py | 134 +++++++++++++++++++++++++++++++++++++++++++------ 2 files changed, 195 insertions(+), 16 deletions(-) create mode 100644 database.ipynb diff --git a/database.ipynb b/database.ipynb new file mode 100644 index 0000000..e048b6a --- /dev/null +++ b/database.ipynb @@ -0,0 +1,77 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": 1, + "id": "4cae6bf1", + "metadata": {}, + "outputs": [], + "source": [ + "from sqlalchemy import create_engine, text\n", + "import pandas as pd" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "f5040527", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Connection failed: (pymysql.err.OperationalError) (2003, \"Can't connect to MySQL server on 'localhost' ([Errno 111] Connection refused)\")\n", + "(Background on this error at: https://sqlalche.me/e/20/e3q8)\n" + ] + } + ], + "source": [ + "### MYSQL ###\n", + "engine = create_engine('mysql+pymysql://root:pwd@localhost/mkt_maker')\n", + "try:\n", + " with engine.connect() as conn:\n", + " print(\"Connection successful\")\n", + "except Exception as e:\n", + " print(f\"Connection failed: {e}\") " + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "a866e9ca", + "metadata": {}, + "outputs": [], + "source": [] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "5ba7be5f", + "metadata": {}, + "outputs": [], + "source": [] + } + ], + "metadata": { + "kernelspec": { + "display_name": "py_313", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.13.5" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/ws_rtds.py b/ws_rtds.py index e860944..705138b 100644 --- a/ws_rtds.py +++ b/ws_rtds.py @@ -1,24 +1,98 @@ import asyncio import json -import math -import pandas as pd -import os -from datetime import datetime, timezone -import websockets +import logging +import socket +import traceback +from datetime import datetime +from typing import AsyncContextManager + import numpy as np -import talib -import requests +import pandas as pd +import requests.packages.urllib3.util.connection as urllib3_cn # type: ignore +from sqlalchemy import text +import websockets +from sqlalchemy.ext.asyncio import create_async_engine + +### Allow only ipv4 ### +def allowed_gai_family(): + return socket.AF_INET +urllib3_cn.allowed_gai_family = allowed_gai_family + +### Database ### +USE_DB: bool = True +CON: AsyncContextManager | None = None + +### Logging ### +LOG_FILEPATH: str = '/root/logs/Polymarket_RTDS.log' + +### Globals ### WSS_URL = "wss://ws-live-data.polymarket.com" +# HIST_TRADES = np.empty((0, 2)) -HIST_TRADES = np.empty((0, 2)) +### Database Funcs ### +async def create_rtds_btcusd_table( + CON: AsyncContextManager, + engine: str = 'mysql', # mysql | duckdb + ) -> None: + if CON is None: + logging.info("NO DB CONNECTION, SKIPPING Create Statements") + else: + if engine == 'mysql': + await CON.execute(text(""" + CREATE TABLE IF NOT EXISTS poly_rtds_btcusd_cl ( + timestamp_msg BIGINT, + timestamp_value BIGINT, + value DOUBLE, + ); + """)) + await CON.commit() + else: + raise ValueError('Only MySQL engine is implemented') + +async def insert_rtds_btcusd_table( + timestamp_msg: int, + timestamp_value: int, + value: int, + CON: AsyncContextManager, + engine: str = 'mysql', # mysql | duckdb + ) -> None: + params={ + 'timestamp_msg': timestamp_msg, + 'timestamp_value': timestamp_value, + 'value': value, + } + if CON is None: + logging.info("NO DB CONNECTION, SKIPPING Insert Statements") + else: + if engine == 'mysql': + await CON.execute(text(""" + INSERT INTO poly_rtds_btcusd_cl + ( + timestamp_msg, + timestamp_value, + value + ) + VALUES + ( + :timestamp_msg, + :timestamp_value, + :value + ) + """), + parameters=params + ) + await CON.commit() + else: + raise ValueError('Only MySQL engine is implemented') +### Websocket ### async def rtds_stream(): global HIST_TRADES async with websockets.connect(WSS_URL) as websocket: - print(f"Connected to {WSS_URL}") + logging.info(f"Connected to {WSS_URL}") subscribe_msg = { "action": "subscribe", @@ -41,21 +115,49 @@ async def rtds_stream(): if data['payload'].get('value', None) is not None: print(f'🤑 BTC Chainlink Last Px: {data['payload']['value']:_.4f}; TS: {pd.to_datetime(data['timestamp'], unit='ms')}') else: - print(f'Initial or unexpected data struct, skipping: {data}') + logging.info(f'Initial or unexpected data struct, skipping: {data}') continue except (json.JSONDecodeError, ValueError): - print(f'Message not in JSON format, skipping: {message}') + logging.warning(f'Message not in JSON format, skipping: {message}') continue else: raise ValueError(f'Type: {type(data)} not expected: {message}') - + except websockets.ConnectionClosed as e: + logging.error(f'Connection closed: {e}') + logging.error(traceback.format_exc()) + except Exception as e: + logging.error(f'Connection closed: {e}') + logging.error(traceback.format_exc()) - except websockets.ConnectionClosed: - print("Connection closed by server.") + +async def main(): + global CON + + if USE_DB: + engine = create_async_engine('mysql+asyncmy://root:pwd@localhost/mkt_maker') + async with engine.connect() as CON: + await rtds_stream() + else: + CON = None + logging.warning("DATABASE NOT BEING USED, NO DATA WILL BE RECORDED") + await rtds_stream() if __name__ == '__main__': + START_TIME = round(datetime.now().timestamp()*1000) + + logging.info(f'Log FilePath: {LOG_FILEPATH}') + + logging.basicConfig( + force=True, + filename=LOG_FILEPATH, + level=logging.INFO, + format='%(asctime)s - %(levelname)s - %(message)s', + filemode='w' + ) + logging.info(f"STARTED: {START_TIME}") + try: - asyncio.run(rtds_stream()) + asyncio.run(main()) except KeyboardInterrupt: - print("Stream stopped.") \ No newline at end of file + logging.info("Stream stopped") \ No newline at end of file -- 2.49.1 From 8d7d99d749aff2534130ee2191d160c793c23bb9 Mon Sep 17 00:00:00 2001 From: stevekeyharvey Date: Sun, 29 Mar 2026 16:27:58 +0000 Subject: [PATCH 2/5] transfer to aws --- Screenshot 2026-03-29 at 12.35.51 AM.png | Bin 0 -> 345416 bytes database.ipynb | 216 +++++++++++++++- modules/__pycache__/api.cpython-313.pyc | Bin 1768 -> 1730 bytes ng.py | 117 +++++++++ nicegui_modules/static/script.js | 220 ++++++++++++++++ nicegui_modules/static/styles.css | 33 +++ order_entry.ipynb | 51 ++-- ws.py | 150 ----------- ws_binance.py | 199 +++++++++++++++ ws_clob.py | 312 +++++++++++++++++++++++ ws_coinbase.py | 224 ++++++++++++++++ ws_pionex.py | 219 ++++++++++++++++ ws_rtds.py | 54 +++- 13 files changed, 1607 insertions(+), 188 deletions(-) create mode 100644 Screenshot 2026-03-29 at 12.35.51 AM.png create mode 100644 ng.py create mode 100644 nicegui_modules/static/script.js create mode 100644 nicegui_modules/static/styles.css delete mode 100644 ws.py create mode 100644 ws_binance.py create mode 100644 ws_clob.py create mode 100644 ws_coinbase.py create mode 100644 ws_pionex.py diff --git a/Screenshot 2026-03-29 at 12.35.51 AM.png b/Screenshot 2026-03-29 at 12.35.51 AM.png new file mode 100644 index 0000000000000000000000000000000000000000..aa978d4075c7ffe0540a76e34dac4871e830b6e3 GIT binary patch literal 345416 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 _OIPt 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

w*i__o?GX?1r53YtgZ)4 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 literal 0 HcmV?d00001 diff --git a/database.ipynb b/database.ipynb index e048b6a..c41f74c 100644 --- a/database.ipynb +++ b/database.ipynb @@ -2,18 +2,19 @@ "cells": [ { "cell_type": "code", - "execution_count": 1, + "execution_count": 2, "id": "4cae6bf1", "metadata": {}, "outputs": [], "source": [ "from sqlalchemy import create_engine, text\n", - "import pandas as pd" + "import pandas as pd\n", + "from datetime import datetime" ] }, { "cell_type": "code", - "execution_count": null, + "execution_count": 6, "id": "f5040527", "metadata": {}, "outputs": [ @@ -21,14 +22,13 @@ "name": "stdout", "output_type": "stream", "text": [ - "Connection failed: (pymysql.err.OperationalError) (2003, \"Can't connect to MySQL server on 'localhost' ([Errno 111] Connection refused)\")\n", - "(Background on this error at: https://sqlalche.me/e/20/e3q8)\n" + "Connection successful\n" ] } ], "source": [ "### MYSQL ###\n", - "engine = create_engine('mysql+pymysql://root:pwd@localhost/mkt_maker')\n", + "engine = create_engine('mysql+pymysql://root:pwd@localhost/polymarket')\n", "try:\n", " with engine.connect() as conn:\n", " print(\"Connection successful\")\n", @@ -38,10 +38,210 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 7, + "id": "5c23110d", + "metadata": {}, + "outputs": [], + "source": [ + "query = '''\n", + "SELECT timestamp_msg,timestamp_value,`value` FROM poly_rtds_cl_btcusd;\n", + "'''" + ] + }, + { + "cell_type": "code", + "execution_count": 8, "id": "a866e9ca", "metadata": {}, "outputs": [], + "source": [ + "df = pd.read_sql(query, con=engine)" + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "id": "954a3c3c", + "metadata": {}, + "outputs": [], + "source": [ + "df['ts'] = pd.to_datetime(df['timestamp_value'], unit='ms')" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "id": "f11fd680", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "1774752413939" + ] + }, + "execution_count": 6, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "round(datetime.now().timestamp()*1000)" + ] + }, + { + "cell_type": "code", + "execution_count": 11, + "id": "eadb0364", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "Timestamp('2026-03-29 03:22:03.145000')" + ] + }, + "execution_count": 11, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "pd.to_datetime(1774754523145, unit='ms')" + ] + }, + { + "cell_type": "code", + "execution_count": 14, + "id": "3fbf525c", + "metadata": {}, + "outputs": [], + "source": [ + "d = {'id': '1753563', 'question': 'Bitcoin Up or Down - March 29, 12:40AM-12:45AM ET', 'conditionId': '0x4cf6815a61939a9a0ee308772e727bbaf8e95803577aad7b9a9d3e028e37f13b', 'slug': 'btc-updown-5m-1774759200', 'resolutionSource': 'https://data.chain.link/streams/btc-usd', 'endDate': '2026-03-29T04:45:00Z', 'liquidity': '40412.2984', 'startDate': '2026-03-28T04:48:26.823506Z', 'image': 'https://polymarket-upload.s3.us-east-2.amazonaws.com/BTC+fullsize.png', 'icon': 'https://polymarket-upload.s3.us-east-2.amazonaws.com/BTC+fullsize.png', 'description': 'This market will resolve to \"Up\" if the Bitcoin price at the end of the time range specified in the title is greater than or equal to the price at the beginning of that range. Otherwise, it will resolve to \"Down\".\\nThe resolution source for this market is information from Chainlink, specifically the BTC/USD data stream available at https://data.chain.link/streams/btc-usd.\\nPlease note that this market is about the price according to Chainlink data stream BTC/USD, not according to other sources or spot markets.', 'outcomes': '[\"Up\", \"Down\"]', 'outcomePrices': '[\"0.505\", \"0.495\"]', 'volume': '1154.594883', 'active': True, 'closed': False, 'marketMakerAddress': '', 'createdAt': '2026-03-28T04:47:13.404208Z', 'updatedAt': '2026-03-29T04:38:45.64501Z', 'new': False, 'featured': False, 'archived': False, 'restricted': True, 'groupItemThreshold': '0', 'questionID': '0x7e86ce5e7ba0eb24758756db7c1443bf48041d08ac10364cf6771ef3f9c26733', 'enableOrderBook': True, 'orderPriceMinTickSize': 0.01, 'orderMinSize': 5, 'volumeNum': 1154.594883, 'liquidityNum': 40412.2984, 'endDateIso': '2026-03-29', 'startDateIso': '2026-03-28', 'hasReviewedDates': True, 'volume24hr': 1154.594883, 'volume1wk': 1154.594883, 'volume1mo': 1154.594883, 'volume1yr': 1154.594883, 'clobTokenIds': '[\"3132608671382208432230794800974499111421928258370863811882545679011841068490\", \"6273455409880805876304408793123549040642604317110995252288748628688958052125\"]', 'volume24hrClob': 1154.594883, 'volume1wkClob': 1154.594883, 'volume1moClob': 1154.594883, 'volume1yrClob': 1154.594883, 'volumeClob': 1154.594883, 'liquidityClob': 40412.2984, 'makerBaseFee': 1000, 'takerBaseFee': 1000, 'acceptingOrders': True, 'negRisk': False, 'ready': False, 'funded': False, 'acceptingOrdersTimestamp': '2026-03-28T04:47:21Z', 'cyom': False, 'competitive': 0.9999750006249843, 'pagerDutyNotificationEnabled': False, 'approved': True, 'rewardsMinSize': 50, 'rewardsMaxSpread': 4.5, 'spread': 0.01, 'lastTradePrice': 0.51, 'bestBid': 0.5, 'bestAsk': 0.51, 'automaticallyActive': True, 'clearBookOnStart': False, 'showGmpSeries': False, 'showGmpOutcome': False, 'manualActivation': False, 'negRiskOther': False, 'umaResolutionStatuses': '[]', 'pendingDeployment': False, 'deploying': False, 'rfqEnabled': False, 'eventStartTime': '2026-03-29T04:40:00Z', 'holdingRewardsEnabled': False, 'feesEnabled': True, 'requiresTranslation': False, 'makerRebatesFeeShareBps': 10000, 'feeType': 'crypto_fees', 'feeSchedule': {'exponent': 2, 'rate': 0.25, 'takerOnly': True, 'rebateRate': 0.2}}" + ] + }, + { + "cell_type": "code", + "execution_count": 15, + "id": "699031ee", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "{'id': '1753563',\n", + " 'question': 'Bitcoin Up or Down - March 29, 12:40AM-12:45AM ET',\n", + " 'conditionId': '0x4cf6815a61939a9a0ee308772e727bbaf8e95803577aad7b9a9d3e028e37f13b',\n", + " 'slug': 'btc-updown-5m-1774759200',\n", + " 'resolutionSource': 'https://data.chain.link/streams/btc-usd',\n", + " 'endDate': '2026-03-29T04:45:00Z',\n", + " 'liquidity': '40412.2984',\n", + " 'startDate': '2026-03-28T04:48:26.823506Z',\n", + " 'image': 'https://polymarket-upload.s3.us-east-2.amazonaws.com/BTC+fullsize.png',\n", + " 'icon': 'https://polymarket-upload.s3.us-east-2.amazonaws.com/BTC+fullsize.png',\n", + " 'description': 'This market will resolve to \"Up\" if the Bitcoin price at the end of the time range specified in the title is greater than or equal to the price at the beginning of that range. Otherwise, it will resolve to \"Down\".\\nThe resolution source for this market is information from Chainlink, specifically the BTC/USD data stream available at https://data.chain.link/streams/btc-usd.\\nPlease note that this market is about the price according to Chainlink data stream BTC/USD, not according to other sources or spot markets.',\n", + " 'outcomes': '[\"Up\", \"Down\"]',\n", + " 'outcomePrices': '[\"0.505\", \"0.495\"]',\n", + " 'volume': '1154.594883',\n", + " 'active': True,\n", + " 'closed': False,\n", + " 'marketMakerAddress': '',\n", + " 'createdAt': '2026-03-28T04:47:13.404208Z',\n", + " 'updatedAt': '2026-03-29T04:38:45.64501Z',\n", + " 'new': False,\n", + " 'featured': False,\n", + " 'archived': False,\n", + " 'restricted': True,\n", + " 'groupItemThreshold': '0',\n", + " 'questionID': '0x7e86ce5e7ba0eb24758756db7c1443bf48041d08ac10364cf6771ef3f9c26733',\n", + " 'enableOrderBook': True,\n", + " 'orderPriceMinTickSize': 0.01,\n", + " 'orderMinSize': 5,\n", + " 'volumeNum': 1154.594883,\n", + " 'liquidityNum': 40412.2984,\n", + " 'endDateIso': '2026-03-29',\n", + " 'startDateIso': '2026-03-28',\n", + " 'hasReviewedDates': True,\n", + " 'volume24hr': 1154.594883,\n", + " 'volume1wk': 1154.594883,\n", + " 'volume1mo': 1154.594883,\n", + " 'volume1yr': 1154.594883,\n", + " 'clobTokenIds': '[\"3132608671382208432230794800974499111421928258370863811882545679011841068490\", \"6273455409880805876304408793123549040642604317110995252288748628688958052125\"]',\n", + " 'volume24hrClob': 1154.594883,\n", + " 'volume1wkClob': 1154.594883,\n", + " 'volume1moClob': 1154.594883,\n", + " 'volume1yrClob': 1154.594883,\n", + " 'volumeClob': 1154.594883,\n", + " 'liquidityClob': 40412.2984,\n", + " 'makerBaseFee': 1000,\n", + " 'takerBaseFee': 1000,\n", + " 'acceptingOrders': True,\n", + " 'negRisk': False,\n", + " 'ready': False,\n", + " 'funded': False,\n", + " 'acceptingOrdersTimestamp': '2026-03-28T04:47:21Z',\n", + " 'cyom': False,\n", + " 'competitive': 0.9999750006249843,\n", + " 'pagerDutyNotificationEnabled': False,\n", + " 'approved': True,\n", + " 'rewardsMinSize': 50,\n", + " 'rewardsMaxSpread': 4.5,\n", + " 'spread': 0.01,\n", + " 'lastTradePrice': 0.51,\n", + " 'bestBid': 0.5,\n", + " 'bestAsk': 0.51,\n", + " 'automaticallyActive': True,\n", + " 'clearBookOnStart': False,\n", + " 'showGmpSeries': False,\n", + " 'showGmpOutcome': False,\n", + " 'manualActivation': False,\n", + " 'negRiskOther': False,\n", + " 'umaResolutionStatuses': '[]',\n", + " 'pendingDeployment': False,\n", + " 'deploying': False,\n", + " 'rfqEnabled': False,\n", + " 'eventStartTime': '2026-03-29T04:40:00Z',\n", + " 'holdingRewardsEnabled': False,\n", + " 'feesEnabled': True,\n", + " 'requiresTranslation': False,\n", + " 'makerRebatesFeeShareBps': 10000,\n", + " 'feeType': 'crypto_fees',\n", + " 'feeSchedule': {'exponent': 2,\n", + " 'rate': 0.25,\n", + " 'takerOnly': True,\n", + " 'rebateRate': 0.2}}" + ] + }, + "execution_count": 15, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "d" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "a620fa17", + "metadata": {}, + "outputs": [], + "source": [] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "2071f014", + "metadata": {}, + "outputs": [], + "source": [] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], "source": [] }, { @@ -69,7 +269,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.13.5" + "version": "3.13.12" } }, "nbformat": 4, diff --git a/modules/__pycache__/api.cpython-313.pyc b/modules/__pycache__/api.cpython-313.pyc index 41ce6d9095f9551805678f53264018c6fd825a29..ddb0ad9364f5e29d127c03da2f6ac9beeb18988b 100644 GIT binary patch delta 44 ycmaFCdx)3&GcPX}0}yOIc5EZJ2$PVueo=mYiGE6IS!zyxL2hbZ$>wmTi7Ws*M-H6; delta 82 zcmX@a`+}GIGcPX}0}yCcAKl0;!ekMsA6lGRRIFcIl3JFUom!cZSX7o;sqX^h<>VLS jrskFCJC>9umZTQNhZH5IWagzS1f>?_7jHIXn#ckGW``W0 diff --git a/ng.py b/ng.py new file mode 100644 index 0000000..a18693f --- /dev/null +++ b/ng.py @@ -0,0 +1,117 @@ +import os +from nicegui import ui, app +from sqlalchemy import create_engine +# import requests +import json +# import time +# import re +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 = 300 +REFRESH_INTERVAL_SEC: int = 10 +REFRESH_INTERVAL_RT_SEC: int = 0.1 + +ENGINE = create_engine('mysql+pymysql://root:pwd@localhost/polymarket') +VALKEY_R = valkey.Valkey(host='localhost', port=6379, db=0, decode_responses=True) +# VALKEY_P = VALKEY_R.pubsub() +# VALKEY_P.subscribe('mexc_mkt_bookTicker') + + +def root(): + app.add_static_files(max_cache_age=0, url_path='/static', local_directory=os.path.join(os.path.dirname(__file__), 'nicegui_modules/static')) + ui.add_head_html(''' + + + + + ''' + ) + + # ui.add_head_html('') + update_body_scroll(bool_override=ALLOW_BODY_SCROLL) + + ui.sub_pages({ + '/': rt_chart_page, + }).classes('w-full') + + +async def update_tv(): + series_update = json.loads(VALKEY_R.get('poly_rtds_cl_btcusd')) + series_update_b = json.loads(VALKEY_R.get('poly_coinbase_btcusd')) + series_update_c = json.loads(VALKEY_R.get('poly_5min_btcusd')) + 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});') + + +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;') + +# async def refresh_lookback_funcs(lookback: int = LOOKBACK): +# lookback = app.storage.user.get('lookback', lookback) + +# await data.trades_pnl_graph.refresh(ENGINE=ENGINE, LH_PAIR=LH_PAIR, RH_PAIR=RH_PAIR, lookback=lookback) +# await ui_components.er_table.refresh(ENGINE=ENGINE, lookback=lookback) +# await ui_components.trades_table.refresh(ENGINE=ENGINE, lookback=lookback) +# await ui_components.er_stats.refresh(ENGINE=ENGINE, lookback=lookback) + +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('

', sanitize=False).classes('w-full') + ui.run_javascript('await create_tv();') + + +ui.run(root, storage_secret="123ABC", reload=True, dark=True, title='Atwater Trading') \ No newline at end of file diff --git a/nicegui_modules/static/script.js b/nicegui_modules/static/script.js new file mode 100644 index 0000000..add9035 --- /dev/null +++ b/nicegui_modules/static/script.js @@ -0,0 +1,220 @@ +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 + }, + 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', + priceRange: { + minValue: 0, + maxValue: 1 + }, + // 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'; + //
+ // Atwater Trading + //
+ toolTip.innerHTML = ` +
+ Chainlink: ${Math.round(100 * value_px) / 100} + Binance: ${Math.round(100 * value_px_b) / 100} +
+
+ ${dateStr} +
+ `; + + 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!") +}; \ No newline at end of file diff --git a/nicegui_modules/static/styles.css b/nicegui_modules/static/styles.css new file mode 100644 index 0000000..7dc00b2 --- /dev/null +++ b/nicegui_modules/static/styles.css @@ -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; +} */ \ No newline at end of file diff --git a/order_entry.ipynb b/order_entry.ipynb index 2b6a579..18da8a8 100644 --- a/order_entry.ipynb +++ b/order_entry.ipynb @@ -2,7 +2,7 @@ "cells": [ { "cell_type": "code", - "execution_count": 64, + "execution_count": 2, "id": "c0bfb3b5", "metadata": {}, "outputs": [], @@ -21,7 +21,7 @@ }, { "cell_type": "code", - "execution_count": 65, + "execution_count": 3, "id": "7d7dc787", "metadata": {}, "outputs": [], @@ -49,17 +49,17 @@ }, { "cell_type": "code", - "execution_count": 66, + "execution_count": 4, "id": "c3e07e21", "metadata": {}, "outputs": [ { "data": { "text/plain": [ - "Timestamp('2026-03-27 03:15:00')" + "Timestamp('2026-03-29 05:05:00')" ] }, - "execution_count": 66, + "execution_count": 4, "metadata": {}, "output_type": "execute_result" } @@ -74,22 +74,22 @@ }, { "cell_type": "code", - "execution_count": 67, + "execution_count": 5, "id": "5ba43ffc", "metadata": {}, "outputs": [ { "data": { "text/plain": [ - "{'Up': '97875487643168796351669326324566509161830383659944117871160601839654217457417',\n", - " 'Down': '96344823573113580705457152659674776966355813491715728702490170635510049560213',\n", + "{'Up': '100363012413042717231742069495969784533240629092182259086983062365921258452690',\n", + " 'Down': '33447700958945117801119737006959954665425600820162218508565813029801829841900',\n", " 'isActive': False,\n", " 'MinTickSize': 0.01,\n", " 'isNegRisk': False,\n", - " 'ConditionId': '0x071d8568d3d736502bd450e150ef93481992d1d26df0c094cc119246d8931a23'}" + " 'ConditionId': '0x7e64102ae25311ab43ed27d0fe2c614dd9deefdd1324012b91f0814b530cb7db'}" ] }, - "execution_count": 67, + "execution_count": 5, "metadata": {}, "output_type": "execute_result" } @@ -100,7 +100,7 @@ }, { "cell_type": "code", - "execution_count": 61, + "execution_count": 7, "id": "5d356d3b", "metadata": {}, "outputs": [ @@ -109,7 +109,7 @@ "output_type": "stream", "text": [ "creating client...\n", - "You've made 41 trades\n", + "You've made 44 trades\n", "client created successfully!\n" ] } @@ -120,7 +120,7 @@ }, { "cell_type": "code", - "execution_count": 62, + "execution_count": 8, "id": "bebb53eb", "metadata": {}, "outputs": [ @@ -128,7 +128,7 @@ "name": "stdout", "output_type": "stream", "text": [ - "{'price': '0.84', 'side': 'BUY'}\n" + "{'price': '0.01', 'side': 'SELL'}\n" ] } ], @@ -147,15 +147,22 @@ }, { "cell_type": "code", - "execution_count": 63, + "execution_count": 9, "id": "52c0c38a", "metadata": {}, "outputs": [ { - "name": "stdout", - "output_type": "stream", - "text": [ - "[{'errorMsg': '', 'orderID': '0x4e8e8b193d91c2d4e7b455d3d654a26697ee3289399a9140b2b1888dda7b9a16', 'takingAmount': '10', 'makingAmount': '1.7', 'status': 'matched', 'transactionsHashes': ['0x4f66978cc001a819d9ba266f708148f0512414627a59ffd9a48d8e4b92e5a716'], 'success': True}, {'errorMsg': '', 'orderID': '0x6c5aad6b231c8a6d7a91c1260fe31955e3715e6557aaa773029bd9ec42f26917', 'takingAmount': '', 'makingAmount': '', 'status': 'live', 'success': True}]\n" + "ename": "PolyApiException", + "evalue": "PolyApiException[status_code=403, error_message={'error': 'Trading restricted in your region, please refer to available regions - https://docs.polymarket.com/developers/CLOB/geoblock'}]", + "output_type": "error", + "traceback": [ + "\u001b[31m---------------------------------------------------------------------------\u001b[39m", + "\u001b[31mPolyApiException\u001b[39m Traceback (most recent call last)", + "\u001b[36mCell\u001b[39m\u001b[36m \u001b[39m\u001b[32mIn[9]\u001b[39m\u001b[32m, line 1\u001b[39m\n\u001b[32m----> \u001b[39m\u001b[32m1\u001b[39m response = \u001b[43mclient\u001b[49m\u001b[43m.\u001b[49m\u001b[43mpost_orders\u001b[49m\u001b[43m(\u001b[49m\u001b[43m[\u001b[49m\n\u001b[32m 2\u001b[39m \u001b[43m \u001b[49m\u001b[43mPostOrdersArgs\u001b[49m\u001b[43m(\u001b[49m\n\u001b[32m 3\u001b[39m \u001b[43m \u001b[49m\u001b[43morder\u001b[49m\u001b[43m=\u001b[49m\u001b[43mclient\u001b[49m\u001b[43m.\u001b[49m\u001b[43mcreate_order\u001b[49m\u001b[43m(\u001b[49m\n\u001b[32m 4\u001b[39m \u001b[43m \u001b[49m\u001b[43morder_args\u001b[49m\u001b[43m=\u001b[49m\u001b[43mOrderArgs\u001b[49m\u001b[43m(\u001b[49m\n\u001b[32m 5\u001b[39m \u001b[43m \u001b[49m\u001b[43mtoken_id\u001b[49m\u001b[43m=\u001b[49m\u001b[43mmarket_details\u001b[49m\u001b[43m[\u001b[49m\u001b[33;43m'\u001b[39;49m\u001b[33;43mUp\u001b[39;49m\u001b[33;43m'\u001b[39;49m\u001b[43m]\u001b[49m\u001b[43m,\u001b[49m\n\u001b[32m 6\u001b[39m \u001b[43m \u001b[49m\u001b[43mprice\u001b[49m\u001b[43m=\u001b[49m\u001b[32;43m0.20\u001b[39;49m\u001b[43m,\u001b[49m\n\u001b[32m 7\u001b[39m \u001b[43m \u001b[49m\u001b[43msize\u001b[49m\u001b[43m=\u001b[49m\u001b[32;43m10\u001b[39;49m\u001b[43m,\u001b[49m\n\u001b[32m 8\u001b[39m \u001b[43m \u001b[49m\u001b[43mside\u001b[49m\u001b[43m=\u001b[49m\u001b[43mBUY\u001b[49m\u001b[43m,\u001b[49m\n\u001b[32m 9\u001b[39m \u001b[43m \u001b[49m\u001b[43m)\u001b[49m\u001b[43m,\u001b[49m\n\u001b[32m 10\u001b[39m \u001b[43m \u001b[49m\u001b[43moptions\u001b[49m\u001b[43m=\u001b[49m\u001b[43mPartialCreateOrderOptions\u001b[49m\u001b[43m(\u001b[49m\n\u001b[32m 11\u001b[39m \u001b[43m \u001b[49m\u001b[43mtick_size\u001b[49m\u001b[43m=\u001b[49m\u001b[38;5;28;43mstr\u001b[39;49m\u001b[43m(\u001b[49m\u001b[43mmarket_details\u001b[49m\u001b[43m[\u001b[49m\u001b[33;43m'\u001b[39;49m\u001b[33;43mMinTickSize\u001b[39;49m\u001b[33;43m'\u001b[39;49m\u001b[43m]\u001b[49m\u001b[43m)\u001b[49m\u001b[43m,\u001b[49m\n\u001b[32m 12\u001b[39m \u001b[43m \u001b[49m\u001b[43mneg_risk\u001b[49m\u001b[43m=\u001b[49m\u001b[43mmarket_details\u001b[49m\u001b[43m[\u001b[49m\u001b[33;43m'\u001b[39;49m\u001b[33;43misNegRisk\u001b[39;49m\u001b[33;43m'\u001b[39;49m\u001b[43m]\u001b[49m\n\u001b[32m 13\u001b[39m \u001b[43m \u001b[49m\u001b[43m)\u001b[49m\u001b[43m,\u001b[49m\n\u001b[32m 14\u001b[39m \u001b[43m \u001b[49m\u001b[43m)\u001b[49m\u001b[43m,\u001b[49m\n\u001b[32m 15\u001b[39m \u001b[43m \u001b[49m\u001b[43morderType\u001b[49m\u001b[43m=\u001b[49m\u001b[43mOrderType\u001b[49m\u001b[43m.\u001b[49m\u001b[43mGTC\u001b[49m\u001b[43m,\u001b[49m\n\u001b[32m 16\u001b[39m \u001b[43m \u001b[49m\u001b[43mpostOnly\u001b[49m\u001b[43m=\u001b[49m\u001b[38;5;28;43;01mFalse\u001b[39;49;00m\u001b[43m,\u001b[49m\n\u001b[32m 17\u001b[39m \u001b[43m \u001b[49m\u001b[43m)\u001b[49m\u001b[43m,\u001b[49m\n\u001b[32m 18\u001b[39m \u001b[43m \u001b[49m\u001b[43mPostOrdersArgs\u001b[49m\u001b[43m(\u001b[49m\n\u001b[32m 19\u001b[39m \u001b[43m \u001b[49m\u001b[43morder\u001b[49m\u001b[43m=\u001b[49m\u001b[43mclient\u001b[49m\u001b[43m.\u001b[49m\u001b[43mcreate_order\u001b[49m\u001b[43m(\u001b[49m\n\u001b[32m 20\u001b[39m \u001b[43m \u001b[49m\u001b[43morder_args\u001b[49m\u001b[43m=\u001b[49m\u001b[43mOrderArgs\u001b[49m\u001b[43m(\u001b[49m\n\u001b[32m 21\u001b[39m \u001b[43m \u001b[49m\u001b[43mtoken_id\u001b[49m\u001b[43m=\u001b[49m\u001b[43mmarket_details\u001b[49m\u001b[43m[\u001b[49m\u001b[33;43m'\u001b[39;49m\u001b[33;43mDown\u001b[39;49m\u001b[33;43m'\u001b[39;49m\u001b[43m]\u001b[49m\u001b[43m,\u001b[49m\n\u001b[32m 22\u001b[39m \u001b[43m \u001b[49m\u001b[43mprice\u001b[49m\u001b[43m=\u001b[49m\u001b[32;43m0.10\u001b[39;49m\u001b[43m,\u001b[49m\n\u001b[32m 23\u001b[39m \u001b[43m \u001b[49m\u001b[43msize\u001b[49m\u001b[43m=\u001b[49m\u001b[32;43m10\u001b[39;49m\u001b[43m,\u001b[49m\n\u001b[32m 24\u001b[39m \u001b[43m \u001b[49m\u001b[43mside\u001b[49m\u001b[43m=\u001b[49m\u001b[43mBUY\u001b[49m\u001b[43m,\u001b[49m\n\u001b[32m 25\u001b[39m \u001b[43m \u001b[49m\u001b[43m)\u001b[49m\u001b[43m,\u001b[49m\n\u001b[32m 26\u001b[39m \u001b[43m \u001b[49m\u001b[43moptions\u001b[49m\u001b[43m=\u001b[49m\u001b[43mPartialCreateOrderOptions\u001b[49m\u001b[43m(\u001b[49m\n\u001b[32m 27\u001b[39m \u001b[43m \u001b[49m\u001b[43mtick_size\u001b[49m\u001b[43m=\u001b[49m\u001b[38;5;28;43mstr\u001b[39;49m\u001b[43m(\u001b[49m\u001b[43mmarket_details\u001b[49m\u001b[43m[\u001b[49m\u001b[33;43m'\u001b[39;49m\u001b[33;43mMinTickSize\u001b[39;49m\u001b[33;43m'\u001b[39;49m\u001b[43m]\u001b[49m\u001b[43m)\u001b[49m\u001b[43m,\u001b[49m\n\u001b[32m 28\u001b[39m \u001b[43m \u001b[49m\u001b[43mneg_risk\u001b[49m\u001b[43m=\u001b[49m\u001b[43mmarket_details\u001b[49m\u001b[43m[\u001b[49m\u001b[33;43m'\u001b[39;49m\u001b[33;43misNegRisk\u001b[39;49m\u001b[33;43m'\u001b[39;49m\u001b[43m]\u001b[49m\n\u001b[32m 29\u001b[39m \u001b[43m \u001b[49m\u001b[43m)\u001b[49m\u001b[43m,\u001b[49m\n\u001b[32m 30\u001b[39m \u001b[43m \u001b[49m\u001b[43m)\u001b[49m\u001b[43m,\u001b[49m\n\u001b[32m 31\u001b[39m \u001b[43m \u001b[49m\u001b[43morderType\u001b[49m\u001b[43m=\u001b[49m\u001b[43mOrderType\u001b[49m\u001b[43m.\u001b[49m\u001b[43mGTC\u001b[49m\u001b[43m,\u001b[49m\n\u001b[32m 32\u001b[39m \u001b[43m \u001b[49m\u001b[43mpostOnly\u001b[49m\u001b[43m=\u001b[49m\u001b[38;5;28;43;01mTrue\u001b[39;49;00m\u001b[43m,\u001b[49m\n\u001b[32m 33\u001b[39m \u001b[43m \u001b[49m\u001b[43m)\u001b[49m\u001b[43m,\u001b[49m\n\u001b[32m 34\u001b[39m \u001b[43m]\u001b[49m\u001b[43m)\u001b[49m\n\u001b[32m 36\u001b[39m \u001b[38;5;28mprint\u001b[39m(response)\n", + "\u001b[36mFile \u001b[39m\u001b[32m~/miniconda3/envs/py_313/lib/python3.13/site-packages/py_clob_client/client.py:617\u001b[39m, in \u001b[36mClobClient.post_orders\u001b[39m\u001b[34m(self, args)\u001b[39m\n\u001b[32m 611\u001b[39m \u001b[38;5;28;01mreturn\u001b[39;00m post(\n\u001b[32m 612\u001b[39m \u001b[33m\"\u001b[39m\u001b[38;5;132;01m{}\u001b[39;00m\u001b[38;5;132;01m{}\u001b[39;00m\u001b[33m\"\u001b[39m.format(\u001b[38;5;28mself\u001b[39m.host, POST_ORDERS),\n\u001b[32m 613\u001b[39m headers=builder_headers,\n\u001b[32m 614\u001b[39m data=request_args.serialized_body,\n\u001b[32m 615\u001b[39m )\n\u001b[32m 616\u001b[39m \u001b[38;5;66;03m# send exact serialized bytes\u001b[39;00m\n\u001b[32m--> \u001b[39m\u001b[32m617\u001b[39m \u001b[38;5;28;01mreturn\u001b[39;00m \u001b[43mpost\u001b[49m\u001b[43m(\u001b[49m\n\u001b[32m 618\u001b[39m \u001b[43m \u001b[49m\u001b[33;43m\"\u001b[39;49m\u001b[38;5;132;43;01m{}\u001b[39;49;00m\u001b[38;5;132;43;01m{}\u001b[39;49;00m\u001b[33;43m\"\u001b[39;49m\u001b[43m.\u001b[49m\u001b[43mformat\u001b[49m\u001b[43m(\u001b[49m\u001b[38;5;28;43mself\u001b[39;49m\u001b[43m.\u001b[49m\u001b[43mhost\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mPOST_ORDERS\u001b[49m\u001b[43m)\u001b[49m\u001b[43m,\u001b[49m\n\u001b[32m 619\u001b[39m \u001b[43m \u001b[49m\u001b[43mheaders\u001b[49m\u001b[43m=\u001b[49m\u001b[43mheaders\u001b[49m\u001b[43m,\u001b[49m\n\u001b[32m 620\u001b[39m \u001b[43m \u001b[49m\u001b[43mdata\u001b[49m\u001b[43m=\u001b[49m\u001b[43mrequest_args\u001b[49m\u001b[43m.\u001b[49m\u001b[43mserialized_body\u001b[49m\u001b[43m,\u001b[49m\n\u001b[32m 621\u001b[39m \u001b[43m\u001b[49m\u001b[43m)\u001b[49m\n", + "\u001b[36mFile \u001b[39m\u001b[32m~/miniconda3/envs/py_313/lib/python3.13/site-packages/py_clob_client/http_helpers/helpers.py:69\u001b[39m, in \u001b[36mpost\u001b[39m\u001b[34m(endpoint, headers, data)\u001b[39m\n\u001b[32m 68\u001b[39m \u001b[38;5;28;01mdef\u001b[39;00m\u001b[38;5;250m \u001b[39m\u001b[34mpost\u001b[39m(endpoint, headers=\u001b[38;5;28;01mNone\u001b[39;00m, data=\u001b[38;5;28;01mNone\u001b[39;00m):\n\u001b[32m---> \u001b[39m\u001b[32m69\u001b[39m \u001b[38;5;28;01mreturn\u001b[39;00m \u001b[43mrequest\u001b[49m\u001b[43m(\u001b[49m\u001b[43mendpoint\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mPOST\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mheaders\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mdata\u001b[49m\u001b[43m)\u001b[49m\n", + "\u001b[36mFile \u001b[39m\u001b[32m~/miniconda3/envs/py_313/lib/python3.13/site-packages/py_clob_client/http_helpers/helpers.py:57\u001b[39m, in \u001b[36mrequest\u001b[39m\u001b[34m(endpoint, method, headers, data)\u001b[39m\n\u001b[32m 49\u001b[39m resp = _http_client.request(\n\u001b[32m 50\u001b[39m method=method,\n\u001b[32m 51\u001b[39m url=endpoint,\n\u001b[32m 52\u001b[39m headers=headers,\n\u001b[32m 53\u001b[39m json=data,\n\u001b[32m 54\u001b[39m )\n\u001b[32m 56\u001b[39m \u001b[38;5;28;01mif\u001b[39;00m resp.status_code != \u001b[32m200\u001b[39m:\n\u001b[32m---> \u001b[39m\u001b[32m57\u001b[39m \u001b[38;5;28;01mraise\u001b[39;00m PolyApiException(resp)\n\u001b[32m 59\u001b[39m \u001b[38;5;28;01mtry\u001b[39;00m:\n\u001b[32m 60\u001b[39m \u001b[38;5;28;01mreturn\u001b[39;00m resp.json()\n", + "\u001b[31mPolyApiException\u001b[39m: PolyApiException[status_code=403, error_message={'error': 'Trading restricted in your region, please refer to available regions - https://docs.polymarket.com/developers/CLOB/geoblock'}]" ] } ], @@ -165,7 +172,7 @@ " order=client.create_order(\n", " order_args=OrderArgs(\n", " token_id=market_details['Up'],\n", - " price=0.90,\n", + " price=0.20,\n", " size=10,\n", " side=BUY,\n", " ),\n", @@ -659,7 +666,7 @@ ], "metadata": { "kernelspec": { - "display_name": "py313", + "display_name": "py_313", "language": "python", "name": "python3" }, @@ -673,7 +680,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.13.2" + "version": "3.13.12" } }, "nbformat": 4, diff --git a/ws.py b/ws.py deleted file mode 100644 index 461e516..0000000 --- a/ws.py +++ /dev/null @@ -1,150 +0,0 @@ -import asyncio -import json -import math -import pandas as pd -import os -from datetime import datetime, timezone -import websockets -import numpy as np -import talib -import requests - -WSS_URL = "wss://ws-subscriptions-clob.polymarket.com/ws/market" -SLUG_END_TIME = 0 - -HIST_TRADES = np.empty((0, 2)) - -def format_timestamp(total_seconds) -> str: - minutes, seconds = divmod(total_seconds, 60) - return f"{minutes} minutes and {seconds} seconds" - -def time_round_down(dt, interval_mins=5) -> int: # returns timestamp in seconds - interval_secs = interval_mins * 60 - seconds = dt.timestamp() - rounded_seconds = math.floor(seconds / interval_secs) * interval_secs - - return rounded_seconds - -def get_mkt_details_by_slug(slug: str) -> dict[str, str, str]: # {'Up' : 123, 'Down': 456, 'isActive': True, 'MinTickSize': 0.01, 'isNegRisk': False} - r = requests.get(f"https://gamma-api.polymarket.com/events/slug/{slug}") - market = r.json()['markets'][0] - token_ids = json.loads(market.get("clobTokenIds", "[]")) - outcomes = json.loads(market.get("outcomes", "[]")) - d = dict(zip(outcomes, token_ids)) - d['isActive'] = market['negRisk'] - d['MinTickSize'] = market['orderPriceMinTickSize'] - d['isNegRisk'] = market['negRisk'] - d['ConditionId'] = market['conditionId'] - d['EndDateTime'] = market['endDate'] - - return d, market - -def gen_slug(): - slug_prefix = 'btc-updown-5m-' - slug_time_id = time_round_down(dt=datetime.now(timezone.utc)) - return slug_prefix + str(slug_time_id) - - -async def polymarket_stream(): - global SLUG_END_TIME - global HIST_TRADES - - slug_full = gen_slug() - market_details, market = get_mkt_details_by_slug(slug_full) - TARGET_ASSET_ID = market_details['Up'] - SLUG_END_TIME = round(datetime.strptime(market_details['EndDateTime'], '%Y-%m-%dT%H:%M:%SZ').replace(tzinfo=timezone.utc).timestamp()) - print(f'********* NEW MKT - END DATETIME: {pd.to_datetime(SLUG_END_TIME, unit='s')} *********') - - async with websockets.connect(WSS_URL) as websocket: - print(f"Connected to {WSS_URL}") - - subscribe_msg = { - "assets_ids": [TARGET_ASSET_ID], - "type": "market", - "custom_feature_enabled": True - } - - await websocket.send(json.dumps(subscribe_msg)) - print(f"Subscribed to Asset: {TARGET_ASSET_ID}") - - try: - async for message in websocket: - current_ts = round(datetime.now().timestamp()) - sec_remaining = SLUG_END_TIME - current_ts - - if sec_remaining <= 0: - HIST_TRADES = np.empty((0, 2)) - - print('*** Attempting to unsub from past 5min') - update_unsub_msg = { - "operation": 'unsubscribe', - "assets_ids": [TARGET_ASSET_ID], - "custom_feature_enabled": True - } - await websocket.send(json.dumps(update_unsub_msg)) - - print('*** Attempting to SUB to new 5min') - slug_full = gen_slug() - market_details, market = get_mkt_details_by_slug(slug_full) - TARGET_ASSET_ID = market_details['Up'] - SLUG_END_TIME = round(datetime.strptime(market_details['EndDateTime'], '%Y-%m-%dT%H:%M:%SZ').replace(tzinfo=timezone.utc).timestamp()) - - update_sub_msg = { - "operation": 'subscribe', - "assets_ids": [TARGET_ASSET_ID], - "custom_feature_enabled": True - } - await websocket.send(json.dumps(update_sub_msg)) - - - if isinstance(message, str): - data = json.loads(message) - - if isinstance(data, dict): - # print(data.get("event_type", None)) - pass - elif isinstance(data, list): - print('initial book: ') - print(data) - continue - else: - raise ValueError(f'Type: {type(data)} not expected: {message}') - - event_type = data.get("event_type", None) - - if event_type == "price_change": - # print("📈 Price Change") - # print(pd.DataFrame(data['price_changes'])) - pass - elif event_type == "best_bid_ask": - # print(pd.DataFrame([data])) - pass - elif event_type == "last_trade_price": - px = float(data['price']) - qty = float(data['size']) - HIST_TRADES = np.append(HIST_TRADES, np.array([[px, qty]]), axis=0) - SMA = talib.ROC(HIST_TRADES[:,0], timeperiod=10)[-1] - print(f"✨ Last Px: {px:.2f}; ROC: {SMA:.4f}; Qty: {qty:6.2f}; Sec Left: {sec_remaining}") - elif event_type == "book": - pass - elif event_type == "new_market": - print('Received new_market') - elif event_type == "market_resolved": - print(f"Received: {event_type}") - print(data) - elif event_type == "tick_size_change": # may want for CLOB order routing - print(f"Received: {event_type}") - print(data) - else: - print(f"Received: {event_type}") - print(data) - - except websockets.ConnectionClosed: - print("Connection closed by server.") - - -if __name__ == '__main__': - try: - asyncio.run(polymarket_stream()) - except KeyboardInterrupt: - print("Stream stopped.") \ No newline at end of file diff --git a/ws_binance.py b/ws_binance.py new file mode 100644 index 0000000..2202358 --- /dev/null +++ b/ws_binance.py @@ -0,0 +1,199 @@ +import asyncio +import json +import logging +import socket +import traceback +from datetime import datetime +from typing import AsyncContextManager + +import numpy as np +import pandas as pd +import requests.packages.urllib3.util.connection as urllib3_cn # type: ignore +from sqlalchemy import text +import websockets +from sqlalchemy.ext.asyncio import create_async_engine +import valkey + +### Allow only ipv4 ### +def allowed_gai_family(): + return socket.AF_INET +urllib3_cn.allowed_gai_family = allowed_gai_family + +### Database ### +USE_DB: bool = True +USE_VK: bool = True +VK_CHANNEL = 'poly_binance_btcusd' +CON: AsyncContextManager | None = None +VAL_KEY = None + + +### Logging ### +LOG_FILEPATH: str = '/root/logs/Polymarket_Binance_Trades.log' + +### Globals ### +WSS_URL = "wss://stream.binance.com:9443/ws/BTCUSDT@trade" +# HIST_TRADES = np.empty((0, 2)) + +### Database Funcs ### +async def create_rtds_btcusd_table( + CON: AsyncContextManager, + engine: str = 'mysql', # mysql | duckdb + ) -> None: + if CON is None: + logging.info("NO DB CONNECTION, SKIPPING Create Statements") + else: + if engine == 'mysql': + logging.info('Creating Table if Does Not Exist: binance_btcusd_trades') + await CON.execute(text(""" + CREATE TABLE IF NOT EXISTS binance_btcusd_trades ( + timestamp_msg BIGINT, + timestamp_value BIGINT, + value DOUBLE, + qty DOUBLE + ); + """)) + await CON.commit() + else: + raise ValueError('Only MySQL engine is implemented') + +async def insert_rtds_btcusd_table( + timestamp_msg: int, + timestamp_value: int, + value: float, + qty: float, + CON: AsyncContextManager, + engine: str = 'mysql', # mysql | duckdb + ) -> None: + params={ + 'timestamp_msg': timestamp_msg, + 'timestamp_value': timestamp_value, + 'value': value, + 'qty': qty, + } + if CON is None: + logging.info("NO DB CONNECTION, SKIPPING Insert Statements") + else: + if engine == 'mysql': + await CON.execute(text(""" + INSERT INTO binance_btcusd_trades + ( + timestamp_msg, + timestamp_value, + value, + qty + ) + VALUES + ( + :timestamp_msg, + :timestamp_value, + :value, + :qty + ) + """), + parameters=params + ) + await CON.commit() + else: + raise ValueError('Only MySQL engine is implemented') + + +### Websocket ### +async def binance_trades_stream(): + global HIST_TRADES + + async for websocket in websockets.connect(WSS_URL): + logging.info(f"Connected to {WSS_URL}") + + subscribe_msg = { + "method": "SUBSCRIBE", + "params": ["btcusdt@trade"], + "id": 1 + } + + await websocket.send(json.dumps(subscribe_msg)) + + try: + async for message in websocket: + if isinstance(message, str): + try: + data = json.loads(message) + if data.get('t', None) is not None: + last_px = float(data['p']) + print(f'🤑 BTC Binance Last Px: {last_px:_.4f}; TS: {pd.to_datetime(data['T'], unit='ms')}') + VAL_KEY.publish(VK_CHANNEL, json.dumps({ + 'timestamp_msg': data['E'], + 'timestamp_value': data['T'], + 'value': last_px, + 'qty': data['q'], + })) + VAL_KEY.set(VK_CHANNEL, json.dumps({ + 'timestamp_msg': data['E'], + 'timestamp_value': data['T'], + 'value': last_px, + 'qty': data['q'], + })) + await insert_rtds_btcusd_table( + CON=CON, + timestamp_msg=data['E'], + timestamp_value=data['T'], + value=last_px, + qty=data['q'], + ) + else: + logging.info(f'Initial or unexpected data struct, skipping: {data}') + continue + except (json.JSONDecodeError, ValueError): + logging.warning(f'Message not in JSON format, skipping: {message}') + continue + else: + raise ValueError(f'Type: {type(data)} not expected: {message}') + except websockets.ConnectionClosed as e: + logging.error(f'Connection closed: {e}') + logging.error(traceback.format_exc()) + continue + except Exception as e: + logging.error(f'Connection closed: {e}') + logging.error(traceback.format_exc()) + + +async def main(): + global VAL_KEY + global CON + + if USE_VK: + VAL_KEY = valkey.Valkey(host='localhost', port=6379, db=0) + published_count = VAL_KEY.publish(VK_CHANNEL,f"Hola, starting to publish to valkey: {VK_CHANNEL} @ {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}") + logging.info(f"Valkey message published to {published_count} subscribers of {VK_CHANNEL}") + else: + VAL_KEY = None + logging.warning("VALKEY NOT BEING USED, NO DATA WILL BE PUBLISHED") + + if USE_DB: + engine = create_async_engine('mysql+asyncmy://root:pwd@localhost/polymarket') + async with engine.connect() as CON: + await create_rtds_btcusd_table(CON=CON) + await binance_trades_stream() + else: + CON = None + logging.warning("DATABASE NOT BEING USED, NO DATA WILL BE RECORDED") + await binance_trades_stream() + + +if __name__ == '__main__': + START_TIME = round(datetime.now().timestamp()*1000) + + logging.info(f'Log FilePath: {LOG_FILEPATH}') + + logging.basicConfig( + force=True, + filename=LOG_FILEPATH, + level=logging.INFO, + format='%(asctime)s - %(levelname)s - %(message)s', + filemode='w' + ) + logging.info(f"STARTED: {START_TIME}") + + try: + asyncio.run(main()) + except KeyboardInterrupt: + logging.info("Stream stopped") \ No newline at end of file diff --git a/ws_clob.py b/ws_clob.py new file mode 100644 index 0000000..3a2cd6b --- /dev/null +++ b/ws_clob.py @@ -0,0 +1,312 @@ +import asyncio +import json +import math +import logging +import pandas as pd +import os +from datetime import datetime, timezone +import websockets +import numpy as np +import talib +import requests +from typing import AsyncContextManager +from sqlalchemy.ext.asyncio import create_async_engine +from sqlalchemy import text +import valkey + +### Database ### +USE_DB: bool = True +USE_VK: bool = True +VK_CHANNEL = 'poly_5min_btcusd' +CON: AsyncContextManager | None = None +VAL_KEY = None + +### Logging ### +LOG_FILEPATH: str = '/root/logs/Polymarket_5min.log' + +WSS_URL = "wss://ws-subscriptions-clob.polymarket.com/ws/market" +SLUG_END_TIME = 0 + +HIST_TRADES = np.empty((0, 2)) +TARGET_PX = 0 + +def format_timestamp(total_seconds) -> str: + minutes, seconds = divmod(total_seconds, 60) + + return f"{minutes} minutes and {seconds} seconds" + +def time_round_down(dt, interval_mins=5) -> int: # returns timestamp in seconds + interval_secs = interval_mins * 60 + seconds = dt.timestamp() + rounded_seconds = math.floor(seconds / interval_secs) * interval_secs + + return rounded_seconds + +def get_mkt_details_by_slug(slug: str) -> dict[str, str, str]: # {'Up' : 123, 'Down': 456, 'isActive': True, 'MinTickSize': 0.01, 'isNegRisk': False} + r = requests.get(f"https://gamma-api.polymarket.com/events/slug/{slug}") + market = r.json()['markets'][0] + token_ids = json.loads(market.get("clobTokenIds", "[]")) + outcomes = json.loads(market.get("outcomes", "[]")) + d = dict(zip(outcomes, token_ids)) + d['isActive'] = market['negRisk'] + d['MinTickSize'] = market['orderPriceMinTickSize'] + d['OrderMinSize'] = market['orderMinSize'] + d['isNegRisk'] = market['negRisk'] + d['ConditionId'] = market['conditionId'] + d['EndDateTime'] = market['endDate'] + # d['Liquidity'] = market['liquidity'] + d['LiquidityClob'] = market['liquidityClob'] + d['VolumeNum'] = market['volumeNum'] + d['Volume24hr'] = market['volume24hr'] + print(market) + + return d, market + +def gen_slug(): + slug_prefix = 'btc-updown-5m-' + slug_time_id = time_round_down(dt=datetime.now(timezone.utc)) + + return slug_prefix + str(slug_time_id) + + +### Database Funcs ### +async def create_poly_btcusd_trades_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: poly_btcusd_trades') + await CON.execute(text(""" + CREATE TABLE IF NOT EXISTS poly_btcusd_trades ( + timestamp_arrival BIGINT, + timestamp_msg BIGINT, + timestamp_value BIGINT, + price DOUBLE, + qty DOUBLE, + side_taker VARCHAR(8) + ); + """)) + await CON.commit() + else: + raise ValueError('Only MySQL engine is implemented') + +async def insert_poly_btcusd_trades_table( + timestamp_arrival: int, + timestamp_msg: int, + timestamp_value: int, + price: float, + qty: float, + side_taker: str, + CON: AsyncContextManager, + engine: str = 'mysql', # mysql | duckdb + ) -> None: + params={ + 'timestamp_arrival': timestamp_arrival, + 'timestamp_msg': timestamp_msg, + 'timestamp_value': timestamp_value, + 'price': price, + 'qty': qty, + 'side_taker': side_taker, + } + if CON is None: + logging.info("NO DB CONNECTION, SKIPPING Insert Statements") + else: + if engine == 'mysql': + await CON.execute(text(""" + INSERT INTO poly_btcusd_trades + ( + timestamp_arrival, + timestamp_msg, + timestamp_value, + price, + qty, + side_taker + ) + VALUES + ( + :timestamp_arrival, + :timestamp_msg, + :timestamp_value, + :price, + :qty, + :side_taker + ) + """), + parameters=params + ) + await CON.commit() + else: + raise ValueError('Only MySQL engine is implemented') + + +async def polymarket_stream(): + global SLUG_END_TIME + global TARGET_PX + global HIST_TRADES + + slug_full = gen_slug() + market_details, market = get_mkt_details_by_slug(slug_full) + TARGET_ASSET_ID = market_details['Up'] + SLUG_END_TIME = round(datetime.strptime(market_details['EndDateTime'], '%Y-%m-%dT%H:%M:%SZ').replace(tzinfo=timezone.utc).timestamp()) + print(f'********* NEW MKT - END DATETIME: {pd.to_datetime(SLUG_END_TIME, unit='s')} *********') + + async with websockets.connect(WSS_URL) as websocket: + print(f"Connected to {WSS_URL}") + + subscribe_msg = { + "assets_ids": [TARGET_ASSET_ID], + "type": "market", + "custom_feature_enabled": True + } + + await websocket.send(json.dumps(subscribe_msg)) + print(f"Subscribed to Asset: {TARGET_ASSET_ID}") + + try: + async for message in websocket: + ts_arrival = round(datetime.now().timestamp()*1000) + sec_remaining = SLUG_END_TIME - round(datetime.now().timestamp()) + + if sec_remaining <= 0: + ref_data = json.loads(VAL_KEY.get('poly_rtds_cl_btcusd')) + TARGET_PX = float(ref_data['value']) + HIST_TRADES = np.empty((0, 2)) + + print('*** Attempting to unsub from past 5min') + update_unsub_msg = { + "operation": 'unsubscribe', + "assets_ids": [TARGET_ASSET_ID], + "custom_feature_enabled": True + } + await websocket.send(json.dumps(update_unsub_msg)) + + print('*** Attempting to SUB to new 5min') + slug_full = gen_slug() + market_details, market = get_mkt_details_by_slug(slug_full) + TARGET_ASSET_ID = market_details['Up'] + SLUG_END_TIME = round(datetime.strptime(market_details['EndDateTime'], '%Y-%m-%dT%H:%M:%SZ').replace(tzinfo=timezone.utc).timestamp()) + + update_sub_msg = { + "operation": 'subscribe', + "assets_ids": [TARGET_ASSET_ID], + "custom_feature_enabled": True + } + await websocket.send(json.dumps(update_sub_msg)) + + if isinstance(message, str): + data = json.loads(message) + if isinstance(data, list): + print('initial book:') + print(data) + continue + + event_type = data.get("event_type", None) + + if event_type == "price_change": + # print("📈 Price Change") + # print(pd.DataFrame(data['price_changes'])) + pass + elif event_type == "best_bid_ask": + # print(pd.DataFrame([data])) + pass + elif event_type == "last_trade_price": + ts_msg = int(data['timestamp']) + ts_value = int(ts_msg) + px = float(data['price']) + qty = float(data['size']) + side_taker = data['side'] + HIST_TRADES = np.append(HIST_TRADES, np.array([[px, qty]]), axis=0) + # SMA = talib.ROC(HIST_TRADES[:,0], timeperiod=10)[-1] + # print(f"✨ Last Px: {px:.2f}; ROC: {SMA:.4f}; Qty: {qty:6.2f}; Sec Left: {sec_remaining}") + print(f"✨ Last Px: {px:.2f}; Qty: {qty:6.2f}; Sec Left: {sec_remaining}") + + if USE_VK: + VAL_KEY_OBJ = json.dumps({ + 'timestamp_arrival': ts_arrival, + 'timestamp_msg': ts_msg, + 'timestamp_value': ts_value, + 'price': px, + 'qty': qty, + 'side_taker': side_taker, + 'sec_remaining': sec_remaining, + 'target_price': TARGET_PX + }) + VAL_KEY.publish(VK_CHANNEL, VAL_KEY_OBJ) + VAL_KEY.set(VK_CHANNEL, VAL_KEY_OBJ) + if USE_DB: + await insert_poly_btcusd_trades_table( + CON=CON, + timestamp_arrival=ts_arrival, + timestamp_msg=ts_msg, + timestamp_value=ts_value, + price=px, + qty=qty, + side_taker=side_taker, + ) + + elif event_type == "book": + pass + elif event_type == "new_market": + print('Received new_market') + elif event_type == "market_resolved": + print(f"Received: {event_type}") + print(data) + elif event_type == "tick_size_change": # may want for CLOB order routing + print(f"Received: {event_type}") + print(data) + else: + print(f"*********** REC UNMAPPED EVENT: {event_type}") + print(data) + elif isinstance(data, dict): + # print(data.get("event_type", None)) + pass + else: + raise ValueError(f'Type: {type(data)} not expected: {message}') + + except websockets.ConnectionClosed as e: + print(f"Connection closed by server. Exception: {e}") + +async def main(): + global VAL_KEY + global CON + + if USE_VK: + VAL_KEY = valkey.Valkey(host='localhost', port=6379, db=0) + published_count = VAL_KEY.publish(VK_CHANNEL,f"Hola, starting to publish to valkey: {VK_CHANNEL} @ {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}") + logging.info(f"Valkey message published to {published_count} subscribers of {VK_CHANNEL}") + else: + VAL_KEY = None + logging.warning("VALKEY NOT BEING USED, NO DATA WILL BE PUBLISHED") + + if USE_DB: + engine = create_async_engine('mysql+asyncmy://root:pwd@localhost/polymarket') + async with engine.connect() as CON: + await create_poly_btcusd_trades_table(CON=CON) + await polymarket_stream() + else: + CON = None + logging.warning("DATABASE NOT BEING USED, NO DATA WILL BE RECORDED") + await polymarket_stream() + + +if __name__ == '__main__': + START_TIME = round(datetime.now().timestamp()*1000) + + logging.info(f'Log FilePath: {LOG_FILEPATH}') + + logging.basicConfig( + force=True, + filename=LOG_FILEPATH, + level=logging.INFO, + format='%(asctime)s - %(levelname)s - %(message)s', + filemode='w' + ) + logging.info(f"STARTED: {START_TIME}") + + try: + asyncio.run(main()) + except KeyboardInterrupt as e: + print(f"Stream stopped: {e}") \ No newline at end of file diff --git a/ws_coinbase.py b/ws_coinbase.py new file mode 100644 index 0000000..e4bc138 --- /dev/null +++ b/ws_coinbase.py @@ -0,0 +1,224 @@ +import asyncio +import json +import logging +import socket +import traceback +from datetime import datetime +from typing import AsyncContextManager + +import numpy as np +import pandas as pd +import requests.packages.urllib3.util.connection as urllib3_cn # type: ignore +from sqlalchemy import text +import websockets +from sqlalchemy.ext.asyncio import create_async_engine +import valkey + +### Allow only ipv4 ### +def allowed_gai_family(): + return socket.AF_INET +urllib3_cn.allowed_gai_family = allowed_gai_family + +### Database ### +USE_DB: bool = True +USE_VK: bool = True +VK_CHANNEL = 'poly_coinbase_btcusd' +CON: AsyncContextManager | None = None +VAL_KEY = None + +### Logging ### +LOG_FILEPATH: str = '/root/logs/Polymarket_coinbase_Trades.log' + +### Globals ### +WSS_URL = "wss://ws-feed.exchange.coinbase.com" +# HIST_TRADES = np.empty((0, 2)) + +### Database Funcs ### +async def create_rtds_btcusd_table( + CON: AsyncContextManager, + engine: str = 'mysql', # mysql | duckdb + ) -> None: + if CON is None: + logging.info("NO DB CONNECTION, SKIPPING Create Statements") + else: + if engine == 'mysql': + logging.info('Creating Table if Does Not Exist: coinbase_btcusd_trades') + await CON.execute(text(""" + CREATE TABLE IF NOT EXISTS coinbase_btcusd_trades ( + timestamp_arrival BIGINT, + timestamp_msg BIGINT, + timestamp_value BIGINT, + value DOUBLE, + qty DOUBLE, + side VARCHAR(8) + ); + """)) + await CON.commit() + else: + raise ValueError('Only MySQL engine is implemented') + +async def insert_rtds_btcusd_table( + timestamp_arrival: int, + timestamp_msg: int, + timestamp_value: int, + value: float, + qty: float, + side: str, + CON: AsyncContextManager, + engine: str = 'mysql', # mysql | duckdb + ) -> None: + params={ + 'timestamp_arrival': timestamp_arrival, + 'timestamp_msg': timestamp_msg, + 'timestamp_value': timestamp_value, + 'value': value, + 'qty': qty, + 'side': side, + } + if CON is None: + logging.info("NO DB CONNECTION, SKIPPING Insert Statements") + else: + if engine == 'mysql': + await CON.execute(text(""" + INSERT INTO coinbase_btcusd_trades + ( + timestamp_arrival, + timestamp_msg, + timestamp_value, + value, + qty, + side + ) + VALUES + ( + :timestamp_arrival, + :timestamp_msg, + :timestamp_value, + :value, + :qty, + :side + ) + """), + parameters=params + ) + await CON.commit() + else: + raise ValueError('Only MySQL engine is implemented') + + +### Websocket ### +async def coinbase_trades_stream(): + global HIST_TRADES + + async with websockets.connect(WSS_URL) as websocket: + logging.info(f"Connected to {WSS_URL}") + + subscribe_msg = { + "type": "subscribe", + "product_ids": ["BTC-USD"], + "channels": [ + { + "name": "ticker", + "product_ids": ["BTC-USD"] + } + ] + } + + await websocket.send(json.dumps(subscribe_msg)) + + try: + async for message in websocket: + if isinstance(message, str) or isinstance(message, bytes): + try: + data = json.loads(message) + if data.get('price', None) is not None: + ts_arrival = round(datetime.now().timestamp()*1000) + ts_msg = round(datetime.strptime(data['time'], "%Y-%m-%dT%H:%M:%S.%fZ").timestamp()*1000) + ts_value = ts_msg + last_px = float(data['price']) + qty = float(data['last_size']) + side = data['side'] + print(f'🤑 BTC Coinbase Last Px: {last_px:_.4f}; TS: {pd.to_datetime(ts_value, unit='ms')}; Side: {side};') + if USE_VK: + VAL_KEY_OBJ = json.dumps({ + 'timestamp_arrival': ts_arrival, + 'timestamp_msg': ts_msg, + 'timestamp_value': ts_value, + 'value': last_px, + 'qty': qty, + 'side': side, + }) + VAL_KEY.publish(VK_CHANNEL, VAL_KEY_OBJ) + VAL_KEY.set(VK_CHANNEL, VAL_KEY_OBJ) + if USE_DB: + await insert_rtds_btcusd_table( + CON=CON, + timestamp_arrival=ts_arrival, + timestamp_msg=ts_msg, + timestamp_value=ts_value, + value=last_px, + qty=qty, + side=side, + ) + # elif data.get('op'): + # if data['op'] == 'PING': + # pong = {"op": "PONG", "timestamp": ts_arrival} + # await websocket.send(json.dumps(pong)) + # logging.info(f'PING RECEIVED: {data}; PONG SENT: {pong}') + else: + logging.info(f'Initial or unexpected data struct, skipping: {data}') + continue + except (json.JSONDecodeError, ValueError) as e: + logging.warning(f'Message not in JSON format, skipping: {message}; excepion: {e}') + continue + else: + raise ValueError(f'Type: {type(message)} not expected: {message}') + except websockets.ConnectionClosed as e: + logging.error(f'Connection closed: {e}') + logging.error(traceback.format_exc()) + except Exception as e: + logging.error(f'Connection closed: {e}') + logging.error(traceback.format_exc()) + + +async def main(): + global VAL_KEY + global CON + + if USE_VK: + VAL_KEY = valkey.Valkey(host='localhost', port=6379, db=0) + published_count = VAL_KEY.publish(VK_CHANNEL,f"Hola, starting to publish to valkey: {VK_CHANNEL} @ {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}") + logging.info(f"Valkey message published to {published_count} subscribers of {VK_CHANNEL}") + else: + VAL_KEY = None + logging.warning("VALKEY NOT BEING USED, NO DATA WILL BE PUBLISHED") + + if USE_DB: + engine = create_async_engine('mysql+asyncmy://root:pwd@localhost/polymarket') + async with engine.connect() as CON: + await create_rtds_btcusd_table(CON=CON) + await coinbase_trades_stream() + else: + CON = None + logging.warning("DATABASE NOT BEING USED, NO DATA WILL BE RECORDED") + await coinbase_trades_stream() + + +if __name__ == '__main__': + START_TIME = round(datetime.now().timestamp()*1000) + + logging.info(f'Log FilePath: {LOG_FILEPATH}') + + logging.basicConfig( + force=True, + filename=LOG_FILEPATH, + level=logging.INFO, + format='%(asctime)s - %(levelname)s - %(message)s', + filemode='w' + ) + logging.info(f"STARTED: {START_TIME}") + + try: + asyncio.run(main()) + except KeyboardInterrupt: + logging.info("Stream stopped") \ No newline at end of file diff --git a/ws_pionex.py b/ws_pionex.py new file mode 100644 index 0000000..25ec648 --- /dev/null +++ b/ws_pionex.py @@ -0,0 +1,219 @@ +import asyncio +import json +import logging +import socket +import traceback +from datetime import datetime +from typing import AsyncContextManager + +import numpy as np +import pandas as pd +import requests.packages.urllib3.util.connection as urllib3_cn # type: ignore +from sqlalchemy import text +import websockets +from sqlalchemy.ext.asyncio import create_async_engine +import valkey + +### Allow only ipv4 ### +def allowed_gai_family(): + return socket.AF_INET +urllib3_cn.allowed_gai_family = allowed_gai_family + +### Database ### +USE_DB: bool = True +USE_VK: bool = True +VK_CHANNEL = 'poly_pionex_btcusd' +CON: AsyncContextManager | None = None +VAL_KEY = None + + +### Logging ### +LOG_FILEPATH: str = '/root/logs/Polymarket_Pionex_Trades.log' + +### Globals ### +WSS_URL = "wss://ws.pionex.com/wsPub" +# HIST_TRADES = np.empty((0, 2)) + +### Database Funcs ### +async def create_rtds_btcusd_table( + CON: AsyncContextManager, + engine: str = 'mysql', # mysql | duckdb + ) -> None: + if CON is None: + logging.info("NO DB CONNECTION, SKIPPING Create Statements") + else: + if engine == 'mysql': + logging.info('Creating Table if Does Not Exist: pionex_btcusd_trades') + await CON.execute(text(""" + CREATE TABLE IF NOT EXISTS pionex_btcusd_trades ( + timestamp_msg BIGINT, + timestamp_value BIGINT, + value DOUBLE, + qty DOUBLE, + side VARCHAR(8) + ); + """)) + await CON.commit() + else: + raise ValueError('Only MySQL engine is implemented') + +async def insert_rtds_btcusd_table( + timestamp_msg: int, + timestamp_value: int, + value: float, + qty: float, + side: str, + CON: AsyncContextManager, + engine: str = 'mysql', # mysql | duckdb + ) -> None: + params={ + 'timestamp_msg': timestamp_msg, + 'timestamp_value': timestamp_value, + 'value': value, + 'qty': qty, + 'side': side, + } + if CON is None: + logging.info("NO DB CONNECTION, SKIPPING Insert Statements") + else: + if engine == 'mysql': + await CON.execute(text(""" + INSERT INTO pionex_btcusd_trades + ( + timestamp_msg, + timestamp_value, + value, + qty, + side + ) + VALUES + ( + :timestamp_msg, + :timestamp_value, + :value, + :qty, + :side + ) + """), + parameters=params + ) + await CON.commit() + else: + raise ValueError('Only MySQL engine is implemented') + + +### Websocket ### +async def pionex_trades_stream(): + global HIST_TRADES + + async with websockets.connect(WSS_URL) as websocket: + logging.info(f"Connected to {WSS_URL}") + + subscribe_msg = { + "op": "SUBSCRIBE", + "topic": "TRADE", + "symbol": "BTC_USDT" + } + + await websocket.send(json.dumps(subscribe_msg)) + + try: + async for message in websocket: + if isinstance(message, str) or isinstance(message, bytes): + try: + data = json.loads(message) + if data.get('data', None) is not None: + ts_msg = data['timestamp'] + data = data['data'] + ts_value = data[0]['timestamp'] + last_px = float(data[0]['price']) + qty = float(data[0]['size']) + side = data[0]['side'] + print(f'🤑 BTC Pionex Last Px: {last_px:_.4f}; TS: {pd.to_datetime(ts_value, unit='ms')}; Side: {side};') + print(ts_value) + if USE_VK: + VAL_KEY.publish(VK_CHANNEL, json.dumps({ + 'timestamp_msg': ts_msg, + 'timestamp_value': ts_value, + 'value': last_px, + 'qty': qty, + 'side': side, + })) + VAL_KEY.set(VK_CHANNEL, json.dumps({ + 'timestamp_msg': ts_msg, + 'timestamp_value': ts_value, + 'value': last_px, + 'qty': qty, + 'side': side, + })) + if USE_DB: + await insert_rtds_btcusd_table( + CON=CON, + timestamp_msg=ts_msg, + timestamp_value=ts_value, + value=last_px, + qty=qty, + side=side, + ) + elif data.get('op'): + if data['op'] == 'PING': + pong = {"op": "PONG", "timestamp": round(datetime.now().timestamp()*1000)} + await websocket.send(json.dumps(pong)) + logging.info(f'PING RECEIVED: {data}; PONG SENT: {pong}') + else: + logging.info(f'Initial or unexpected data struct, skipping: {data}') + continue + except (json.JSONDecodeError, ValueError) as e: + logging.warning(f'Message not in JSON format, skipping: {message}; excepion: {e}') + continue + else: + raise ValueError(f'Type: {type(message)} not expected: {message}') + except websockets.ConnectionClosed as e: + logging.error(f'Connection closed: {e}') + logging.error(traceback.format_exc()) + except Exception as e: + logging.error(f'Connection closed: {e}') + logging.error(traceback.format_exc()) + + +async def main(): + global VAL_KEY + global CON + + if USE_VK: + VAL_KEY = valkey.Valkey(host='localhost', port=6379, db=0) + published_count = VAL_KEY.publish(VK_CHANNEL,f"Hola, starting to publish to valkey: {VK_CHANNEL} @ {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}") + logging.info(f"Valkey message published to {published_count} subscribers of {VK_CHANNEL}") + else: + VAL_KEY = None + logging.warning("VALKEY NOT BEING USED, NO DATA WILL BE PUBLISHED") + + if USE_DB: + engine = create_async_engine('mysql+asyncmy://root:pwd@localhost/polymarket') + async with engine.connect() as CON: + await create_rtds_btcusd_table(CON=CON) + await pionex_trades_stream() + else: + CON = None + logging.warning("DATABASE NOT BEING USED, NO DATA WILL BE RECORDED") + await pionex_trades_stream() + + +if __name__ == '__main__': + START_TIME = round(datetime.now().timestamp()*1000) + + logging.info(f'Log FilePath: {LOG_FILEPATH}') + + logging.basicConfig( + force=True, + filename=LOG_FILEPATH, + level=logging.INFO, + format='%(asctime)s - %(levelname)s - %(message)s', + filemode='w' + ) + logging.info(f"STARTED: {START_TIME}") + + try: + asyncio.run(main()) + except KeyboardInterrupt: + logging.info("Stream stopped") \ No newline at end of file diff --git a/ws_rtds.py b/ws_rtds.py index 705138b..1325b38 100644 --- a/ws_rtds.py +++ b/ws_rtds.py @@ -12,7 +12,7 @@ import requests.packages.urllib3.util.connection as urllib3_cn # type: ignore from sqlalchemy import text import websockets from sqlalchemy.ext.asyncio import create_async_engine - +import valkey ### Allow only ipv4 ### def allowed_gai_family(): @@ -21,7 +21,11 @@ urllib3_cn.allowed_gai_family = allowed_gai_family ### Database ### USE_DB: bool = True +USE_VK: bool = True +VK_CHANNEL = 'poly_rtds_cl_btcusd' CON: AsyncContextManager | None = None +VAL_KEY = None + ### Logging ### LOG_FILEPATH: str = '/root/logs/Polymarket_RTDS.log' @@ -39,11 +43,13 @@ async def create_rtds_btcusd_table( logging.info("NO DB CONNECTION, SKIPPING Create Statements") else: if engine == 'mysql': + logging.info('Creating Table if Does Not Exist: poly_rtds_cl_btcusd') await CON.execute(text(""" - CREATE TABLE IF NOT EXISTS poly_rtds_btcusd_cl ( + CREATE TABLE IF NOT EXISTS poly_rtds_cl_btcusd ( + timestamp_arrival BIGINT, timestamp_msg BIGINT, timestamp_value BIGINT, - value DOUBLE, + value DOUBLE ); """)) await CON.commit() @@ -51,6 +57,7 @@ async def create_rtds_btcusd_table( raise ValueError('Only MySQL engine is implemented') async def insert_rtds_btcusd_table( + timestamp_arrival: int, timestamp_msg: int, timestamp_value: int, value: int, @@ -58,6 +65,7 @@ async def insert_rtds_btcusd_table( engine: str = 'mysql', # mysql | duckdb ) -> None: params={ + 'timestamp_arrival': timestamp_arrival, 'timestamp_msg': timestamp_msg, 'timestamp_value': timestamp_value, 'value': value, @@ -67,14 +75,16 @@ async def insert_rtds_btcusd_table( else: if engine == 'mysql': await CON.execute(text(""" - INSERT INTO poly_rtds_btcusd_cl + INSERT INTO poly_rtds_cl_btcusd ( + timestamp_arrival, timestamp_msg, timestamp_value, value ) VALUES ( + :timestamp_arrival, :timestamp_msg, :timestamp_value, :value @@ -91,7 +101,7 @@ async def insert_rtds_btcusd_table( async def rtds_stream(): global HIST_TRADES - async with websockets.connect(WSS_URL) as websocket: + async for websocket in websockets.connect(WSS_URL): logging.info(f"Connected to {WSS_URL}") subscribe_msg = { @@ -113,9 +123,26 @@ async def rtds_stream(): try: data = json.loads(message) if data['payload'].get('value', None) is not None: - print(f'🤑 BTC Chainlink Last Px: {data['payload']['value']:_.4f}; TS: {pd.to_datetime(data['timestamp'], unit='ms')}') + ts_arrival = round(datetime.now().timestamp()*1000) + print(f'🤑 BTC Chainlink Last Px: {data['payload']['value']:_.4f}; TS: {pd.to_datetime(data['payload']['timestamp'], unit='ms')}') + VAL_KEY_OBJ = json.dumps({ + 'timestamp_arrival': ts_arrival, + 'timestamp_msg': data['timestamp'], + 'timestamp_value': data['payload']['timestamp'], + 'value': data['payload']['value'], + }) + VAL_KEY.publish(VK_CHANNEL, VAL_KEY_OBJ) + VAL_KEY.set(VK_CHANNEL, VAL_KEY_OBJ) + await insert_rtds_btcusd_table( + CON=CON, + timestamp_arrival=ts_arrival, + timestamp_msg=data['timestamp'], + timestamp_value=data['payload']['timestamp'], + value=data['payload']['value'], + ) else: - logging.info(f'Initial or unexpected data struct, skipping: {data}') + # logging.info(f'Initial or unexpected data struct, skipping: {data}') + logging.info('Initial or unexpected data struct, skipping') continue except (json.JSONDecodeError, ValueError): logging.warning(f'Message not in JSON format, skipping: {message}') @@ -125,17 +152,28 @@ async def rtds_stream(): except websockets.ConnectionClosed as e: logging.error(f'Connection closed: {e}') logging.error(traceback.format_exc()) + continue except Exception as e: logging.error(f'Connection closed: {e}') logging.error(traceback.format_exc()) async def main(): + global VAL_KEY global CON + + if USE_VK: + VAL_KEY = valkey.Valkey(host='localhost', port=6379, db=0) + published_count = VAL_KEY.publish(VK_CHANNEL,f"Hola, starting to publish to valkey: {VK_CHANNEL} @ {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}") + logging.info(f"Valkey message published to {published_count} subscribers of {VK_CHANNEL}") + else: + VAL_KEY = None + logging.warning("VALKEY NOT BEING USED, NO DATA WILL BE PUBLISHED") if USE_DB: - engine = create_async_engine('mysql+asyncmy://root:pwd@localhost/mkt_maker') + engine = create_async_engine('mysql+asyncmy://root:pwd@localhost/polymarket') async with engine.connect() as CON: + await create_rtds_btcusd_table(CON=CON) await rtds_stream() else: CON = None -- 2.49.1 From 259ea93479ef947a878e70de7c37af6973a0c595 Mon Sep 17 00:00:00 2001 From: stevekeyharvey Date: Wed, 1 Apr 2026 17:37:19 +0000 Subject: [PATCH 3/5] saving --- database.ipynb | 400 ++++---- docker-compose.yml | 51 + main.py | 808 +++++++++++++++ modules/__pycache__/api.cpython-313.pyc | Bin 1730 -> 1737 bytes ng.py | 4 +- ng/Dockerfile | 19 + nicegui_modules/static/script.js | 15 +- order_entry.ipynb | 1197 +++++++++++++++++++---- requirements.txt | 22 + test.py | 63 ++ ws_binance.py | 62 +- ws_binance/Dockerfile | 19 + ws_clob.py | 142 ++- ws_clob/Dockerfile | 19 + ws_coinbase.py | 9 +- ws_coinbase/Dockerfile | 19 + ws_pionex.py | 5 +- ws_rtds.py | 13 +- ws_rtds/Dockerfile | 19 + ws_user.py | 431 ++++++++ ws_user/Dockerfile | 19 + 21 files changed, 2900 insertions(+), 436 deletions(-) create mode 100644 docker-compose.yml create mode 100644 main.py create mode 100644 ng/Dockerfile create mode 100644 requirements.txt create mode 100644 test.py create mode 100644 ws_binance/Dockerfile create mode 100644 ws_clob/Dockerfile create mode 100644 ws_coinbase/Dockerfile create mode 100644 ws_rtds/Dockerfile create mode 100644 ws_user.py create mode 100644 ws_user/Dockerfile diff --git a/database.ipynb b/database.ipynb index c41f74c..5c02c8e 100644 --- a/database.ipynb +++ b/database.ipynb @@ -14,7 +14,7 @@ }, { "cell_type": "code", - "execution_count": 6, + "execution_count": 11, "id": "f5040527", "metadata": {}, "outputs": [ @@ -36,195 +36,236 @@ " print(f\"Connection failed: {e}\") " ] }, - { - "cell_type": "code", - "execution_count": 7, - "id": "5c23110d", - "metadata": {}, - "outputs": [], - "source": [ - "query = '''\n", - "SELECT timestamp_msg,timestamp_value,`value` FROM poly_rtds_cl_btcusd;\n", - "'''" - ] - }, - { - "cell_type": "code", - "execution_count": 8, - "id": "a866e9ca", - "metadata": {}, - "outputs": [], - "source": [ - "df = pd.read_sql(query, con=engine)" - ] - }, { "cell_type": "code", "execution_count": 10, - "id": "954a3c3c", + "id": "72059b3f", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Connection successful\n" + ] + } + ], + "source": [ + "### MYSQL ###\n", + "engine_inter_storage = create_engine('mysql+pymysql://root:pwd@100.84.226.40/polymarket')\n", + "try:\n", + " with engine.connect() as conn:\n", + " print(\"Connection successful\")\n", + "except Exception as e:\n", + " print(f\"Connection failed: {e}\") " + ] + }, + { + "cell_type": "code", + "execution_count": 48, + "id": "b723a51f", "metadata": {}, "outputs": [], "source": [ - "df['ts'] = pd.to_datetime(df['timestamp_value'], unit='ms')" - ] - }, - { - "cell_type": "code", - "execution_count": 6, - "id": "f11fd680", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "1774752413939" - ] - }, - "execution_count": 6, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "round(datetime.now().timestamp()*1000)" - ] - }, - { - "cell_type": "code", - "execution_count": 11, - "id": "eadb0364", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "Timestamp('2026-03-29 03:22:03.145000')" - ] - }, - "execution_count": 11, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "pd.to_datetime(1774754523145, unit='ms')" - ] - }, - { - "cell_type": "code", - "execution_count": 14, - "id": "3fbf525c", - "metadata": {}, - "outputs": [], - "source": [ - "d = {'id': '1753563', 'question': 'Bitcoin Up or Down - March 29, 12:40AM-12:45AM ET', 'conditionId': '0x4cf6815a61939a9a0ee308772e727bbaf8e95803577aad7b9a9d3e028e37f13b', 'slug': 'btc-updown-5m-1774759200', 'resolutionSource': 'https://data.chain.link/streams/btc-usd', 'endDate': '2026-03-29T04:45:00Z', 'liquidity': '40412.2984', 'startDate': '2026-03-28T04:48:26.823506Z', 'image': 'https://polymarket-upload.s3.us-east-2.amazonaws.com/BTC+fullsize.png', 'icon': 'https://polymarket-upload.s3.us-east-2.amazonaws.com/BTC+fullsize.png', 'description': 'This market will resolve to \"Up\" if the Bitcoin price at the end of the time range specified in the title is greater than or equal to the price at the beginning of that range. Otherwise, it will resolve to \"Down\".\\nThe resolution source for this market is information from Chainlink, specifically the BTC/USD data stream available at https://data.chain.link/streams/btc-usd.\\nPlease note that this market is about the price according to Chainlink data stream BTC/USD, not according to other sources or spot markets.', 'outcomes': '[\"Up\", \"Down\"]', 'outcomePrices': '[\"0.505\", \"0.495\"]', 'volume': '1154.594883', 'active': True, 'closed': False, 'marketMakerAddress': '', 'createdAt': '2026-03-28T04:47:13.404208Z', 'updatedAt': '2026-03-29T04:38:45.64501Z', 'new': False, 'featured': False, 'archived': False, 'restricted': True, 'groupItemThreshold': '0', 'questionID': '0x7e86ce5e7ba0eb24758756db7c1443bf48041d08ac10364cf6771ef3f9c26733', 'enableOrderBook': True, 'orderPriceMinTickSize': 0.01, 'orderMinSize': 5, 'volumeNum': 1154.594883, 'liquidityNum': 40412.2984, 'endDateIso': '2026-03-29', 'startDateIso': '2026-03-28', 'hasReviewedDates': True, 'volume24hr': 1154.594883, 'volume1wk': 1154.594883, 'volume1mo': 1154.594883, 'volume1yr': 1154.594883, 'clobTokenIds': '[\"3132608671382208432230794800974499111421928258370863811882545679011841068490\", \"6273455409880805876304408793123549040642604317110995252288748628688958052125\"]', 'volume24hrClob': 1154.594883, 'volume1wkClob': 1154.594883, 'volume1moClob': 1154.594883, 'volume1yrClob': 1154.594883, 'volumeClob': 1154.594883, 'liquidityClob': 40412.2984, 'makerBaseFee': 1000, 'takerBaseFee': 1000, 'acceptingOrders': True, 'negRisk': False, 'ready': False, 'funded': False, 'acceptingOrdersTimestamp': '2026-03-28T04:47:21Z', 'cyom': False, 'competitive': 0.9999750006249843, 'pagerDutyNotificationEnabled': False, 'approved': True, 'rewardsMinSize': 50, 'rewardsMaxSpread': 4.5, 'spread': 0.01, 'lastTradePrice': 0.51, 'bestBid': 0.5, 'bestAsk': 0.51, 'automaticallyActive': True, 'clearBookOnStart': False, 'showGmpSeries': False, 'showGmpOutcome': False, 'manualActivation': False, 'negRiskOther': False, 'umaResolutionStatuses': '[]', 'pendingDeployment': False, 'deploying': False, 'rfqEnabled': False, 'eventStartTime': '2026-03-29T04:40:00Z', 'holdingRewardsEnabled': False, 'feesEnabled': True, 'requiresTranslation': False, 'makerRebatesFeeShareBps': 10000, 'feeType': 'crypto_fees', 'feeSchedule': {'exponent': 2, 'rate': 0.25, 'takerOnly': True, 'rebateRate': 0.2}}" - ] - }, - { - "cell_type": "code", - "execution_count": 15, - "id": "699031ee", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "{'id': '1753563',\n", - " 'question': 'Bitcoin Up or Down - March 29, 12:40AM-12:45AM ET',\n", - " 'conditionId': '0x4cf6815a61939a9a0ee308772e727bbaf8e95803577aad7b9a9d3e028e37f13b',\n", - " 'slug': 'btc-updown-5m-1774759200',\n", - " 'resolutionSource': 'https://data.chain.link/streams/btc-usd',\n", - " 'endDate': '2026-03-29T04:45:00Z',\n", - " 'liquidity': '40412.2984',\n", - " 'startDate': '2026-03-28T04:48:26.823506Z',\n", - " 'image': 'https://polymarket-upload.s3.us-east-2.amazonaws.com/BTC+fullsize.png',\n", - " 'icon': 'https://polymarket-upload.s3.us-east-2.amazonaws.com/BTC+fullsize.png',\n", - " 'description': 'This market will resolve to \"Up\" if the Bitcoin price at the end of the time range specified in the title is greater than or equal to the price at the beginning of that range. Otherwise, it will resolve to \"Down\".\\nThe resolution source for this market is information from Chainlink, specifically the BTC/USD data stream available at https://data.chain.link/streams/btc-usd.\\nPlease note that this market is about the price according to Chainlink data stream BTC/USD, not according to other sources or spot markets.',\n", - " 'outcomes': '[\"Up\", \"Down\"]',\n", - " 'outcomePrices': '[\"0.505\", \"0.495\"]',\n", - " 'volume': '1154.594883',\n", - " 'active': True,\n", - " 'closed': False,\n", - " 'marketMakerAddress': '',\n", - " 'createdAt': '2026-03-28T04:47:13.404208Z',\n", - " 'updatedAt': '2026-03-29T04:38:45.64501Z',\n", - " 'new': False,\n", - " 'featured': False,\n", - " 'archived': False,\n", - " 'restricted': True,\n", - " 'groupItemThreshold': '0',\n", - " 'questionID': '0x7e86ce5e7ba0eb24758756db7c1443bf48041d08ac10364cf6771ef3f9c26733',\n", - " 'enableOrderBook': True,\n", - " 'orderPriceMinTickSize': 0.01,\n", - " 'orderMinSize': 5,\n", - " 'volumeNum': 1154.594883,\n", - " 'liquidityNum': 40412.2984,\n", - " 'endDateIso': '2026-03-29',\n", - " 'startDateIso': '2026-03-28',\n", - " 'hasReviewedDates': True,\n", - " 'volume24hr': 1154.594883,\n", - " 'volume1wk': 1154.594883,\n", - " 'volume1mo': 1154.594883,\n", - " 'volume1yr': 1154.594883,\n", - " 'clobTokenIds': '[\"3132608671382208432230794800974499111421928258370863811882545679011841068490\", \"6273455409880805876304408793123549040642604317110995252288748628688958052125\"]',\n", - " 'volume24hrClob': 1154.594883,\n", - " 'volume1wkClob': 1154.594883,\n", - " 'volume1moClob': 1154.594883,\n", - " 'volume1yrClob': 1154.594883,\n", - " 'volumeClob': 1154.594883,\n", - " 'liquidityClob': 40412.2984,\n", - " 'makerBaseFee': 1000,\n", - " 'takerBaseFee': 1000,\n", - " 'acceptingOrders': True,\n", - " 'negRisk': False,\n", - " 'ready': False,\n", - " 'funded': False,\n", - " 'acceptingOrdersTimestamp': '2026-03-28T04:47:21Z',\n", - " 'cyom': False,\n", - " 'competitive': 0.9999750006249843,\n", - " 'pagerDutyNotificationEnabled': False,\n", - " 'approved': True,\n", - " 'rewardsMinSize': 50,\n", - " 'rewardsMaxSpread': 4.5,\n", - " 'spread': 0.01,\n", - " 'lastTradePrice': 0.51,\n", - " 'bestBid': 0.5,\n", - " 'bestAsk': 0.51,\n", - " 'automaticallyActive': True,\n", - " 'clearBookOnStart': False,\n", - " 'showGmpSeries': False,\n", - " 'showGmpOutcome': False,\n", - " 'manualActivation': False,\n", - " 'negRiskOther': False,\n", - " 'umaResolutionStatuses': '[]',\n", - " 'pendingDeployment': False,\n", - " 'deploying': False,\n", - " 'rfqEnabled': False,\n", - " 'eventStartTime': '2026-03-29T04:40:00Z',\n", - " 'holdingRewardsEnabled': False,\n", - " 'feesEnabled': True,\n", - " 'requiresTranslation': False,\n", - " 'makerRebatesFeeShareBps': 10000,\n", - " 'feeType': 'crypto_fees',\n", - " 'feeSchedule': {'exponent': 2,\n", - " 'rate': 0.25,\n", - " 'takerOnly': True,\n", - " 'rebateRate': 0.2}}" - ] - }, - "execution_count": 15, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "d" + "# with engine.connect() as conn:\n", + "# print(\"Connection successful\")\n", + "# sql = text(\"TRUNCATE TABLE coinbase_btcusd_trades;\")\n", + "# conn.execute(sql)" ] }, { "cell_type": "code", "execution_count": null, - "id": "a620fa17", + "id": "5c23110d", + "metadata": {}, + "outputs": [], + "source": [ + "q_binance = '''\n", + "SELECT * FROM binance_btcusd_trades;\n", + "'''\n", + "q_coinbase = '''\n", + "SELECT * FROM coinbase_btcusd_trades;\n", + "'''\n", + "q_rtds = '''\n", + "SELECT * FROM poly_rtds_cl_btcusd;\n", + "'''\n", + "q_clob = '''\n", + "SELECT * FROM poly_btcusd_trades;\n", + "'''" + ] + }, + { + "cell_type": "code", + "execution_count": 24, + "id": "a866e9ca", + "metadata": {}, + "outputs": [], + "source": [ + "# df_binance = pd.read_sql(q_binance, con=engine)\n", + "df_coinbase = pd.read_sql(q_coinbase, con=engine)\n", + "df_rtds = pd.read_sql(q_rtds, con=engine)\n", + "df_clob = pd.read_sql(q_clob, con=engine)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "954a3c3c", + "metadata": {}, + "outputs": [], + "source": [ + "# df_binance['timestamp_arrival'] = pd.to_datetime(df_binance['timestamp_arrival'], unit='ms')\n", + "df_coinbase['timestamp_arrival'] = pd.to_datetime(df_coinbase['timestamp_arrival'], unit='ms')\n", + "df_rtds['timestamp_arrival'] = pd.to_datetime(df_rtds['timestamp_arrival'], unit='ms')\n", + "df_clob['timestamp_arrival'] = pd.to_datetime(df_clob['timestamp_arrival'], unit='ms')" + ] + }, + { + "cell_type": "code", + "execution_count": 57, + "id": "50c6339f", + "metadata": {}, + "outputs": [], + "source": [ + "def copy_table_data_btw_servers(df, table_name, engine_destination) -> None:\n", + " rows_imported = df.to_sql(name=table_name, con=engine_destination, if_exists='append')\n", + " if rows_imported == len(df):\n", + " print(f'SUCCESS: COPIED {rows_imported} to table \"{table_name}\" on INTERSERVER_STORAGE')\n", + " else:\n", + " raise ValueError(f'FAILED: COPIED {rows_imported} rows to table {table_name} on INTERSERVER_STORAGE; EXPECTED {len(df)}')\n", + " \n", + "def truncate_table(engine, table):\n", + " with engine.connect() as conn:\n", + " sql = text(f\"TRUNCATE TABLE {table};\")\n", + " conn.execute(sql)\n", + " conn.commit()" + ] + }, + { + "cell_type": "code", + "execution_count": 61, + "id": "d0399a96", + "metadata": {}, + "outputs": [], + "source": [ + "def backup_all_tables(engine_origin, engine_destination, tables_to_copy):\n", + " for t in tables_to_copy:\n", + " q = f'''\n", + " SELECT * FROM {t};\n", + " '''\n", + " df = pd.read_sql(q, con=engine_origin)\n", + " print('-------------------------------------------------------------------------')\n", + " print(f'Loaded Data for Table: {t}...Attempting to Transfer to Destination Server')\n", + " copy_table_data_btw_servers(\n", + " df=df,\n", + " table_name=t,\n", + " engine_destination=engine_destination,\n", + " )\n", + " print(f'Attempting to Truncate Table: {t}...')\n", + " \n", + " ### FOR REALTIME - instead of truncate, need to delete rows using a conditon (e.g. delete all rows <= max timestamp arrival in the DF)\n", + " \n", + " truncate_table(\n", + " engine=engine_origin,\n", + " table=t,\n", + " )\n", + " print(f'...Successfully Truncated Table: {t}')\n", + " print(f'Done Transferring Data for Table: {t}')\n", + " \n" + ] + }, + { + "cell_type": "code", + "execution_count": 59, + "id": "0de1629a", + "metadata": {}, + "outputs": [], + "source": [ + "tables_to_copy = [\n", + " # 'binance_btcusd_trades',\n", + " # 'coinbase_btcusd_trades',\n", + " 'poly_btcusd_trades',\n", + " 'poly_rtds_cl_btcusd',\n", + " # 'user_stream_orders',\n", + " # 'user_stream_trades',\n", + "]" + ] + }, + { + "cell_type": "code", + "execution_count": 60, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "-------------------------------------------------------------------------\n", + "Loaded Data for Table: poly_btcusd_trades...Attempting to Transfer to Destination Server\n", + "SUCCESS: COPIED 720568 to table \"poly_btcusd_trades\" on INTERSERVER_STORAGE\n", + "Attempting to Truncate Table: poly_btcusd_trades...\n", + "...Successfully Truncated Table: poly_btcusd_trades\n", + "Done Transferring Data for Table: poly_btcusd_trades\n", + "-------------------------------------------------------------------------\n", + "Loaded Data for Table: poly_rtds_cl_btcusd...Attempting to Transfer to Destination Server\n", + "SUCCESS: COPIED 73771 to table \"poly_rtds_cl_btcusd\" on INTERSERVER_STORAGE\n", + "Attempting to Truncate Table: poly_rtds_cl_btcusd...\n", + "...Successfully Truncated Table: poly_rtds_cl_btcusd\n", + "Done Transferring Data for Table: poly_rtds_cl_btcusd\n" + ] + } + ], + "source": [ + "backup_all_tables(\n", + " engine_origin=engine,\n", + " engine_destination=engine_inter_storage,\n", + " tables_to_copy=tables_to_copy\n", + ")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "cd0b40d2", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "SUCCESS COPIED 326007 to binance_btcusd_trades to INTERSERVER_STORAGE\n" + ] + } + ], + "source": [] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "48b47799", + "metadata": {}, + "outputs": [], + "source": [] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "ad030f88", + "metadata": {}, + "outputs": [], + "source": [] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "cafc5060", "metadata": {}, "outputs": [], "source": [] @@ -232,7 +273,6 @@ { "cell_type": "code", "execution_count": null, - "id": "2071f014", "metadata": {}, "outputs": [], "source": [] diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..a9401d3 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,51 @@ +services: + ws_binance: + container_name: ws_binance + restart: "unless-stopped" + build: + context: ./ + dockerfile: ./ws_binance/Dockerfile + volumes: + - /home/ubuntu/data:/home/ubuntu/data:rw # Read-write access to data + - /home/ubuntu/logs:/home/ubuntu/logs:rw # Read-write access to data + network_mode: "host" + ws_clob: + container_name: ws_clob + restart: "unless-stopped" + build: + context: ./ + dockerfile: ./ws_clob/Dockerfile + volumes: + - /home/ubuntu/data:/home/ubuntu/data:rw # Read-write access to data + - /home/ubuntu/logs:/home/ubuntu/logs:rw # Read-write access to data + network_mode: "host" + ws_rtds: + container_name: ws_rtds + restart: "unless-stopped" + build: + context: ./ + dockerfile: ./ws_rtds/Dockerfile + volumes: + - /home/ubuntu/data:/home/ubuntu/data:rw # Read-write access to data + - /home/ubuntu/logs:/home/ubuntu/logs:rw # Read-write access to data + network_mode: "host" + ws_user: + container_name: ws_user + restart: "unless-stopped" + build: + context: ./ + dockerfile: ./ws_user/Dockerfile + volumes: + - /home/ubuntu/data:/home/ubuntu/data:rw # Read-write access to data + - /home/ubuntu/logs:/home/ubuntu/logs:rw # Read-write access to data + network_mode: "host" + ng: + container_name: ng + restart: "unless-stopped" + build: + context: ./ + dockerfile: ./ng/Dockerfile + volumes: + - /home/ubuntu/data:/home/ubuntu/data:rw # Read-write access to data + - /home/ubuntu/logs:/home/ubuntu/logs:rw # Read-write access to data + network_mode: "host" \ No newline at end of file diff --git a/main.py b/main.py new file mode 100644 index 0000000..6ae4a8a --- /dev/null +++ b/main.py @@ -0,0 +1,808 @@ +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 +) +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 + + +### 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.1 # 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.02 +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": + logging.info(f'Cancel request failed b/c already matched: {cxl_resp}') + return False + 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}') + LOCAL_ACTIVE_ORDERS.pop(idx) + return False + 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) + + ### Submit orders to flatten outstanding balances ### + if 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.01, + size=up, + side=SELL, + )] + ) + if 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.01, + 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): + 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=False, + ), + ) + + ### POST + response = CLIENT.post_orders(orders) + for idx, d in enumerate(response): + if d['errorMsg'] == '': + d['token_id'] = OrderArgs_list[idx].token_id + d['price'] = OrderArgs_list[idx].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) + + if 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) + else: + raise ValueError(f'Order entry failed: {d}') + + logging.info(f'Order Posted Resp: {response}') + print(f'Order Posted Resp: {response}') + + +### Routes ### +async def no_orders_no_positions_route(): + 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 = POLY_CLOB.get('token_id_up', None) if slope_side=='UP' else POLY_CLOB.get('token_id_down', None) + + + ### Order Entry ### + px = float(POLY_CLOB['price'])+0.01 + order = Custom_OrderArgs( + token_id=token_id, + price=px, + size=DEFAULT_ORDER_SIZE, + side=BUY, + max_price = px + CHASE_TO_BUY_CENTS + ) + + ### ADD CHECK FOR MKT MOVED AWAY FROM OPPORTNITY ### + + if ORDER_LOCK: + logging.info(f'BUY ORDER BLOCKED BY LOCK: {order}') + + else: + logging.info(f'Attempting BUY Order {order}') + await post_order( + CLIENT = CLIENT, + tick_size = POLY_CLOB['tick_size'], + neg_risk = POLY_CLOB['neg_risk'], + OrderArgs_list = [order] + ) + # ORDER_LOCK = ORDER_LOCK + 1 + +async def active_orders_no_positions_route(): + if len(LOCAL_ACTIVE_ORDERS) > 2: + logging.critical('More than two active orders, shutting down') + await kill_algo() + b_c = 0 + s_c = 0 + for o in LOCAL_ACTIVE_ORDERS: + if o['side'] == 'BUY': + b_c = b_c + 1 + elif o['side'] == 'SELL': + s_c = s_c + 1 + if (b_c > 1) or (s_c > 1): + logging.critical(f'More than one active buy or more than one active sell: 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') + + elif o.get('status').upper() == 'FAILED': + raise ValueError(f'Trade FAILED after matching: {o}') + else: + orig_px = float(o['price']) + orig_size = float(o['size']) + if o['side'] == 'BUY': + 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 above buy order px: {orig_px:.2f})") + 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})") + + order_matched = await cancel_single_order_by_id(CLIENT=CLIENT, order_id=o['orderID']) + + if order_matched: + o['status'] = 'MATCHED' + else: + px = orig_px+0.01 + + 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=px, + size=orig_size, + side=BUY, + max_price=o['max_price'] + + )] + ) + else: + await cancel_single_order_by_id(CLIENT=CLIENT, order_id=o['orderID']) + 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(): + 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('...algo killed') + raise Exception('Algo Killed') + +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')) + + 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}') + 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}') + 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() + logging.info(f'Updated Order Status: {o['status']} --> {order_status}; {o['orderID']}') + # LOCAL_ACTIVE_ORDERS[idx]['status'] = order_status + # if order_status == 'MATCHED': + # LOCAL_ACTIVE_ORDERS.pop(idx) + + # token_id = user_order['asset_id'] + # current_balance = float(LOCAL_TOKEN_BALANCES.get(token_id, 0.00)) + + # if user_order['side'] == 'BUY': + # size = float(user_order['size_matched']) + # else: + # size = float(user_order['size_matched']) * -1 + + # LOCAL_TOKEN_BALANCES[token_id] = current_balance + size + + # # px = user_order['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!') + if order_status == 'CANCELED': + LOCAL_ACTIVE_ORDERS.pop(idx) + logging.info('Order Canceled') + else: + logging.info('Order Live or Trade Awaiting Confirm') + + ### UPDATES CAN COME THRU EITHER ORDER OR TRADE CHANNELS - NEED TO UPDATE TO HANDLE TRADE CHANNLE + + 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_no_positions_route() + + ### 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_no_positions_route') + await no_orders_active_positions_route() + + ### Open Orders and Open Positions Route ### + else: + print('ROUTE: active_orders_active_positions_route') + 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()) + \ No newline at end of file diff --git a/modules/__pycache__/api.cpython-313.pyc b/modules/__pycache__/api.cpython-313.pyc index ddb0ad9364f5e29d127c03da2f6ac9beeb18988b..3f48f5ca76de6f8a7d824c0c53b8cbd84ac46786 100644 GIT binary patch delta 41 vcmX@ady<#?GcPX}0}u#@oZQGQ!o+W+pOK%Ns$ZH^npaY)@3L8)X*LT0=y409 delta 34 ocmX@fdx)3&GcPX}0}yOIc5EZJ2os04eo=mYiGIpvJ*L?#0I@I%761SM diff --git a/ng.py b/ng.py index a18693f..7cd0a31 100644 --- a/ng.py +++ b/ng.py @@ -23,7 +23,7 @@ ALLOW_BODY_SCROLL: bool = True LOOKBACK: int = 60 LOOKBACK_RT_TV_MAX_POINTS: int = 300 REFRESH_INTERVAL_SEC: int = 10 -REFRESH_INTERVAL_RT_SEC: int = 0.1 +REFRESH_INTERVAL_RT_SEC: int = 1/30 ENGINE = create_engine('mysql+pymysql://root:pwd@localhost/polymarket') VALKEY_R = valkey.Valkey(host='localhost', port=6379, db=0, decode_responses=True) @@ -51,7 +51,7 @@ def root(): async def update_tv(): series_update = json.loads(VALKEY_R.get('poly_rtds_cl_btcusd')) - series_update_b = json.loads(VALKEY_R.get('poly_coinbase_btcusd')) + series_update_b = json.loads(VALKEY_R.get('poly_binance_btcusd')) series_update_c = json.loads(VALKEY_R.get('poly_5min_btcusd')) timestamp = round( ( series_update['timestamp_arrival'] / 1000 ) , 2) timestamp_b = round( ( series_update_b['timestamp_arrival'] / 1000 ) , 2) diff --git a/ng/Dockerfile b/ng/Dockerfile new file mode 100644 index 0000000..042d9b9 --- /dev/null +++ b/ng/Dockerfile @@ -0,0 +1,19 @@ +FROM python:3.13-slim + +RUN apt-get update && \ + apt-get install -y build-essential + +RUN gcc --version +RUN rm -rf /var/lib/apt/lists/* + +WORKDIR /app + +COPY requirements.txt . + +RUN pip install --no-cache-dir -r requirements.txt + +COPY . . + +# Finally, run gunicorn. +CMD [ "python", "ng.py"] +# CMD [ "gunicorn", "--workers=5", "--threads=1", "-b 0.0.0.0:8000", "app:server"] \ No newline at end of file diff --git a/nicegui_modules/static/script.js b/nicegui_modules/static/script.js index add9035..aedf603 100644 --- a/nicegui_modules/static/script.js +++ b/nicegui_modules/static/script.js @@ -59,10 +59,11 @@ async function create_tv() { secondsVisible: true // Optional: show seconds }, rightPriceScale: { - visible: true + visible: true, + autoScale: true }, leftPriceScale: { - visible: true + visible: true }, layout: { @@ -102,10 +103,12 @@ async function create_tv() { window.lineSeries_c = chart.addSeries(LightweightCharts.LineSeries, { color: '#ea0707', priceScaleId: 'left', - priceRange: { - minValue: 0, - maxValue: 1 - }, + autoscaleInfoProvider: () => ({ + priceRange: { + minValue: 0.0, + maxValue: 1.0 + } + }) // topColor: '#94fcdf', // bottomColor: 'rgba(112, 171, 249, 0.28)', // invertFilledArea: false diff --git a/order_entry.ipynb b/order_entry.ipynb index 18da8a8..7028783 100644 --- a/order_entry.ipynb +++ b/order_entry.ipynb @@ -2,7 +2,7 @@ "cells": [ { "cell_type": "code", - "execution_count": 2, + "execution_count": 134, "id": "c0bfb3b5", "metadata": {}, "outputs": [], @@ -15,13 +15,13 @@ "import json\n", "from dataclasses import dataclass\n", "\n", - "from py_clob_client.clob_types import OrderArgs, OrderType, PostOrdersArgs, PartialCreateOrderOptions\n", + "from py_clob_client.clob_types import OrderArgs, OrderType, PostOrdersArgs, PartialCreateOrderOptions, BalanceAllowanceParams\n", "from py_clob_client.order_builder.constants import BUY, SELL\n" ] }, { "cell_type": "code", - "execution_count": 3, + "execution_count": 135, "id": "7d7dc787", "metadata": {}, "outputs": [], @@ -49,17 +49,17 @@ }, { "cell_type": "code", - "execution_count": 4, + "execution_count": 136, "id": "c3e07e21", "metadata": {}, "outputs": [ { "data": { "text/plain": [ - "Timestamp('2026-03-29 05:05:00')" + "Timestamp('2026-03-31 05:45:00')" ] }, - "execution_count": 4, + "execution_count": 136, "metadata": {}, "output_type": "execute_result" } @@ -74,22 +74,123 @@ }, { "cell_type": "code", - "execution_count": 5, + "execution_count": 137, + "id": "10671da4", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "{'id': '1787714',\n", + " 'question': 'Bitcoin Up or Down - March 31, 1:45AM-1:50AM ET',\n", + " 'conditionId': '0xd1773b412dacad884c202a7b14f0197918b1e22028ce2b5737fbd659bbe150f0',\n", + " 'slug': 'btc-updown-5m-1774935900',\n", + " 'resolutionSource': 'https://data.chain.link/streams/btc-usd',\n", + " 'endDate': '2026-03-31T05:50:00Z',\n", + " 'liquidity': '12631.9812',\n", + " 'startDate': '2026-03-30T05:53:51.811773Z',\n", + " 'image': 'https://polymarket-upload.s3.us-east-2.amazonaws.com/BTC+fullsize.png',\n", + " 'icon': 'https://polymarket-upload.s3.us-east-2.amazonaws.com/BTC+fullsize.png',\n", + " 'description': 'This market will resolve to \"Up\" if the Bitcoin price at the end of the time range specified in the title is greater than or equal to the price at the beginning of that range. Otherwise, it will resolve to \"Down\".\\nThe resolution source for this market is information from Chainlink, specifically the BTC/USD data stream available at https://data.chain.link/streams/btc-usd.\\nPlease note that this market is about the price according to Chainlink data stream BTC/USD, not according to other sources or spot markets.',\n", + " 'outcomes': '[\"Up\", \"Down\"]',\n", + " 'outcomePrices': '[\"0.875\", \"0.125\"]',\n", + " 'volume': '94249.88157999996',\n", + " 'active': True,\n", + " 'closed': False,\n", + " 'marketMakerAddress': '',\n", + " 'createdAt': '2026-03-30T05:52:34.18484Z',\n", + " 'updatedAt': '2026-03-31T05:48:51.799961Z',\n", + " 'new': False,\n", + " 'featured': False,\n", + " 'archived': False,\n", + " 'restricted': True,\n", + " 'groupItemThreshold': '0',\n", + " 'questionID': '0x33d208062bed0d8fcf68d2f3899e528f103ad7e7763efc763f71c065ea6c842c',\n", + " 'enableOrderBook': True,\n", + " 'orderPriceMinTickSize': 0.01,\n", + " 'orderMinSize': 5,\n", + " 'volumeNum': 94249.88157999996,\n", + " 'liquidityNum': 12631.9812,\n", + " 'endDateIso': '2026-03-31',\n", + " 'startDateIso': '2026-03-30',\n", + " 'hasReviewedDates': True,\n", + " 'volume24hr': 93644.03624199987,\n", + " 'volume1wk': 93644.03624199987,\n", + " 'volume1mo': 93644.03624199987,\n", + " 'volume1yr': 93644.03624199987,\n", + " 'clobTokenIds': '[\"13157292356296687506747919717798752029699544499054519087985411865141996614822\", \"70507961363566124468475538524172170043725846352735387705515911177933084557518\"]',\n", + " 'volume24hrClob': 93644.03624199987,\n", + " 'volume1wkClob': 93644.03624199987,\n", + " 'volume1moClob': 93644.03624199987,\n", + " 'volume1yrClob': 93644.03624199987,\n", + " 'volumeClob': 94249.88157999996,\n", + " 'liquidityClob': 12631.9812,\n", + " 'makerBaseFee': 1000,\n", + " 'takerBaseFee': 1000,\n", + " 'acceptingOrders': True,\n", + " 'negRisk': False,\n", + " 'ready': False,\n", + " 'funded': False,\n", + " 'acceptingOrdersTimestamp': '2026-03-30T05:52:46Z',\n", + " 'cyom': False,\n", + " 'competitive': 0.8767123287671234,\n", + " 'pagerDutyNotificationEnabled': False,\n", + " 'approved': True,\n", + " 'rewardsMinSize': 50,\n", + " 'rewardsMaxSpread': 4.5,\n", + " 'spread': 0.01,\n", + " 'lastTradePrice': 0.89,\n", + " 'bestBid': 0.87,\n", + " 'bestAsk': 0.88,\n", + " 'automaticallyActive': True,\n", + " 'clearBookOnStart': False,\n", + " 'showGmpSeries': False,\n", + " 'showGmpOutcome': False,\n", + " 'manualActivation': False,\n", + " 'negRiskOther': False,\n", + " 'umaResolutionStatuses': '[]',\n", + " 'pendingDeployment': False,\n", + " 'deploying': False,\n", + " 'rfqEnabled': False,\n", + " 'eventStartTime': '2026-03-31T05:45:00Z',\n", + " 'holdingRewardsEnabled': False,\n", + " 'feesEnabled': True,\n", + " 'requiresTranslation': False,\n", + " 'makerRebatesFeeShareBps': 10000,\n", + " 'feeType': 'crypto_fees_v2',\n", + " 'feeSchedule': {'exponent': 1,\n", + " 'rate': 0.072,\n", + " 'takerOnly': True,\n", + " 'rebateRate': 0.2}}" + ] + }, + "execution_count": 137, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "market" + ] + }, + { + "cell_type": "code", + "execution_count": 132, "id": "5ba43ffc", "metadata": {}, "outputs": [ { "data": { "text/plain": [ - "{'Up': '100363012413042717231742069495969784533240629092182259086983062365921258452690',\n", - " 'Down': '33447700958945117801119737006959954665425600820162218508565813029801829841900',\n", + "{'Up': '29663568421665501825278796284809925893978140634751674792176179244450686939029',\n", + " 'Down': '17234806616394620691850452772328817031586809400835664344689152810552758258009',\n", " 'isActive': False,\n", " 'MinTickSize': 0.01,\n", " 'isNegRisk': False,\n", - " 'ConditionId': '0x7e64102ae25311ab43ed27d0fe2c614dd9deefdd1324012b91f0814b530cb7db'}" + " 'ConditionId': '0x1778b4a38f2ce99260f9d3d0d78729d37fbd24439e06e6c92aad429439e0f7b2'}" ] }, - "execution_count": 5, + "execution_count": 132, "metadata": {}, "output_type": "execute_result" } @@ -100,7 +201,7 @@ }, { "cell_type": "code", - "execution_count": 7, + "execution_count": 124, "id": "5d356d3b", "metadata": {}, "outputs": [ @@ -109,7 +210,7 @@ "output_type": "stream", "text": [ "creating client...\n", - "You've made 44 trades\n", + "You've made 55 trades\n", "client created successfully!\n" ] } @@ -120,7 +221,94 @@ }, { "cell_type": "code", - "execution_count": 8, + "execution_count": 128, + "id": "de5ccc3a", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "7.6829" + ] + }, + "execution_count": 128, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "collateral = client.get_balance_allowance(\n", + " BalanceAllowanceParams(\n", + " asset_type='CONDITIONAL',\n", + " token_id='29663568421665501825278796284809925893978140634751674792176179244450686939029',\n", + " )\n", + ")\n", + "a = collateral['balance']\n", + "a = int(a) / 1_000_000\n", + "a" + ] + }, + { + "cell_type": "code", + "execution_count": 129, + "id": "40323f8d", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "{'balance': '7682900',\n", + " 'allowances': {'0x4bFb41d5B3570DeFd03C39a9A4D8dE6Bd8B8982E': '115792089237316195398462578067141184799968521174335529155754622898352762650625',\n", + " '0xC5d563A36AE78145C45a50134d48A1215220f80a': '115792089237316195398462578067141184799968521174335529155754622898352762650625',\n", + " '0xd91E80cF2E7be2e162c6513ceD06f1dD0dA35296': '115792089237316195398462578067141184799968521174335529155754622898352762650625'}}" + ] + }, + "execution_count": 129, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "collateral" + ] + }, + { + "cell_type": "code", + "execution_count": 119, + "id": "45fe4f53", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "{'balance': '24494337',\n", + " 'allowances': {'0x4bFb41d5B3570DeFd03C39a9A4D8dE6Bd8B8982E': '115792089237316195423570985008687907853269984665640564039457584007912981179951',\n", + " '0xC5d563A36AE78145C45a50134d48A1215220f80a': '115792089237316195423570985008687907853269984665640564039457584007913129639935',\n", + " '0xd91E80cF2E7be2e162c6513ceD06f1dD0dA35296': '115792089237316195423570985008687907853269984665640564039457584007913129639935'}}" + ] + }, + "execution_count": 119, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "collateral" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "6cf19a23", + "metadata": {}, + "outputs": [], + "source": [ + "client.cancel(order_id=)" + ] + }, + { + "cell_type": "code", + "execution_count": 106, "id": "bebb53eb", "metadata": {}, "outputs": [ @@ -128,7 +316,7 @@ "name": "stdout", "output_type": "stream", "text": [ - "{'price': '0.01', 'side': 'SELL'}\n" + "{'price': '0.3', 'side': 'BUY'}\n" ] } ], @@ -139,40 +327,57 @@ }, { "cell_type": "code", - "execution_count": null, - "id": "bae5e6a9", + "execution_count": 109, + "id": "1c5a6c9a", "metadata": {}, "outputs": [], - "source": [] + "source": [ + "d = client.cancel_all()" + ] }, { "cell_type": "code", - "execution_count": 9, + "execution_count": null, + "id": "7bc2b7e7", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "False" + ] + }, + "execution_count": 111, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "bool(d['not_canceled'])" + ] + }, + { + "cell_type": "code", + "execution_count": null, "id": "52c0c38a", "metadata": {}, "outputs": [ { - "ename": "PolyApiException", - "evalue": "PolyApiException[status_code=403, error_message={'error': 'Trading restricted in your region, please refer to available regions - https://docs.polymarket.com/developers/CLOB/geoblock'}]", - "output_type": "error", - "traceback": [ - "\u001b[31m---------------------------------------------------------------------------\u001b[39m", - "\u001b[31mPolyApiException\u001b[39m Traceback (most recent call last)", - "\u001b[36mCell\u001b[39m\u001b[36m \u001b[39m\u001b[32mIn[9]\u001b[39m\u001b[32m, line 1\u001b[39m\n\u001b[32m----> \u001b[39m\u001b[32m1\u001b[39m response = \u001b[43mclient\u001b[49m\u001b[43m.\u001b[49m\u001b[43mpost_orders\u001b[49m\u001b[43m(\u001b[49m\u001b[43m[\u001b[49m\n\u001b[32m 2\u001b[39m \u001b[43m \u001b[49m\u001b[43mPostOrdersArgs\u001b[49m\u001b[43m(\u001b[49m\n\u001b[32m 3\u001b[39m \u001b[43m \u001b[49m\u001b[43morder\u001b[49m\u001b[43m=\u001b[49m\u001b[43mclient\u001b[49m\u001b[43m.\u001b[49m\u001b[43mcreate_order\u001b[49m\u001b[43m(\u001b[49m\n\u001b[32m 4\u001b[39m \u001b[43m \u001b[49m\u001b[43morder_args\u001b[49m\u001b[43m=\u001b[49m\u001b[43mOrderArgs\u001b[49m\u001b[43m(\u001b[49m\n\u001b[32m 5\u001b[39m \u001b[43m \u001b[49m\u001b[43mtoken_id\u001b[49m\u001b[43m=\u001b[49m\u001b[43mmarket_details\u001b[49m\u001b[43m[\u001b[49m\u001b[33;43m'\u001b[39;49m\u001b[33;43mUp\u001b[39;49m\u001b[33;43m'\u001b[39;49m\u001b[43m]\u001b[49m\u001b[43m,\u001b[49m\n\u001b[32m 6\u001b[39m \u001b[43m \u001b[49m\u001b[43mprice\u001b[49m\u001b[43m=\u001b[49m\u001b[32;43m0.20\u001b[39;49m\u001b[43m,\u001b[49m\n\u001b[32m 7\u001b[39m \u001b[43m \u001b[49m\u001b[43msize\u001b[49m\u001b[43m=\u001b[49m\u001b[32;43m10\u001b[39;49m\u001b[43m,\u001b[49m\n\u001b[32m 8\u001b[39m \u001b[43m \u001b[49m\u001b[43mside\u001b[49m\u001b[43m=\u001b[49m\u001b[43mBUY\u001b[49m\u001b[43m,\u001b[49m\n\u001b[32m 9\u001b[39m \u001b[43m \u001b[49m\u001b[43m)\u001b[49m\u001b[43m,\u001b[49m\n\u001b[32m 10\u001b[39m \u001b[43m \u001b[49m\u001b[43moptions\u001b[49m\u001b[43m=\u001b[49m\u001b[43mPartialCreateOrderOptions\u001b[49m\u001b[43m(\u001b[49m\n\u001b[32m 11\u001b[39m \u001b[43m \u001b[49m\u001b[43mtick_size\u001b[49m\u001b[43m=\u001b[49m\u001b[38;5;28;43mstr\u001b[39;49m\u001b[43m(\u001b[49m\u001b[43mmarket_details\u001b[49m\u001b[43m[\u001b[49m\u001b[33;43m'\u001b[39;49m\u001b[33;43mMinTickSize\u001b[39;49m\u001b[33;43m'\u001b[39;49m\u001b[43m]\u001b[49m\u001b[43m)\u001b[49m\u001b[43m,\u001b[49m\n\u001b[32m 12\u001b[39m \u001b[43m \u001b[49m\u001b[43mneg_risk\u001b[49m\u001b[43m=\u001b[49m\u001b[43mmarket_details\u001b[49m\u001b[43m[\u001b[49m\u001b[33;43m'\u001b[39;49m\u001b[33;43misNegRisk\u001b[39;49m\u001b[33;43m'\u001b[39;49m\u001b[43m]\u001b[49m\n\u001b[32m 13\u001b[39m \u001b[43m \u001b[49m\u001b[43m)\u001b[49m\u001b[43m,\u001b[49m\n\u001b[32m 14\u001b[39m \u001b[43m \u001b[49m\u001b[43m)\u001b[49m\u001b[43m,\u001b[49m\n\u001b[32m 15\u001b[39m \u001b[43m \u001b[49m\u001b[43morderType\u001b[49m\u001b[43m=\u001b[49m\u001b[43mOrderType\u001b[49m\u001b[43m.\u001b[49m\u001b[43mGTC\u001b[49m\u001b[43m,\u001b[49m\n\u001b[32m 16\u001b[39m \u001b[43m \u001b[49m\u001b[43mpostOnly\u001b[49m\u001b[43m=\u001b[49m\u001b[38;5;28;43;01mFalse\u001b[39;49;00m\u001b[43m,\u001b[49m\n\u001b[32m 17\u001b[39m \u001b[43m \u001b[49m\u001b[43m)\u001b[49m\u001b[43m,\u001b[49m\n\u001b[32m 18\u001b[39m \u001b[43m \u001b[49m\u001b[43mPostOrdersArgs\u001b[49m\u001b[43m(\u001b[49m\n\u001b[32m 19\u001b[39m \u001b[43m \u001b[49m\u001b[43morder\u001b[49m\u001b[43m=\u001b[49m\u001b[43mclient\u001b[49m\u001b[43m.\u001b[49m\u001b[43mcreate_order\u001b[49m\u001b[43m(\u001b[49m\n\u001b[32m 20\u001b[39m \u001b[43m \u001b[49m\u001b[43morder_args\u001b[49m\u001b[43m=\u001b[49m\u001b[43mOrderArgs\u001b[49m\u001b[43m(\u001b[49m\n\u001b[32m 21\u001b[39m \u001b[43m \u001b[49m\u001b[43mtoken_id\u001b[49m\u001b[43m=\u001b[49m\u001b[43mmarket_details\u001b[49m\u001b[43m[\u001b[49m\u001b[33;43m'\u001b[39;49m\u001b[33;43mDown\u001b[39;49m\u001b[33;43m'\u001b[39;49m\u001b[43m]\u001b[49m\u001b[43m,\u001b[49m\n\u001b[32m 22\u001b[39m \u001b[43m \u001b[49m\u001b[43mprice\u001b[49m\u001b[43m=\u001b[49m\u001b[32;43m0.10\u001b[39;49m\u001b[43m,\u001b[49m\n\u001b[32m 23\u001b[39m \u001b[43m \u001b[49m\u001b[43msize\u001b[49m\u001b[43m=\u001b[49m\u001b[32;43m10\u001b[39;49m\u001b[43m,\u001b[49m\n\u001b[32m 24\u001b[39m \u001b[43m \u001b[49m\u001b[43mside\u001b[49m\u001b[43m=\u001b[49m\u001b[43mBUY\u001b[49m\u001b[43m,\u001b[49m\n\u001b[32m 25\u001b[39m \u001b[43m \u001b[49m\u001b[43m)\u001b[49m\u001b[43m,\u001b[49m\n\u001b[32m 26\u001b[39m \u001b[43m \u001b[49m\u001b[43moptions\u001b[49m\u001b[43m=\u001b[49m\u001b[43mPartialCreateOrderOptions\u001b[49m\u001b[43m(\u001b[49m\n\u001b[32m 27\u001b[39m \u001b[43m \u001b[49m\u001b[43mtick_size\u001b[49m\u001b[43m=\u001b[49m\u001b[38;5;28;43mstr\u001b[39;49m\u001b[43m(\u001b[49m\u001b[43mmarket_details\u001b[49m\u001b[43m[\u001b[49m\u001b[33;43m'\u001b[39;49m\u001b[33;43mMinTickSize\u001b[39;49m\u001b[33;43m'\u001b[39;49m\u001b[43m]\u001b[49m\u001b[43m)\u001b[49m\u001b[43m,\u001b[49m\n\u001b[32m 28\u001b[39m \u001b[43m \u001b[49m\u001b[43mneg_risk\u001b[49m\u001b[43m=\u001b[49m\u001b[43mmarket_details\u001b[49m\u001b[43m[\u001b[49m\u001b[33;43m'\u001b[39;49m\u001b[33;43misNegRisk\u001b[39;49m\u001b[33;43m'\u001b[39;49m\u001b[43m]\u001b[49m\n\u001b[32m 29\u001b[39m \u001b[43m \u001b[49m\u001b[43m)\u001b[49m\u001b[43m,\u001b[49m\n\u001b[32m 30\u001b[39m \u001b[43m \u001b[49m\u001b[43m)\u001b[49m\u001b[43m,\u001b[49m\n\u001b[32m 31\u001b[39m \u001b[43m \u001b[49m\u001b[43morderType\u001b[49m\u001b[43m=\u001b[49m\u001b[43mOrderType\u001b[49m\u001b[43m.\u001b[49m\u001b[43mGTC\u001b[49m\u001b[43m,\u001b[49m\n\u001b[32m 32\u001b[39m \u001b[43m \u001b[49m\u001b[43mpostOnly\u001b[49m\u001b[43m=\u001b[49m\u001b[38;5;28;43;01mTrue\u001b[39;49;00m\u001b[43m,\u001b[49m\n\u001b[32m 33\u001b[39m \u001b[43m \u001b[49m\u001b[43m)\u001b[49m\u001b[43m,\u001b[49m\n\u001b[32m 34\u001b[39m \u001b[43m]\u001b[49m\u001b[43m)\u001b[49m\n\u001b[32m 36\u001b[39m \u001b[38;5;28mprint\u001b[39m(response)\n", - "\u001b[36mFile \u001b[39m\u001b[32m~/miniconda3/envs/py_313/lib/python3.13/site-packages/py_clob_client/client.py:617\u001b[39m, in \u001b[36mClobClient.post_orders\u001b[39m\u001b[34m(self, args)\u001b[39m\n\u001b[32m 611\u001b[39m \u001b[38;5;28;01mreturn\u001b[39;00m post(\n\u001b[32m 612\u001b[39m \u001b[33m\"\u001b[39m\u001b[38;5;132;01m{}\u001b[39;00m\u001b[38;5;132;01m{}\u001b[39;00m\u001b[33m\"\u001b[39m.format(\u001b[38;5;28mself\u001b[39m.host, POST_ORDERS),\n\u001b[32m 613\u001b[39m headers=builder_headers,\n\u001b[32m 614\u001b[39m data=request_args.serialized_body,\n\u001b[32m 615\u001b[39m )\n\u001b[32m 616\u001b[39m \u001b[38;5;66;03m# send exact serialized bytes\u001b[39;00m\n\u001b[32m--> \u001b[39m\u001b[32m617\u001b[39m \u001b[38;5;28;01mreturn\u001b[39;00m \u001b[43mpost\u001b[49m\u001b[43m(\u001b[49m\n\u001b[32m 618\u001b[39m \u001b[43m \u001b[49m\u001b[33;43m\"\u001b[39;49m\u001b[38;5;132;43;01m{}\u001b[39;49;00m\u001b[38;5;132;43;01m{}\u001b[39;49;00m\u001b[33;43m\"\u001b[39;49m\u001b[43m.\u001b[49m\u001b[43mformat\u001b[49m\u001b[43m(\u001b[49m\u001b[38;5;28;43mself\u001b[39;49m\u001b[43m.\u001b[49m\u001b[43mhost\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mPOST_ORDERS\u001b[49m\u001b[43m)\u001b[49m\u001b[43m,\u001b[49m\n\u001b[32m 619\u001b[39m \u001b[43m \u001b[49m\u001b[43mheaders\u001b[49m\u001b[43m=\u001b[49m\u001b[43mheaders\u001b[49m\u001b[43m,\u001b[49m\n\u001b[32m 620\u001b[39m \u001b[43m \u001b[49m\u001b[43mdata\u001b[49m\u001b[43m=\u001b[49m\u001b[43mrequest_args\u001b[49m\u001b[43m.\u001b[49m\u001b[43mserialized_body\u001b[49m\u001b[43m,\u001b[49m\n\u001b[32m 621\u001b[39m \u001b[43m\u001b[49m\u001b[43m)\u001b[49m\n", - "\u001b[36mFile \u001b[39m\u001b[32m~/miniconda3/envs/py_313/lib/python3.13/site-packages/py_clob_client/http_helpers/helpers.py:69\u001b[39m, in \u001b[36mpost\u001b[39m\u001b[34m(endpoint, headers, data)\u001b[39m\n\u001b[32m 68\u001b[39m \u001b[38;5;28;01mdef\u001b[39;00m\u001b[38;5;250m \u001b[39m\u001b[34mpost\u001b[39m(endpoint, headers=\u001b[38;5;28;01mNone\u001b[39;00m, data=\u001b[38;5;28;01mNone\u001b[39;00m):\n\u001b[32m---> \u001b[39m\u001b[32m69\u001b[39m \u001b[38;5;28;01mreturn\u001b[39;00m \u001b[43mrequest\u001b[49m\u001b[43m(\u001b[49m\u001b[43mendpoint\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mPOST\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mheaders\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mdata\u001b[49m\u001b[43m)\u001b[49m\n", - "\u001b[36mFile \u001b[39m\u001b[32m~/miniconda3/envs/py_313/lib/python3.13/site-packages/py_clob_client/http_helpers/helpers.py:57\u001b[39m, in \u001b[36mrequest\u001b[39m\u001b[34m(endpoint, method, headers, data)\u001b[39m\n\u001b[32m 49\u001b[39m resp = _http_client.request(\n\u001b[32m 50\u001b[39m method=method,\n\u001b[32m 51\u001b[39m url=endpoint,\n\u001b[32m 52\u001b[39m headers=headers,\n\u001b[32m 53\u001b[39m json=data,\n\u001b[32m 54\u001b[39m )\n\u001b[32m 56\u001b[39m \u001b[38;5;28;01mif\u001b[39;00m resp.status_code != \u001b[32m200\u001b[39m:\n\u001b[32m---> \u001b[39m\u001b[32m57\u001b[39m \u001b[38;5;28;01mraise\u001b[39;00m PolyApiException(resp)\n\u001b[32m 59\u001b[39m \u001b[38;5;28;01mtry\u001b[39;00m:\n\u001b[32m 60\u001b[39m \u001b[38;5;28;01mreturn\u001b[39;00m resp.json()\n", - "\u001b[31mPolyApiException\u001b[39m: PolyApiException[status_code=403, error_message={'error': 'Trading restricted in your region, please refer to available regions - https://docs.polymarket.com/developers/CLOB/geoblock'}]" + "name": "stdout", + "output_type": "stream", + "text": [ + "[{'errorMsg': '', 'orderID': '0x575c43f35b1f5e3e01779df5293d987dc81a347a4e09423a856ecb45555e30cb', 'takingAmount': '', 'makingAmount': '', 'status': 'live', 'success': True}]\n" ] } ], "source": [ + "### POST \n", "response = client.post_orders([\n", " PostOrdersArgs(\n", " order=client.create_order(\n", " order_args=OrderArgs(\n", " token_id=market_details['Up'],\n", - " price=0.20,\n", + " price=0.1,\n", " size=10,\n", " side=BUY,\n", " ),\n", @@ -184,11 +389,67 @@ " orderType=OrderType.GTC,\n", " postOnly=False,\n", " ),\n", + " # PostOrdersArgs(\n", + " # order=client.create_order(\n", + " # order_args=OrderArgs(\n", + " # token_id=market_details['Down'],\n", + " # price=0.10,\n", + " # size=10,\n", + " # side=BUY,\n", + " # ),\n", + " # options=PartialCreateOrderOptions(\n", + " # tick_size=str(market_details['MinTickSize']),\n", + " # neg_risk=market_details['isNegRisk']\n", + " # ),\n", + " # ),\n", + " # orderType=OrderType.GTC,\n", + " # postOnly=True,\n", + " # ),\n", + "])\n", + "\n", + "print(response)" + ] + }, + { + "cell_type": "code", + "execution_count": 15, + "id": "52a7229b", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "[{'errorMsg': '',\n", + " 'orderID': '0x483e094216d6453f0398875094a29a29a913b103fb1180164e092e3ee65209fe',\n", + " 'takingAmount': '',\n", + " 'makingAmount': '',\n", + " 'status': 'live',\n", + " 'success': True}]" + ] + }, + "execution_count": 15, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "response" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "bbed2536", + "metadata": {}, + "outputs": [], + "source": [ + "### POST \n", + "response = client.post_orders([\n", " PostOrdersArgs(\n", " order=client.create_order(\n", " order_args=OrderArgs(\n", - " token_id=market_details['Down'],\n", - " price=0.10,\n", + " token_id=market_details['Up'],\n", + " price=0.1,\n", " size=10,\n", " side=BUY,\n", " ),\n", @@ -198,17 +459,59 @@ " ),\n", " ),\n", " orderType=OrderType.GTC,\n", - " postOnly=True,\n", + " postOnly=False,\n", " ),\n", + " # PostOrdersArgs(\n", + " # order=client.create_order(\n", + " # order_args=OrderArgs(\n", + " # token_id=market_details['Down'],\n", + " # price=0.10,\n", + " # size=10,\n", + " # side=BUY,\n", + " # ),\n", + " # options=PartialCreateOrderOptions(\n", + " # tick_size=str(market_details['MinTickSize']),\n", + " # neg_risk=market_details['isNegRisk']\n", + " # ),\n", + " # ),\n", + " # orderType=OrderType.GTC,\n", + " # postOnly=True,\n", + " # ),\n", "])\n", "\n", "print(response)" ] }, + { + "cell_type": "code", + "execution_count": 138, + "id": "9ec69680", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "[{'errorMsg': '',\n", + " 'orderID': '0x575c43f35b1f5e3e01779df5293d987dc81a347a4e09423a856ecb45555e30cb',\n", + " 'takingAmount': '',\n", + " 'makingAmount': '',\n", + " 'status': 'live',\n", + " 'success': True}]" + ] + }, + "execution_count": 138, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "response" + ] + }, { "cell_type": "code", "execution_count": null, - "id": "52a7229b", + "id": "b7234035", "metadata": {}, "outputs": [], "source": [] @@ -216,7 +519,6 @@ { "cell_type": "code", "execution_count": null, - "id": "fb5f066a", "metadata": {}, "outputs": [], "source": [] @@ -241,167 +543,84 @@ }, { "cell_type": "code", - "execution_count": 69, - "id": "f608378f", + "execution_count": null, + "id": "2fe32a82", "metadata": {}, "outputs": [ { "data": { - "application/vnd.microsoft.datawrangler.viewer.v0+json": { - "columns": [ - { - "name": "index", - "rawType": "int64", - "type": "integer" - }, - { - "name": "asset_id", - "rawType": "object", - "type": "string" - }, - { - "name": "price", - "rawType": "object", - "type": "string" - }, - { - "name": "size", - "rawType": "object", - "type": "string" - }, - { - "name": "side", - "rawType": "object", - "type": "string" - }, - { - "name": "hash", - "rawType": "object", - "type": "string" - }, - { - "name": "best_bid", - "rawType": "object", - "type": "string" - }, - { - "name": "best_ask", - "rawType": "object", - "type": "string" - } - ], - "ref": "310c4763-20db-4051-931a-ef52e1b6513b", - "rows": [ - [ - "0", - "97987758532314346863331680319607711694838937984814950023901315671390566048932", - "0.37", - "429.41", - "BUY", - "999d5b73d83a840c0df7dc2d817ae55a242845d5", - "0.37", - "0.38" - ], - [ - "1", - "92959981857766705879127008770062050214089835506649207585188324269480756219695", - "0.63", - "429.41", - "SELL", - "ead5af5a55f8b125b1e50f1c80a783c3c2d33187", - "0.62", - "0.63" - ] - ], - "shape": { - "columns": 7, - "rows": 2 - } - }, - "text/html": [ - "
\n", - "\n", - "\n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "
asset_idpricesizesidehashbest_bidbest_ask
09798775853231434686333168031960771169483893798...0.37429.41BUY999d5b73d83a840c0df7dc2d817ae55a242845d50.370.38
19295998185776670587912700877006205021408983550...0.63429.41SELLead5af5a55f8b125b1e50f1c80a783c3c2d331870.620.63
\n", - "
" - ], "text/plain": [ - " asset_id price size side \\\n", - "0 9798775853231434686333168031960771169483893798... 0.37 429.41 BUY \n", - "1 9295998185776670587912700877006205021408983550... 0.63 429.41 SELL \n", - "\n", - " hash best_bid best_ask \n", - "0 999d5b73d83a840c0df7dc2d817ae55a242845d5 0.37 0.38 \n", - "1 ead5af5a55f8b125b1e50f1c80a783c3c2d33187 0.62 0.63 " + "Timestamp('2026-03-30 23:32:03')" ] }, - "execution_count": 69, + "execution_count": 80, "metadata": {}, "output_type": "execute_result" } ], "source": [ - "pd.DataFrame(d)" + "hist_trades_lookback_ts = round(datetime.now().timestamp() - 10)*1000\n", + "pd.to_datetime(hist_trades_lookback_ts*1000, unit='ms')" + ] + }, + { + "cell_type": "code", + "execution_count": 81, + "id": "2a3b19f2", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "Timestamp('2026-03-30 04:22:23.585000')" + ] + }, + "execution_count": 81, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "pd.to_datetime(1774844543585, unit='ms')" + ] + }, + { + "cell_type": "code", + "execution_count": 79, + "id": "b5139910", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "1774913409000" + ] + }, + "execution_count": 79, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "hist_trades_lookback_ts*1000" ] }, { "cell_type": "code", "execution_count": null, - "id": "2fe32a82", + "id": "d6d3c1bb", "metadata": {}, "outputs": [], - "source": [] + "source": [ + "import numpy as np\n", + "n = np.empty((0, 3))\n", + "np.append(n, [1,2,3])" + ] }, { "cell_type": "code", "execution_count": null, - "id": "7603be6c", + "id": "1e5cba44", "metadata": {}, "outputs": [], "source": [] @@ -633,19 +852,524 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 140, "id": "008cb5c9", "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "{'Up': '13157292356296687506747919717798752029699544499054519087985411865141996614822',\n", + " 'Down': '70507961363566124468475538524172170043725846352735387705515911177933084557518',\n", + " 'isActive': False,\n", + " 'MinTickSize': 0.01,\n", + " 'isNegRisk': False,\n", + " 'ConditionId': '0xd1773b412dacad884c202a7b14f0197918b1e22028ce2b5737fbd659bbe150f0'}" + ] + }, + "execution_count": 140, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "market_details" + ] + }, + { + "cell_type": "code", + "execution_count": 130, + "id": "f6c647b7", + "metadata": {}, + "outputs": [], + "source": [ + "d = {\"id\":\"0x73c6d7c0cba705a06d840c185ef188a195b3e36472dceab61cee23202ec7a9a0\",\"owner\":\"00e5d36c-6c46-77e1-a436-0f0a4bfbfdd8\",\"market\":\"0xf856ff89eb9ed2ab393a1bbc496b5db539b4d7f9cec91202b13c97379e9e58d6\",\"asset_id\":\"39378292107289994981363071337831788209917841322683175778910172170676449806535\",\"side\":\"BUY\",\"order_owner\":\"00e5d36c-6c46-77e1-a436-0f0a4bfbfdd8\",\"original_size\":\"10\",\"size_matched\":\"0\",\"price\":\"0.2\",\"associate_trades\":[],\"outcome\":\"Up\",\"type\":\"PLACEMENT\",\"created_at\":\"1774818626\",\"expiration\":\"0\",\"order_type\":\"GTC\",\"status\":\"LIVE\",\"maker_address\":\"0xb2967A7e578E700E27611238B7F762BdADC72CcB\",\"timestamp\":\"1774818626080\",\"event_type\":\"order\"}\n", + "e = {\"id\":\"0x73c6d7c0cba705a06d840c185ef188a195b3e36472dceab61cee23202ec7a9a0\",\"owner\":\"00e5d36c-6c46-77e1-a436-0f0a4bfbfdd8\",\"market\":\"0xf856ff89eb9ed2ab393a1bbc496b5db539b4d7f9cec91202b13c97379e9e58d6\",\"asset_id\":\"39378292107289994981363071337831788209917841322683175778910172170676449806535\",\"side\":\"BUY\",\"order_owner\":\"00e5d36c-6c46-77e1-a436-0f0a4bfbfdd8\",\"original_size\":\"10\",\"size_matched\":\"0\",\"price\":\"0.2\",\"associate_trades\":[],\"outcome\":\"Up\",\"type\":\"PLACEMENT\",\"created_at\":\"1774818626\",\"expiration\":\"0\",\"order_type\":\"GTC\",\"status\":\"LIVE\",\"maker_address\":\"0xb2967A7e578E700E27611238B7F762BdADC72CcB\",\"timestamp\":\"9774818626080\",\"event_type\":\"order\"}" + ] + }, + { + "cell_type": "code", + "execution_count": 131, + "id": "00c55bf5", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "{'id': '0x73c6d7c0cba705a06d840c185ef188a195b3e36472dceab61cee23202ec7a9a0',\n", + " 'owner': '00e5d36c-6c46-77e1-a436-0f0a4bfbfdd8',\n", + " 'market': '0xf856ff89eb9ed2ab393a1bbc496b5db539b4d7f9cec91202b13c97379e9e58d6',\n", + " 'asset_id': '39378292107289994981363071337831788209917841322683175778910172170676449806535',\n", + " 'side': 'BUY',\n", + " 'order_owner': '00e5d36c-6c46-77e1-a436-0f0a4bfbfdd8',\n", + " 'original_size': '10',\n", + " 'size_matched': '0',\n", + " 'price': '0.2',\n", + " 'associate_trades': [],\n", + " 'outcome': 'Up',\n", + " 'type': 'PLACEMENT',\n", + " 'created_at': '1774818626',\n", + " 'expiration': '0',\n", + " 'order_type': 'GTC',\n", + " 'status': 'LIVE',\n", + " 'maker_address': '0xb2967A7e578E700E27611238B7F762BdADC72CcB',\n", + " 'timestamp': '1774818626080',\n", + " 'event_type': 'order'}" + ] + }, + "execution_count": 131, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "d" + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "id": "cf38819c", + "metadata": {}, + "outputs": [], + "source": [ + "f = {\"id\":\"0x73c6d7c0cba705a06d840c185ef188a195b3e36472dceab61cee23202ec7a9a0\",\"owner\":\"00e5d36c-6c46-77e1-a436-0f0a4bfbfdd8\",\"market\":\"0xf856ff89eb9ed2ab393a1bbc496b5db539b4d7f9cec91202b13c97379e9e58d6\",\"asset_id\":\"39378292107289994981363071337831788209917841322683175778910172170676449806535\",\"side\":\"BUY\",\"order_owner\":\"00e5d36c-6c46-77e1-a436-0f0a4bfbfdd8\",\"original_size\":\"10\",\"size_matched\":\"0\",\"price\":\"0.2\",\"associate_trades\":[],\"outcome\":\"Up\",\"type\":\"CANCELLATION\",\"created_at\":\"1774818626\",\"expiration\":\"0\",\"order_type\":\"GTC\",\"status\":\"CANCELED\",\"maker_address\":\"0xb2967A7e578E700E27611238B7F762BdADC72CcB\",\"timestamp\":\"1774818630291\",\"event_type\":\"order\"}" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "ea67fb64", + "metadata": {}, + "outputs": [], + "source": [ + "a = json.dumps(f['associate_trades']) if len(f['associate_trades']) > 0 else None" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "59521b6e", + "metadata": {}, "outputs": [], "source": [] }, { "cell_type": "code", "execution_count": null, - "id": "f6c647b7", + "id": "6b4ee964", "metadata": {}, "outputs": [], - "source": [] + "source": [ + "def live_orders_only(live_orders, new_msg):\n", + " return [d for d in live_orders if d.get('status')=='live']" + ] + }, + { + "cell_type": "code", + "execution_count": 117, + "id": "715d8606", + "metadata": {}, + "outputs": [], + "source": [ + "def upsert_list_of_dicts_by_id(list_of_dicts, new_dict):\n", + " for index, item in enumerate(list_of_dicts):\n", + " if item.get('id') == new_dict.get('id'):\n", + " list_of_dicts[index] = new_dict\n", + " return list_of_dicts\n", + " \n", + " list_of_dicts.append(new_dict)\n", + " return list_of_dicts" + ] + }, + { + "cell_type": "code", + "execution_count": 113, + "id": "4c00fdb7", + "metadata": {}, + "outputs": [], + "source": [ + "ORDERS_STATUS = []\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "d8a2bbbf", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "[{'id': '0x73c6d7c0cba705a06d840c185ef188a195b3e36472dceab61cee23202ec7a9a0',\n", + " 'owner': '00e5d36c-6c46-77e1-a436-0f0a4bfbfdd8',\n", + " 'market': '0xf856ff89eb9ed2ab393a1bbc496b5db539b4d7f9cec91202b13c97379e9e58d6',\n", + " 'asset_id': '39378292107289994981363071337831788209917841322683175778910172170676449806535',\n", + " 'side': 'BUY',\n", + " 'order_owner': '00e5d36c-6c46-77e1-a436-0f0a4bfbfdd8',\n", + " 'original_size': '10',\n", + " 'size_matched': '0',\n", + " 'price': '0.2',\n", + " 'associate_trades': [],\n", + " 'outcome': 'Up',\n", + " 'type': 'PLACEMENT',\n", + " 'created_at': '1774818626',\n", + " 'expiration': '0',\n", + " 'order_type': 'GTC',\n", + " 'status': 'LIVE',\n", + " 'maker_address': '0xb2967A7e578E700E27611238B7F762BdADC72CcB',\n", + " 'timestamp': '1774818626080',\n", + " 'event_type': 'order'}]" + ] + }, + "execution_count": 114, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "ORDERS_STATUS = upsert_list_of_dicts_by_id(ORDERS_STATUS, d)\n", + "ORDERS_STATUS" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "6e8be4ab", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "[{'id': '0x73c6d7c0cba705a06d840c185ef188a195b3e36472dceab61cee23202ec7a9a0',\n", + " 'owner': '00e5d36c-6c46-77e1-a436-0f0a4bfbfdd8',\n", + " 'market': '0xf856ff89eb9ed2ab393a1bbc496b5db539b4d7f9cec91202b13c97379e9e58d6',\n", + " 'asset_id': '39378292107289994981363071337831788209917841322683175778910172170676449806535',\n", + " 'side': 'BUY',\n", + " 'order_owner': '00e5d36c-6c46-77e1-a436-0f0a4bfbfdd8',\n", + " 'original_size': '10',\n", + " 'size_matched': '0',\n", + " 'price': '0.2',\n", + " 'associate_trades': [],\n", + " 'outcome': 'Up',\n", + " 'type': 'PLACEMENT',\n", + " 'created_at': '1774818626',\n", + " 'expiration': '0',\n", + " 'order_type': 'GTC',\n", + " 'status': 'LIVE',\n", + " 'maker_address': '0xb2967A7e578E700E27611238B7F762BdADC72CcB',\n", + " 'timestamp': '9774818626080',\n", + " 'event_type': 'order'}]" + ] + }, + "execution_count": 115, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "ORDERS_STATUS = upsert_list_of_dicts_by_id(ORDERS_STATUS, e)\n", + "ORDERS_STATUS" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "f3b0a0c2", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "[{'id': '0x73c6d7c0cba705a06d840c185ef188a195b3e36472dceab61cee23202ec7a9a0',\n", + " 'owner': '00e5d36c-6c46-77e1-a436-0f0a4bfbfdd8',\n", + " 'market': '0xf856ff89eb9ed2ab393a1bbc496b5db539b4d7f9cec91202b13c97379e9e58d6',\n", + " 'asset_id': '39378292107289994981363071337831788209917841322683175778910172170676449806535',\n", + " 'side': 'BUY',\n", + " 'order_owner': '00e5d36c-6c46-77e1-a436-0f0a4bfbfdd8',\n", + " 'original_size': '10',\n", + " 'size_matched': '0',\n", + " 'price': '0.2',\n", + " 'associate_trades': [],\n", + " 'outcome': 'Up',\n", + " 'type': 'CANCELLATION',\n", + " 'created_at': '1774818626',\n", + " 'expiration': '0',\n", + " 'order_type': 'GTC',\n", + " 'status': 'CANCELED',\n", + " 'maker_address': '0xb2967A7e578E700E27611238B7F762BdADC72CcB',\n", + " 'timestamp': '1774818630291',\n", + " 'event_type': 'order'}]" + ] + }, + "execution_count": 116, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "ORDERS_STATUS = upsert_list_of_dicts_by_id(ORDERS_STATUS, f)\n", + "ORDERS_STATUS" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "6be0d1e8", + "metadata": {}, + "outputs": [], + "source": [ + "{\n", + " \"event_type\": \"order\",\n", + " \"id\": \"0xff354cd7ca7539dfa9c28d90943ab5779a4eac34b9b37a757d7b32bdfb11790b\",\n", + " \"owner\": \"9180014b-33c8-9240-a14b-bdca11c0a465\",\n", + " \"market\": \"0xbd31dc8a20211944f6b70f31557f1001557b59905b7738480ca09bd4532f84af\",\n", + " \"asset_id\": \"52114319501245915516055106046884209969926127482827954674443846427813813222426\",\n", + " \"side\": \"SELL\",\n", + " \"order_owner\": \"9180014b-33c8-9240-a14b-bdca11c0a465\",\n", + " \"original_size\": \"10\",\n", + " \"size_matched\": \"0\",\n", + " \"price\": \"0.57\",\n", + " \"associate_trades\": null,\n", + " \"outcome\": \"YES\",\n", + " \"type\": \"PLACEMENT\",\n", + " \"created_at\": \"1672290687\",\n", + " \"expiration\": \"1234567\",\n", + " \"order_type\": \"GTD\",\n", + " \"status\": \"LIVE\",\n", + " \"maker_address\": \"0x1234...\",\n", + " \"timestamp\": \"1672290687\"\n", + "}" + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "id": "761e4f8a", + "metadata": {}, + "outputs": [], + "source": [ + "z = {\n", + " \"event_type\": \"trade\",\n", + " \"type\": \"TRADE\",\n", + " \"id\": \"28c4d2eb-bbea-40e7-a9f0-b2fdb56b2c2e\",\n", + " \"taker_order_id\": \"0x06bc63e346ed4ceddce9efd6b3af37c8f8f440c92fe7da6b2d0f9e4ccbc50c42\",\n", + " \"market\": \"0xbd31dc8a20211944f6b70f31557f1001557b59905b7738480ca09bd4532f84af\",\n", + " \"asset_id\": \"52114319501245915516055106046884209969926127482827954674443846427813813222426\",\n", + " \"side\": \"BUY\",\n", + " \"size\": \"10\",\n", + " \"price\": \"0.57\",\n", + " \"fee_rate_bps\": \"0\",\n", + " \"status\": \"MATCHED\",\n", + " \"matchtime\": \"1672290701\",\n", + " \"last_update\": \"1672290701\",\n", + " \"outcome\": \"YES\",\n", + " \"owner\": \"9180014b-33c8-9240-a14b-bdca11c0a465\",\n", + " \"trade_owner\": \"9180014b-33c8-9240-a14b-bdca11c0a465\",\n", + " \"maker_address\": \"0x1234...\",\n", + " \"transaction_hash\": \"\",\n", + " \"bucket_index\": 0,\n", + " \"maker_orders\": [\n", + " {\n", + " \"order_id\": \"0xff354cd7ca7539dfa9c28d90943ab5779a4eac34b9b37a757d7b32bdfb11790b\",\n", + " \"owner\": \"9180014b-33c8-9240-a14b-bdca11c0a465\",\n", + " \"maker_address\": \"0x5678...\",\n", + " \"matched_amount\": \"10\",\n", + " \"price\": \"0.57\",\n", + " \"fee_rate_bps\": \"0\",\n", + " \"asset_id\": \"52114319501245915516055106046884209969926127482827954674443846427813813222426\",\n", + " \"outcome\": \"YES\",\n", + " \"side\": \"SELL\"\n", + " }\n", + " ],\n", + " \"trader_side\": \"TAKER\",\n", + " \"timestamp\": \"1672290701\"\n", + "}" + ] + }, + { + "cell_type": "code", + "execution_count": 17, + "id": "3c5a0922", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "{'event_type': 'trade',\n", + " 'type': 'TRADE',\n", + " 'id': '28c4d2eb-bbea-40e7-a9f0-b2fdb56b2c2e',\n", + " 'taker_order_id': '0x06bc63e346ed4ceddce9efd6b3af37c8f8f440c92fe7da6b2d0f9e4ccbc50c42',\n", + " 'market': '0xbd31dc8a20211944f6b70f31557f1001557b59905b7738480ca09bd4532f84af',\n", + " 'asset_id': '52114319501245915516055106046884209969926127482827954674443846427813813222426',\n", + " 'side': 'BUY',\n", + " 'size': '10',\n", + " 'price': '0.57',\n", + " 'fee_rate_bps': '0',\n", + " 'status': 'MATCHED',\n", + " 'matchtime': '1672290701',\n", + " 'last_update': '1672290701',\n", + " 'outcome': 'YES',\n", + " 'owner': '9180014b-33c8-9240-a14b-bdca11c0a465',\n", + " 'trade_owner': '9180014b-33c8-9240-a14b-bdca11c0a465',\n", + " 'maker_address': '0x1234...',\n", + " 'transaction_hash': '',\n", + " 'bucket_index': 0,\n", + " 'maker_orders': [{'order_id': '0xff354cd7ca7539dfa9c28d90943ab5779a4eac34b9b37a757d7b32bdfb11790b',\n", + " 'owner': '9180014b-33c8-9240-a14b-bdca11c0a465',\n", + " 'maker_address': '0x5678...',\n", + " 'matched_amount': '10',\n", + " 'price': '0.57',\n", + " 'fee_rate_bps': '0',\n", + " 'asset_id': '52114319501245915516055106046884209969926127482827954674443846427813813222426',\n", + " 'outcome': 'YES',\n", + " 'side': 'SELL'}],\n", + " 'trader_side': 'TAKER',\n", + " 'timestamp': '1672290701'}" + ] + }, + "execution_count": 17, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "z" + ] + }, + { + "cell_type": "code", + "execution_count": 16, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "'[{\"order_id\": \"0xff354cd7ca7539dfa9c28d90943ab5779a4eac34b9b37a757d7b32bdfb11790b\", \"owner\": \"9180014b-33c8-9240-a14b-bdca11c0a465\", \"maker_address\": \"0x5678...\", \"matched_amount\": \"10\", \"price\": \"0.57\", \"fee_rate_bps\": \"0\", \"asset_id\": \"52114319501245915516055106046884209969926127482827954674443846427813813222426\", \"outcome\": \"YES\", \"side\": \"SELL\"}]'" + ] + }, + "execution_count": 16, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "import json\n", + "json.dumps(z['maker_orders'])" + ] + }, + { + "cell_type": "code", + "execution_count": 18, + "metadata": {}, + "outputs": [], + "source": [ + "z['maker_orders'] = json.dumps(z['maker_orders'])" + ] + }, + { + "cell_type": "code", + "execution_count": 19, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "{'event_type': 'trade',\n", + " 'type': 'TRADE',\n", + " 'id': '28c4d2eb-bbea-40e7-a9f0-b2fdb56b2c2e',\n", + " 'taker_order_id': '0x06bc63e346ed4ceddce9efd6b3af37c8f8f440c92fe7da6b2d0f9e4ccbc50c42',\n", + " 'market': '0xbd31dc8a20211944f6b70f31557f1001557b59905b7738480ca09bd4532f84af',\n", + " 'asset_id': '52114319501245915516055106046884209969926127482827954674443846427813813222426',\n", + " 'side': 'BUY',\n", + " 'size': '10',\n", + " 'price': '0.57',\n", + " 'fee_rate_bps': '0',\n", + " 'status': 'MATCHED',\n", + " 'matchtime': '1672290701',\n", + " 'last_update': '1672290701',\n", + " 'outcome': 'YES',\n", + " 'owner': '9180014b-33c8-9240-a14b-bdca11c0a465',\n", + " 'trade_owner': '9180014b-33c8-9240-a14b-bdca11c0a465',\n", + " 'maker_address': '0x1234...',\n", + " 'transaction_hash': '',\n", + " 'bucket_index': 0,\n", + " 'maker_orders': '[{\"order_id\": \"0xff354cd7ca7539dfa9c28d90943ab5779a4eac34b9b37a757d7b32bdfb11790b\", \"owner\": \"9180014b-33c8-9240-a14b-bdca11c0a465\", \"maker_address\": \"0x5678...\", \"matched_amount\": \"10\", \"price\": \"0.57\", \"fee_rate_bps\": \"0\", \"asset_id\": \"52114319501245915516055106046884209969926127482827954674443846427813813222426\", \"outcome\": \"YES\", \"side\": \"SELL\"}]',\n", + " 'trader_side': 'TAKER',\n", + " 'timestamp': '1672290701'}" + ] + }, + "execution_count": 19, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "z" + ] + }, + { + "cell_type": "code", + "execution_count": 29, + "id": "0dbb8fa9", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "1774899574594" + ] + }, + "execution_count": 29, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "from datetime import datetime\n", + "import pandas as pd\n", + "round(datetime.now().timestamp()*1000)" + ] + }, + { + "cell_type": "code", + "execution_count": 30, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "Timestamp('2026-03-30 19:39:16.685000')" + ] + }, + "execution_count": 30, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "pd.to_datetime(1774899556685, unit='ms')" + ] + }, + { + "cell_type": "code", + "execution_count": 31, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "Timestamp('2026-03-30 19:39:06.685000')" + ] + }, + "execution_count": 31, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "pd.to_datetime(1774899556685-10*1000, unit='ms')" + ] }, { "cell_type": "code", @@ -655,6 +1379,125 @@ "outputs": [], "source": [] }, + { + "cell_type": "code", + "execution_count": null, + "id": "6765c7e6", + "metadata": {}, + "outputs": [], + "source": [] + }, + { + "cell_type": "code", + "execution_count": 50, + "id": "d367f55b", + "metadata": {}, + "outputs": [], + "source": [ + "d = {\"type\":\"TRADE\",\"id\":\"08045b74-925f-4982-bc5b-577104db6530\",\"taker_order_id\":\"0xf6c77c6ef4c229c3edf7ba0a2183c4cd1193211787e2fa7fb0e7bf132189b303\",\"market\":\"0x7cdfcb753bebd87214e719497aa1d1b217582dc22746c8543b74e1e53539a9f5\",\"asset_id\":\"56806427206990113501155420994096888900269904953230745602350514810276549822457\",\"side\":\"BUY\",\"size\":\"7.042251\",\"fee_rate_bps\":\"1000\",\"price\":\"0.71\",\"status\":\"MATCHED\",\"match_time\":\"1774900601\",\"last_update\":\"1774900601\",\"outcome\":\"Up\",\"owner\":\"00e5d36c-6c46-77e1-a436-0f0a4bfbfdd8\",\"trade_owner\":\"00e5d36c-6c46-77e1-a436-0f0a4bfbfdd8\",\"maker_address\":\"0xb2967A7e578E700E27611238B7F762BdADC72CcB\",\"transaction_hash\":\"0x454cbaad0da15d095be778773c362af90de43beb6660c78ff155b3877d283882\",\"bucket_index\":0,\"maker_orders\":[{\"order_id\":\"0x4efdbcd86c81df9d6a02d7e35ff92411af76aa3ca6f445ba9d6e3773e812f706\",\"owner\":\"579ba9d5-cc34-1b66-3b70-d5bb5d1ccc18\",\"maker_address\":\"0x5Bde889dC26B097b5eAa2F1F027e01712EBCcbB7\",\"matched_amount\":\"7.042251\",\"price\":\"0.2900000298200107\",\"fee_rate_bps\":\"1000\",\"asset_id\":\"95594709608392853199445131908660268785353291802775571968225275873296398106331\",\"outcome\":\"Down\",\"outcome_index\":0,\"side\":\"BUY\"}],\"trader_side\":\"TAKER\",\"timestamp\":\"1774900601865\",\"event_type\":\"trade\"}" + ] + }, + { + "cell_type": "code", + "execution_count": 51, + "id": "3e8a71a7", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "{'type': 'TRADE',\n", + " 'id': '08045b74-925f-4982-bc5b-577104db6530',\n", + " 'taker_order_id': '0xf6c77c6ef4c229c3edf7ba0a2183c4cd1193211787e2fa7fb0e7bf132189b303',\n", + " 'market': '0x7cdfcb753bebd87214e719497aa1d1b217582dc22746c8543b74e1e53539a9f5',\n", + " 'asset_id': '56806427206990113501155420994096888900269904953230745602350514810276549822457',\n", + " 'side': 'BUY',\n", + " 'size': '7.042251',\n", + " 'fee_rate_bps': '1000',\n", + " 'price': '0.71',\n", + " 'status': 'MATCHED',\n", + " 'match_time': '1774900601',\n", + " 'last_update': '1774900601',\n", + " 'outcome': 'Up',\n", + " 'owner': '00e5d36c-6c46-77e1-a436-0f0a4bfbfdd8',\n", + " 'trade_owner': '00e5d36c-6c46-77e1-a436-0f0a4bfbfdd8',\n", + " 'maker_address': '0xb2967A7e578E700E27611238B7F762BdADC72CcB',\n", + " 'transaction_hash': '0x454cbaad0da15d095be778773c362af90de43beb6660c78ff155b3877d283882',\n", + " 'bucket_index': 0,\n", + " 'maker_orders': [{'order_id': '0x4efdbcd86c81df9d6a02d7e35ff92411af76aa3ca6f445ba9d6e3773e812f706',\n", + " 'owner': '579ba9d5-cc34-1b66-3b70-d5bb5d1ccc18',\n", + " 'maker_address': '0x5Bde889dC26B097b5eAa2F1F027e01712EBCcbB7',\n", + " 'matched_amount': '7.042251',\n", + " 'price': '0.2900000298200107',\n", + " 'fee_rate_bps': '1000',\n", + " 'asset_id': '95594709608392853199445131908660268785353291802775571968225275873296398106331',\n", + " 'outcome': 'Down',\n", + " 'outcome_index': 0,\n", + " 'side': 'BUY'}],\n", + " 'trader_side': 'TAKER',\n", + " 'timestamp': '1774900601865',\n", + " 'event_type': 'trade'}" + ] + }, + "execution_count": 51, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "d" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "009fab36", + "metadata": {}, + "outputs": [], + "source": [] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [] + }, + { + "cell_type": "code", + "execution_count": 1, + "id": "56b8e1cc", + "metadata": {}, + "outputs": [], + "source": [ + "o = {'type': 'TRADE', 'id': '1de1c277-d281-4be5-a388-305beb95e713', 'taker_order_id': '0xc77331796ad40133a2b4d9a387c10d326c180ad1195306240416150e337f0376', 'market': '0x1cdd4f44f73da702120f02962bf02a2599a7573a39b05ac8ca12a24301542e3e', 'asset_id': '108194336731497303295771496985972597853032131719077291707737599874190461476381', 'side': 'BUY', 'size': '5', 'fee_rate_bps': '1000', 'price': '0.7', 'status': 'CONFIRMED', 'match_time': '1775025187', 'last_update': '1775025253', 'outcome': 'Up', 'owner': '00e5d36c-6c46-77e1-a436-0f0a4bfbfdd8', 'trade_owner': '00e5d36c-6c46-77e1-a436-0f0a4bfbfdd8', 'maker_address': '0xb2967A7e578E700E27611238B7F762BdADC72CcB', 'transaction_hash': '0x3654fac56e1548bcc14d01ce4888ea0c3cec29d553009e054fc67088ece5f5b3', 'bucket_index': 0, 'maker_orders': [{'order_id': '0xb9a39a5e8082846bfdd92d33fba5112466b939ad7f33b76905982549767acf0a', 'owner': '70dd335a-6089-6022-6726-565c7480b291', 'maker_address': '0x010138da36CF100c00a5A59C8643E4A4A55DF19C', 'matched_amount': '5', 'price': '0.3', 'fee_rate_bps': '1000', 'asset_id': '26175835004768067043908573558643253609047604517388456461023977022509795794691', 'outcome': 'Down', 'outcome_index': 0, 'side': 'BUY'}], 'trader_side': 'TAKER', 'timestamp': '1775025253290', 'event_type': 'trade'}" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "ae50f082", + "metadata": {}, + "outputs": [], + "source": [ + "USER_TRADES = [o]\n", + "user_trade = next( ( item for item in USER_TRADES if ( o['orderID'] == item['taker_order_id'] ) or ( o[\"orderID\"] == item['maker_orders'][0]['order_id'] ) ), None )" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [] + }, { "cell_type": "code", "execution_count": null, diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..70e7b45 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,22 @@ +pandas +rel +websockets +pyarrow +plotly +mysql-connector-python +sqlalchemy +requests +pymysql +scipy +asyncmy +cryptography +TA-Lib +valkey +nicegui +py_clob_client +# google +# google-api-core==2.30.0 +# google-api-python-client==2.190.0 +# googleapis-common-protos==1.72.0 +# grpcio==1.76.0 +# grpcio-tools==1.76.0 \ No newline at end of file diff --git a/test.py b/test.py new file mode 100644 index 0000000..717c72b --- /dev/null +++ b/test.py @@ -0,0 +1,63 @@ +import asyncio +import json +import websockets +import time + +# Credentials +API_KEY = "019d2ad3-3755-744b-ace8-ad0f08c958dd" +API_SECRET = "vXT1UeliaP89z9vcxDtdv47422mftijJkrJYE7CFqvA=" +API_PASSPHRASE = "57e703b801f22333d1a66a48c3a71773d3d3a42825ddcf330c3325856bc99756" +WS_URL = "wss://ws-subscriptions-clob.polymarket.com/ws/user" + +async def heartbeat(websocket): + """Sends a heartbeat every 10 seconds to keep the connection alive.""" + while True: + try: + await asyncio.sleep(10) + await websocket.send(json.dumps({})) + # print("Heartbeat sent") + except Exception: + break + +async def connect_polymarket_user_ws(): + while True: # Outer loop for reconnection + try: + async with websockets.connect(WS_URL) as websocket: + subscribe_message = { + "type": "user", + "auth": { + "apiKey": API_KEY, + "secret": API_SECRET, + "passphrase": API_PASSPHRASE + }, + "markets": [] + } + + await websocket.send(json.dumps(subscribe_message)) + print(f"[{time.strftime('%H:%M:%S')}] Subscription sent...") + + # Start the heartbeat task in the background + heartbeat_task = asyncio.create_task(heartbeat(websocket)) + + async for message in websocket: + data = json.loads(message) + + # Log the specific reason if it's an error message + if data.get("type") == "error": + print(f"Server Error: {data.get('message')}") + break + + if data: # Ignore empty heartbeat responses from server + print(f"Update: {data}") + + heartbeat_task.cancel() + + except Exception as e: + print(f"Connection lost: {e}. Retrying in 5s...") + await asyncio.sleep(5) + +if __name__ == "__main__": + try: + asyncio.run(connect_polymarket_user_ws()) + except KeyboardInterrupt: + print("Stopped by user.") \ No newline at end of file diff --git a/ws_binance.py b/ws_binance.py index 2202358..64dc0c4 100644 --- a/ws_binance.py +++ b/ws_binance.py @@ -13,6 +13,9 @@ from sqlalchemy import text import websockets from sqlalchemy.ext.asyncio import create_async_engine import valkey +import os +from dotenv import load_dotenv + ### Allow only ipv4 ### def allowed_gai_family(): @@ -26,13 +29,14 @@ VK_CHANNEL = 'poly_binance_btcusd' CON: AsyncContextManager | None = None VAL_KEY = None - ### Logging ### -LOG_FILEPATH: str = '/root/logs/Polymarket_Binance_Trades.log' +load_dotenv() +LOG_FILEPATH: str = os.getenv("LOGS_PATH") + '/Polymarket_Binance_Trades.log' ### Globals ### -WSS_URL = "wss://stream.binance.com:9443/ws/BTCUSDT@trade" -# HIST_TRADES = np.empty((0, 2)) +WSS_URL = "wss://stream.binance.com:9443/ws/BTCUSDT@aggTrade" +HIST_TRADES = np.empty((0, 3)) +HIST_TRADES_LOOKBACK_SEC = 5 ### Database Funcs ### async def create_rtds_btcusd_table( @@ -46,6 +50,7 @@ async def create_rtds_btcusd_table( logging.info('Creating Table if Does Not Exist: binance_btcusd_trades') await CON.execute(text(""" CREATE TABLE IF NOT EXISTS binance_btcusd_trades ( + timestamp_arrival BIGINT, timestamp_msg BIGINT, timestamp_value BIGINT, value DOUBLE, @@ -57,6 +62,7 @@ async def create_rtds_btcusd_table( raise ValueError('Only MySQL engine is implemented') async def insert_rtds_btcusd_table( + timestamp_arrival: int, timestamp_msg: int, timestamp_value: int, value: float, @@ -65,6 +71,7 @@ async def insert_rtds_btcusd_table( engine: str = 'mysql', # mysql | duckdb ) -> None: params={ + 'timestamp_arrival': timestamp_arrival, 'timestamp_msg': timestamp_msg, 'timestamp_value': timestamp_value, 'value': value, @@ -77,6 +84,7 @@ async def insert_rtds_btcusd_table( await CON.execute(text(""" INSERT INTO binance_btcusd_trades ( + timestamp_arrival, timestamp_msg, timestamp_value, value, @@ -84,6 +92,7 @@ async def insert_rtds_btcusd_table( ) VALUES ( + :timestamp_arrival, :timestamp_msg, :timestamp_value, :value, @@ -96,7 +105,6 @@ async def insert_rtds_btcusd_table( else: raise ValueError('Only MySQL engine is implemented') - ### Websocket ### async def binance_trades_stream(): global HIST_TRADES @@ -106,7 +114,7 @@ async def binance_trades_stream(): subscribe_msg = { "method": "SUBSCRIBE", - "params": ["btcusdt@trade"], + "params": ["btcusdt@aggTrade"], "id": 1 } @@ -114,30 +122,36 @@ async def binance_trades_stream(): try: async for message in websocket: + ts_arrival = round(datetime.now().timestamp()*1000) if isinstance(message, str): try: data = json.loads(message) - if data.get('t', None) is not None: + if data.get('T', None) is not None: + timestamp_msg = data['E'] + timestamp_value = data['T'] last_px = float(data['p']) - print(f'🤑 BTC Binance Last Px: {last_px:_.4f}; TS: {pd.to_datetime(data['T'], unit='ms')}') - VAL_KEY.publish(VK_CHANNEL, json.dumps({ - 'timestamp_msg': data['E'], - 'timestamp_value': data['T'], + qty = float(data['q']) + # print(f'🤑 BTC Binance Last Px: {last_px:_.4f}; TS: {pd.to_datetime(data['T'], unit='ms')}') + HIST_TRADES = np.append(HIST_TRADES, np.array([[timestamp_value, last_px, qty]]), axis=0) + hist_trades_lookback_ts_ms = round(datetime.now().timestamp() - HIST_TRADES_LOOKBACK_SEC)*1000 + HIST_TRADES = HIST_TRADES[HIST_TRADES[:, 0] >= hist_trades_lookback_ts_ms] + VAL_KEY_OBJ = json.dumps({ + 'timestamp_arrival': ts_arrival, + 'timestamp_msg': timestamp_msg, + 'timestamp_value': timestamp_value, 'value': last_px, - 'qty': data['q'], - })) - VAL_KEY.set(VK_CHANNEL, json.dumps({ - 'timestamp_msg': data['E'], - 'timestamp_value': data['T'], - 'value': last_px, - 'qty': data['q'], - })) + 'qty': qty, + 'hist_trades': HIST_TRADES.tolist() + }) + # VAL_KEY.publish(VK_CHANNEL, VAL_KEY_OBJ) + VAL_KEY.set(VK_CHANNEL, VAL_KEY_OBJ) await insert_rtds_btcusd_table( CON=CON, - timestamp_msg=data['E'], - timestamp_value=data['T'], + timestamp_arrival=ts_arrival, + timestamp_msg=timestamp_msg, + timestamp_value=timestamp_value, value=last_px, - qty=data['q'], + qty=qty, ) else: logging.info(f'Initial or unexpected data struct, skipping: {data}') @@ -162,8 +176,8 @@ async def main(): if USE_VK: VAL_KEY = valkey.Valkey(host='localhost', port=6379, db=0) - published_count = VAL_KEY.publish(VK_CHANNEL,f"Hola, starting to publish to valkey: {VK_CHANNEL} @ {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}") - logging.info(f"Valkey message published to {published_count} subscribers of {VK_CHANNEL}") + # published_count = VAL_KEY.publish(VK_CHANNEL,f"Hola, starting to publish to valkey: {VK_CHANNEL} @ {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}") + # logging.info(f"Valkey message published to {published_count} subscribers of {VK_CHANNEL}") else: VAL_KEY = None logging.warning("VALKEY NOT BEING USED, NO DATA WILL BE PUBLISHED") diff --git a/ws_binance/Dockerfile b/ws_binance/Dockerfile new file mode 100644 index 0000000..f2899de --- /dev/null +++ b/ws_binance/Dockerfile @@ -0,0 +1,19 @@ +FROM python:3.13-slim + +RUN apt-get update && \ + apt-get install -y build-essential + +RUN gcc --version +RUN rm -rf /var/lib/apt/lists/* + +WORKDIR /app + +COPY requirements.txt . + +RUN pip install --no-cache-dir -r requirements.txt + +COPY . . + +# Finally, run gunicorn. +CMD [ "python", "ws_binance.py"] +# CMD [ "gunicorn", "--workers=5", "--threads=1", "-b 0.0.0.0:8000", "app:server"] \ No newline at end of file diff --git a/ws_clob.py b/ws_clob.py index 3a2cd6b..edef2ad 100644 --- a/ws_clob.py +++ b/ws_clob.py @@ -14,22 +14,34 @@ from sqlalchemy.ext.asyncio import create_async_engine from sqlalchemy import text import valkey +import os +from dotenv import load_dotenv + ### Database ### USE_DB: bool = True USE_VK: bool = True VK_CHANNEL = 'poly_5min_btcusd' +VK_CHANNEL_DOWN = 'poly_5min_btcusd_down' CON: AsyncContextManager | None = None VAL_KEY = None ### Logging ### -LOG_FILEPATH: str = '/root/logs/Polymarket_5min.log' +load_dotenv() +LOG_FILEPATH: str = os.getenv("LOGS_PATH") + '/Polymarket_5min.log' WSS_URL = "wss://ws-subscriptions-clob.polymarket.com/ws/market" SLUG_END_TIME = 0 HIST_TRADES = np.empty((0, 2)) +HIST_TRADES_DOWN = np.empty((0, 2)) +MIN_TICK_SIZE = 0.01 +NEG_RISK = False + TARGET_PX = 0 +TARGET_ASSET_ID = None +TARGET_ASSET_ID_DOWN = None + def format_timestamp(total_seconds) -> str: minutes, seconds = divmod(total_seconds, 60) @@ -86,7 +98,8 @@ async def create_poly_btcusd_trades_table( timestamp_value BIGINT, price DOUBLE, qty DOUBLE, - side_taker VARCHAR(8) + side_taker VARCHAR(8), + up_or_down VARCHAR(8) ); """)) await CON.commit() @@ -100,6 +113,7 @@ async def insert_poly_btcusd_trades_table( price: float, qty: float, side_taker: str, + up_or_down: str, CON: AsyncContextManager, engine: str = 'mysql', # mysql | duckdb ) -> None: @@ -110,6 +124,7 @@ async def insert_poly_btcusd_trades_table( 'price': price, 'qty': qty, 'side_taker': side_taker, + 'up_or_down': up_or_down, } if CON is None: logging.info("NO DB CONNECTION, SKIPPING Insert Statements") @@ -123,7 +138,8 @@ async def insert_poly_btcusd_trades_table( timestamp_value, price, qty, - side_taker + side_taker, + up_or_down ) VALUES ( @@ -132,7 +148,8 @@ async def insert_poly_btcusd_trades_table( :timestamp_value, :price, :qty, - :side_taker + :side_taker, + :up_or_down ) """), parameters=params @@ -146,24 +163,33 @@ async def polymarket_stream(): global SLUG_END_TIME global TARGET_PX global HIST_TRADES + global HIST_TRADES_DOWN + global MIN_TICK_SIZE + global NEG_RISK + global TARGET_ASSET_ID + global TARGET_ASSET_ID_DOWN slug_full = gen_slug() - market_details, market = get_mkt_details_by_slug(slug_full) + market_details, _ = get_mkt_details_by_slug(slug_full) + CONDITION_ID = market_details['ConditionId'] TARGET_ASSET_ID = market_details['Up'] + TARGET_ASSET_ID_DOWN = market_details['Down'] + MIN_TICK_SIZE = market_details['MinTickSize'] + NEG_RISK = market_details['isNegRisk'] SLUG_END_TIME = round(datetime.strptime(market_details['EndDateTime'], '%Y-%m-%dT%H:%M:%SZ').replace(tzinfo=timezone.utc).timestamp()) print(f'********* NEW MKT - END DATETIME: {pd.to_datetime(SLUG_END_TIME, unit='s')} *********') - async with websockets.connect(WSS_URL) as websocket: + async for websocket in websockets.connect(WSS_URL): print(f"Connected to {WSS_URL}") subscribe_msg = { - "assets_ids": [TARGET_ASSET_ID], + "assets_ids": [TARGET_ASSET_ID, TARGET_ASSET_ID_DOWN], "type": "market", - "custom_feature_enabled": True + "custom_feature_enabled": False } await websocket.send(json.dumps(subscribe_msg)) - print(f"Subscribed to Asset: {TARGET_ASSET_ID}") + print(f"Subscribed to Assets: Up {TARGET_ASSET_ID}; Down: {TARGET_ASSET_ID_DOWN}") try: async for message in websocket: @@ -178,21 +204,25 @@ async def polymarket_stream(): print('*** Attempting to unsub from past 5min') update_unsub_msg = { "operation": 'unsubscribe', - "assets_ids": [TARGET_ASSET_ID], - "custom_feature_enabled": True + "assets_ids": [TARGET_ASSET_ID, TARGET_ASSET_ID_DOWN], + "custom_feature_enabled": False } await websocket.send(json.dumps(update_unsub_msg)) print('*** Attempting to SUB to new 5min') slug_full = gen_slug() market_details, market = get_mkt_details_by_slug(slug_full) + CONDITION_ID = market_details['ConditionId'] TARGET_ASSET_ID = market_details['Up'] + TARGET_ASSET_ID_DOWN = market_details['Down'] + MIN_TICK_SIZE = market_details['MinTickSize'] + NEG_RISK = market_details['isNegRisk'] SLUG_END_TIME = round(datetime.strptime(market_details['EndDateTime'], '%Y-%m-%dT%H:%M:%SZ').replace(tzinfo=timezone.utc).timestamp()) update_sub_msg = { "operation": 'subscribe', - "assets_ids": [TARGET_ASSET_ID], - "custom_feature_enabled": True + "assets_ids": [TARGET_ASSET_ID, TARGET_ASSET_ID_DOWN], + "custom_feature_enabled": False } await websocket.send(json.dumps(update_sub_msg)) @@ -208,34 +238,65 @@ async def polymarket_stream(): if event_type == "price_change": # print("📈 Price Change") # print(pd.DataFrame(data['price_changes'])) - pass + continue elif event_type == "best_bid_ask": # print(pd.DataFrame([data])) - pass + continue elif event_type == "last_trade_price": + token_id = data['asset_id'] ts_msg = int(data['timestamp']) ts_value = int(ts_msg) px = float(data['price']) qty = float(data['size']) side_taker = data['side'] - HIST_TRADES = np.append(HIST_TRADES, np.array([[px, qty]]), axis=0) - # SMA = talib.ROC(HIST_TRADES[:,0], timeperiod=10)[-1] - # print(f"✨ Last Px: {px:.2f}; ROC: {SMA:.4f}; Qty: {qty:6.2f}; Sec Left: {sec_remaining}") - print(f"✨ Last Px: {px:.2f}; Qty: {qty:6.2f}; Sec Left: {sec_remaining}") - - if USE_VK: - VAL_KEY_OBJ = json.dumps({ - 'timestamp_arrival': ts_arrival, - 'timestamp_msg': ts_msg, - 'timestamp_value': ts_value, - 'price': px, - 'qty': qty, - 'side_taker': side_taker, - 'sec_remaining': sec_remaining, - 'target_price': TARGET_PX - }) - VAL_KEY.publish(VK_CHANNEL, VAL_KEY_OBJ) - VAL_KEY.set(VK_CHANNEL, VAL_KEY_OBJ) + if token_id == TARGET_ASSET_ID: + up_or_down = 'UP' + HIST_TRADES = np.append(HIST_TRADES, np.array([[px, qty]]), axis=0) + # print(f"✨ Last Px: {px:.2f}; Qty: {qty:6.2f}; Sec Left: {sec_remaining}") + # print(f'Up: {TARGET_ASSET_ID}') + # print(f'Down: {TARGET_ASSET_ID_DOWN}') + # SMA = talib.ROC(HIST_TRADES[:,0], timeperiod=10)[-1] + # print(f"✨ Last Px: {px:.2f}; ROC: {SMA:.4f}; Qty: {qty:6.2f}; Sec Left: {sec_remaining}") + if USE_VK: + VAL_KEY_OBJ = json.dumps({ + 'timestamp_arrival': ts_arrival, + 'timestamp_msg': ts_msg, + 'timestamp_value': ts_value, + 'price': px, + 'qty': qty, + 'side_taker': side_taker, + 'sec_remaining': sec_remaining, + 'target_price': TARGET_PX, + 'condition_id': CONDITION_ID, + 'token_id_up': TARGET_ASSET_ID, + 'token_id_down': TARGET_ASSET_ID_DOWN, + 'tick_size': MIN_TICK_SIZE, + 'neg_risk': NEG_RISK, + }) + VAL_KEY.set(VK_CHANNEL, VAL_KEY_OBJ) + elif token_id == TARGET_ASSET_ID_DOWN: + up_or_down = 'DOWN' + HIST_TRADES_DOWN = np.append(HIST_TRADES_DOWN, np.array([[px, qty]]), axis=0) + if USE_VK: + VAL_KEY_OBJ = json.dumps({ + 'timestamp_arrival': ts_arrival, + 'timestamp_msg': ts_msg, + 'timestamp_value': ts_value, + 'price': px, + 'qty': qty, + 'side_taker': side_taker, + 'sec_remaining': sec_remaining, + 'target_price': TARGET_PX, + 'condition_id': CONDITION_ID, + 'token_id_up': TARGET_ASSET_ID, + 'token_id_down': TARGET_ASSET_ID_DOWN, + 'tick_size': MIN_TICK_SIZE, + 'neg_risk': NEG_RISK, + }) + VAL_KEY.set(VK_CHANNEL_DOWN, VAL_KEY_OBJ) + else: + logging.warning('Token Id from Market Does Not Match Pricing Data Id') + if USE_DB: await insert_poly_btcusd_trades_table( CON=CON, @@ -245,29 +306,34 @@ async def polymarket_stream(): price=px, qty=qty, side_taker=side_taker, + up_or_down=up_or_down ) elif event_type == "book": - pass + continue elif event_type == "new_market": print('Received new_market') + continue elif event_type == "market_resolved": print(f"Received: {event_type}") print(data) + continue elif event_type == "tick_size_change": # may want for CLOB order routing print(f"Received: {event_type}") print(data) + continue else: print(f"*********** REC UNMAPPED EVENT: {event_type}") print(data) + continue elif isinstance(data, dict): - # print(data.get("event_type", None)) - pass + continue else: raise ValueError(f'Type: {type(data)} not expected: {message}') except websockets.ConnectionClosed as e: print(f"Connection closed by server. Exception: {e}") + continue async def main(): global VAL_KEY @@ -275,8 +341,8 @@ async def main(): if USE_VK: VAL_KEY = valkey.Valkey(host='localhost', port=6379, db=0) - published_count = VAL_KEY.publish(VK_CHANNEL,f"Hola, starting to publish to valkey: {VK_CHANNEL} @ {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}") - logging.info(f"Valkey message published to {published_count} subscribers of {VK_CHANNEL}") + # published_count = VAL_KEY.publish(VK_CHANNEL,f"Hola, starting to publish to valkey: {VK_CHANNEL} @ {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}") + # logging.info(f"Valkey message published to {published_count} subscribers of {VK_CHANNEL}") else: VAL_KEY = None logging.warning("VALKEY NOT BEING USED, NO DATA WILL BE PUBLISHED") diff --git a/ws_clob/Dockerfile b/ws_clob/Dockerfile new file mode 100644 index 0000000..748b47f --- /dev/null +++ b/ws_clob/Dockerfile @@ -0,0 +1,19 @@ +FROM python:3.13-slim + +RUN apt-get update && \ + apt-get install -y build-essential + +RUN gcc --version +RUN rm -rf /var/lib/apt/lists/* + +WORKDIR /app + +COPY requirements.txt . + +RUN pip install --no-cache-dir -r requirements.txt + +COPY . . + +# Finally, run gunicorn. +CMD [ "python", "ws_clob.py"] +# CMD [ "gunicorn", "--workers=5", "--threads=1", "-b 0.0.0.0:8000", "app:server"] \ No newline at end of file diff --git a/ws_coinbase.py b/ws_coinbase.py index e4bc138..93d5758 100644 --- a/ws_coinbase.py +++ b/ws_coinbase.py @@ -5,14 +5,16 @@ import socket import traceback from datetime import datetime from typing import AsyncContextManager - -import numpy as np +import os +# import numpy as np import pandas as pd import requests.packages.urllib3.util.connection as urllib3_cn # type: ignore from sqlalchemy import text import websockets from sqlalchemy.ext.asyncio import create_async_engine import valkey +import os +from dotenv import load_dotenv ### Allow only ipv4 ### def allowed_gai_family(): @@ -27,7 +29,8 @@ CON: AsyncContextManager | None = None VAL_KEY = None ### Logging ### -LOG_FILEPATH: str = '/root/logs/Polymarket_coinbase_Trades.log' +load_dotenv() +LOG_FILEPATH: str = os.getenv("LOGS_PATH") + '/Polymarket_coinbase_Trades.log' ### Globals ### WSS_URL = "wss://ws-feed.exchange.coinbase.com" diff --git a/ws_coinbase/Dockerfile b/ws_coinbase/Dockerfile new file mode 100644 index 0000000..ceba70c --- /dev/null +++ b/ws_coinbase/Dockerfile @@ -0,0 +1,19 @@ +FROM python:3.13-slim + +RUN apt-get update && \ + apt-get install -y build-essential + +RUN gcc --version +RUN rm -rf /var/lib/apt/lists/* + +WORKDIR /app + +COPY requirements.txt . + +RUN pip install --no-cache-dir -r requirements.txt + +COPY . . + +# Finally, run gunicorn. +CMD [ "python", "ws_coinbase.py"] +# CMD [ "gunicorn", "--workers=5", "--threads=1", "-b 0.0.0.0:8000", "app:server"] \ No newline at end of file diff --git a/ws_pionex.py b/ws_pionex.py index 25ec648..5c6efb4 100644 --- a/ws_pionex.py +++ b/ws_pionex.py @@ -13,6 +13,8 @@ from sqlalchemy import text import websockets from sqlalchemy.ext.asyncio import create_async_engine import valkey +import os +from dotenv import load_dotenv ### Allow only ipv4 ### def allowed_gai_family(): @@ -28,7 +30,8 @@ VAL_KEY = None ### Logging ### -LOG_FILEPATH: str = '/root/logs/Polymarket_Pionex_Trades.log' +load_dotenv() +LOG_FILEPATH: str = os.getenv("LOGS_PATH") + '/Polymarket_Pionex_Trades.log' ### Globals ### WSS_URL = "wss://ws.pionex.com/wsPub" diff --git a/ws_rtds.py b/ws_rtds.py index 1325b38..73663c9 100644 --- a/ws_rtds.py +++ b/ws_rtds.py @@ -13,6 +13,8 @@ from sqlalchemy import text import websockets from sqlalchemy.ext.asyncio import create_async_engine import valkey +import os +from dotenv import load_dotenv ### Allow only ipv4 ### def allowed_gai_family(): @@ -28,7 +30,8 @@ VAL_KEY = None ### Logging ### -LOG_FILEPATH: str = '/root/logs/Polymarket_RTDS.log' +load_dotenv() +LOG_FILEPATH: str = os.getenv("LOGS_PATH") + '/Polymarket_RTDS.log' ### Globals ### WSS_URL = "wss://ws-live-data.polymarket.com" @@ -124,14 +127,14 @@ async def rtds_stream(): data = json.loads(message) if data['payload'].get('value', None) is not None: ts_arrival = round(datetime.now().timestamp()*1000) - print(f'🤑 BTC Chainlink Last Px: {data['payload']['value']:_.4f}; TS: {pd.to_datetime(data['payload']['timestamp'], unit='ms')}') + # print(f'🤑 BTC Chainlink Last Px: {data['payload']['value']:_.4f}; TS: {pd.to_datetime(data['payload']['timestamp'], unit='ms')}') VAL_KEY_OBJ = json.dumps({ 'timestamp_arrival': ts_arrival, 'timestamp_msg': data['timestamp'], 'timestamp_value': data['payload']['timestamp'], 'value': data['payload']['value'], }) - VAL_KEY.publish(VK_CHANNEL, VAL_KEY_OBJ) + # VAL_KEY.publish(VK_CHANNEL, VAL_KEY_OBJ) VAL_KEY.set(VK_CHANNEL, VAL_KEY_OBJ) await insert_rtds_btcusd_table( CON=CON, @@ -164,8 +167,8 @@ async def main(): if USE_VK: VAL_KEY = valkey.Valkey(host='localhost', port=6379, db=0) - published_count = VAL_KEY.publish(VK_CHANNEL,f"Hola, starting to publish to valkey: {VK_CHANNEL} @ {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}") - logging.info(f"Valkey message published to {published_count} subscribers of {VK_CHANNEL}") + # published_count = VAL_KEY.publish(VK_CHANNEL,f"Hola, starting to publish to valkey: {VK_CHANNEL} @ {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}") + # logging.info(f"Valkey message published to {published_count} subscribers of {VK_CHANNEL}") else: VAL_KEY = None logging.warning("VALKEY NOT BEING USED, NO DATA WILL BE PUBLISHED") diff --git a/ws_rtds/Dockerfile b/ws_rtds/Dockerfile new file mode 100644 index 0000000..5cb3f84 --- /dev/null +++ b/ws_rtds/Dockerfile @@ -0,0 +1,19 @@ +FROM python:3.13-slim + +RUN apt-get update && \ + apt-get install -y build-essential + +RUN gcc --version +RUN rm -rf /var/lib/apt/lists/* + +WORKDIR /app + +COPY requirements.txt . + +RUN pip install --no-cache-dir -r requirements.txt + +COPY . . + +# Finally, run gunicorn. +CMD [ "python", "ws_rtds.py"] +# CMD [ "gunicorn", "--workers=5", "--threads=1", "-b 0.0.0.0:8000", "app:server"] \ No newline at end of file diff --git a/ws_user.py b/ws_user.py new file mode 100644 index 0000000..fbdda9e --- /dev/null +++ b/ws_user.py @@ -0,0 +1,431 @@ +import asyncio +import json +import logging +import os +from datetime import datetime +from typing import AsyncContextManager + +import numpy as np +import valkey +import websockets +from dotenv import load_dotenv +from py_clob_client.client import ClobClient +from sqlalchemy import text +from sqlalchemy.ext.asyncio import create_async_engine + +### Database ### +USE_DB: bool = True +USE_VK: bool = True + +LOCAL_LIVE_ORDERS = [] +LOCAL_RECENT_TRADES = [] +LOCAL_RECENT_TRADES_LOOKBACK_SEC = 10 + +VK_LIVE_ORDERS = 'poly_user_orders' +VK_RECENT_TRADES = 'poly_user_trades' +CON: AsyncContextManager | None = None +VAL_KEY = None + +### Logging ### +load_dotenv() +LOG_FILEPATH: str = os.getenv("LOGS_PATH") + '/Polymarket_User.log' + +# https://docs.polymarket.com/market-data/websocket/user-channel +WSS_URL = "wss://ws-subscriptions-clob.polymarket.com/ws/user" +API_CREDS = {} + +HIST_TRADES = np.empty((0, 2)) +TARGET_PX = 0 + +### Database Funcs ### +async def create_user_trades_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: user_stream_trades') + await CON.execute(text(""" + CREATE TABLE IF NOT EXISTS user_stream_trades ( + -- event_type VARCHAR(8), + timestamp_arrival BIGINT, + type VARCHAR(20), + id VARCHAR(100), + taker_order_id VARCHAR(100), + market VARCHAR(100), + asset_id VARCHAR(100), + side VARCHAR(8), + size DOUBLE, + price DOUBLE, + fee_rate_bps DOUBLE, + status VARCHAR(20), + matchtime BIGINT, + last_update BIGINT, + outcome VARCHAR(20), + owner VARCHAR(100), + trade_owner VARCHAR(100), + maker_address VARCHAR(100), + transaction_hash VARCHAR(100), + bucket_index INT, + maker_orders JSON NULL, + trader_side VARCHAR(8), + timestamp BIGINT + ); + """)) + await CON.commit() + else: + raise ValueError('Only MySQL engine is implemented') + +async def insert_user_trades_table( + params: dict, + CON: AsyncContextManager, + engine: str = 'mysql', # mysql | duckdb + ) -> None: + if CON is None: + logging.info("NO DB CONNECTION, SKIPPING Insert Statements") + else: + if engine == 'mysql': + await CON.execute(text(""" + INSERT INTO user_stream_trades + ( + timestamp_arrival, + type, + id, + taker_order_id, + market, + asset_id, + side, + size, + price, + fee_rate_bps, + status, + matchtime, + last_update, + outcome, + owner, + trade_owner, + maker_address, + transaction_hash, + bucket_index, + maker_orders, + trader_side, + timestamp + ) + VALUES + ( + :timestamp_arrival, + :type, + :id, + :taker_order_id, + :market, + :asset_id, + :side, + :size, + :price, + :fee_rate_bps, + :status, + :matchtime, + :last_update, + :outcome, + :owner, + :trade_owner, + :maker_address, + :transaction_hash, + :bucket_index, + :maker_orders, + :trader_side, + :timestamp + ) + """), + parameters=params + ) + await CON.commit() + else: + raise ValueError('Only MySQL engine is implemented') + + +async def create_user_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: user_stream_orders') + await CON.execute(text(""" + CREATE TABLE IF NOT EXISTS user_stream_orders ( + -- event_type VARCHAR(8), + timestamp_arrival BIGINT, + id VARCHAR(100), + owner VARCHAR(100), + market VARCHAR(100), + asset_id VARCHAR(100), + side VARCHAR(8), + order_owner VARCHAR(100), + original_size DOUBLE, + size_matched DOUBLE, + price DOUBLE, + associate_trades JSON NULL, + outcome VARCHAR(20), + type VARCHAR(20), + created_at BIGINT, + expiration VARCHAR(20), + order_type VARCHAR(8), + status VARCHAR(20), + maker_address VARCHAR(100), + timestamp BIGINT + ); + """)) + await CON.commit() + else: + raise ValueError('Only MySQL engine is implemented') + +async def insert_user_orders_table( + params: dict, + CON: AsyncContextManager, + engine: str = 'mysql', # mysql | duckdb + ) -> None: + if CON is None: + logging.info("NO DB CONNECTION, SKIPPING Insert Statements") + else: + if engine == 'mysql': + await CON.execute(text(""" + INSERT INTO user_stream_orders + ( + timestamp_arrival, + id, + owner, + market, + asset_id, + side, + order_owner, + original_size, + size_matched, + price, + associate_trades, + outcome, + type, + created_at, + expiration, + order_type, + status, + maker_address, + timestamp + ) + VALUES + ( + :timestamp_arrival, + :id, + :owner, + :market, + :asset_id, + :side, + :order_owner, + :original_size, + :size_matched, + :price, + :associate_trades, + :outcome, + :type, + :created_at, + :expiration, + :order_type, + :status, + :maker_address, + :timestamp + ) + """), + parameters=params + ) + await CON.commit() + else: + raise ValueError('Only MySQL engine is implemented') + +### Helpers ### +def live_orders_only(orders: list[dict]) -> list[dict]: + return [d for d in orders if d.get('status')=='LIVE'] + +def upsert_list_of_dicts_by_id(list_of_dicts, new_dict): + for index, item in enumerate(list_of_dicts): + if item.get('id') == new_dict.get('id'): + list_of_dicts[index] = new_dict + return list_of_dicts + + list_of_dicts.append(new_dict) + return list_of_dicts + + +async def polymarket_stream(): + global TARGET_PX + global HIST_TRADES + global LOCAL_LIVE_ORDERS + global LOCAL_RECENT_TRADES + + POLY_API_KEY = API_CREDS.api_key + POLY_API_SECRET = API_CREDS.api_secret + POLY_API_PASS = API_CREDS.api_passphrase + + async for websocket in websockets.connect(WSS_URL): + print(f"Connected to {WSS_URL}") + + subscribe_msg = { + "auth": { + "apiKey": POLY_API_KEY, + "secret": POLY_API_SECRET, + "passphrase": POLY_API_PASS, + }, + "type": "user", + "markets": [] + } + + await websocket.send(json.dumps(subscribe_msg)) + print("Subscribed to User Data") + + + try: + async for message in websocket: + ts_arrival = round(datetime.now().timestamp()*1000) + if isinstance(message, str): + data = json.loads(message) + if data == {}: # Handle empty server ping - return pong + await websocket.send(json.dumps({})) + print('SENT HEARTBEAT PING') + continue + data['timestamp_arrival'] = ts_arrival + + event_type = data.get('event_type', None) + match event_type: + case 'trade': + logging.info(f'TRADE: {data}') + # trade_status = data.get('status') + # match trade_status: # Raise TELEGRAM ALERT ??? + # case 'MATCHED': + # pass + # case 'MINED': + # pass + # case 'CONFIRMED': + # pass + # case 'RETRYING': + # pass + # case 'FAILED': + # pass + + ### Convert Datatypes ### + data['size'] = float(data['size']) + data['price'] = float(data['price']) + data['fee_rate_bps'] = float(data['fee_rate_bps']) + data['matchtime'] = int(data['match_time']) + data['last_update'] = int(data['last_update']) + data['timestamp'] = int(data['timestamp']) + data['maker_orders'] = json.dumps(data['maker_orders']) if data['maker_orders'] else None + + LOCAL_RECENT_TRADES = upsert_list_of_dicts_by_id(LOCAL_RECENT_TRADES, data) + LOOKBACK_MIN_TS_MS = ts_arrival-LOCAL_RECENT_TRADES_LOOKBACK_SEC*1000 + LOCAL_RECENT_TRADES = [t for t in LOCAL_RECENT_TRADES if t.get('timestamp_arrival', 0) >= LOOKBACK_MIN_TS_MS] + + print("---------------------") + print(LOCAL_RECENT_TRADES) + print("---------------------") + + VAL_KEY_OBJ = json.dumps(LOCAL_RECENT_TRADES) + # VAL_KEY.publish(VK_CHANNEL, VAL_KEY_OBJ) + VAL_KEY.set(VK_RECENT_TRADES, VAL_KEY_OBJ) + + logging.info(f'User Trade Update: {data}') + + ### Insert into DB ### + await insert_user_trades_table( + params=data, + CON=CON + ) + case 'order': + logging.info(f'ORDER: {data}') + ### Convert Datatypes ### + data['original_size'] = float(data['original_size']) + data['size_matched'] = float(data['size_matched']) + data['price'] = float(data['price']) + data['associate_trades'] = json.dumps(data['associate_trades']) if data['associate_trades'] else None + data['created_at'] = int(data['created_at']) + data['timestamp'] = int(data['timestamp']) + + ### Match on Status - Pass Live orders to Valkey for Algo Engine ### + order_status = data.get('status') + match order_status: + case 'live': + LOCAL_LIVE_ORDERS = upsert_list_of_dicts_by_id(LOCAL_LIVE_ORDERS, data) + LOCAL_LIVE_ORDERS = live_orders_only(LOCAL_LIVE_ORDERS) + VAL_KEY_OBJ = json.dumps(LOCAL_LIVE_ORDERS) + # VAL_KEY.publish(VK_CHANNEL, VAL_KEY_OBJ) + VAL_KEY.set(VK_LIVE_ORDERS, VAL_KEY_OBJ) + logging.info(f'Order(s) RESTING: {data}') + case 'matched': + logging.info(f'Order(s) MATCHED: {data}') + case 'delayed': + raise ValueError(f'Order Status of "delayed" which is not expected for non-sports orders: {data}') + case 'unmatched': + raise ValueError(f'Order Status of "unmatched" which is not expected for non-sports orders: {data}') + + ### Insert into DB ### + await insert_user_orders_table( + params=data, + CON=CON, + ) + else: + raise ValueError(f'Type: {type(data)} not expected: {message}') + + except websockets.ConnectionClosed as e: + print(f"Connection closed by server. Exception: {e}") + +async def main(): + global VAL_KEY + global CON + global API_CREDS + + private_key = os.getenv("PRIVATE_KEY") + host = "https://clob.polymarket.com" + chain_id = 137 # Polygon mainnet + + temp_client = ClobClient(host, key=private_key, chain_id=chain_id) + API_CREDS = temp_client.create_or_derive_api_creds() + + if USE_VK: + VAL_KEY = valkey.Valkey(host='localhost', port=6379, db=0) + # published_count = VAL_KEY.publish(VK_CHANNEL,f"Hola, starting to publish to valkey: {VK_CHANNEL} @ {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}") + # logging.info(f"Valkey message published to {published_count} subscribers of {VK_CHANNEL}") + else: + VAL_KEY = None + logging.warning("VALKEY NOT BEING USED, NO DATA WILL BE PUBLISHED") + + if USE_DB: + engine = create_async_engine('mysql+asyncmy://root:pwd@localhost/polymarket') + async with engine.connect() as CON: + await create_user_trades_table(CON=CON) + await create_user_orders_table(CON=CON) + await polymarket_stream() + else: + CON = None + logging.warning("DATABASE NOT BEING USED, NO DATA WILL BE RECORDED") + await polymarket_stream() + + +if __name__ == '__main__': + START_TIME = round(datetime.now().timestamp()*1000) + + logging.info(f'Log FilePath: {LOG_FILEPATH}') + + logging.basicConfig( + force=True, + filename=LOG_FILEPATH, + level=logging.INFO, + format='%(asctime)s - %(levelname)s - %(message)s', + filemode='w' + ) + logging.info(f"STARTED: {START_TIME}") + + try: + asyncio.run(main()) + except KeyboardInterrupt as e: + print(f"Stream stopped: {e}") \ No newline at end of file diff --git a/ws_user/Dockerfile b/ws_user/Dockerfile new file mode 100644 index 0000000..0753635 --- /dev/null +++ b/ws_user/Dockerfile @@ -0,0 +1,19 @@ +FROM python:3.13-slim + +RUN apt-get update && \ + apt-get install -y build-essential + +RUN gcc --version +RUN rm -rf /var/lib/apt/lists/* + +WORKDIR /app + +COPY requirements.txt . + +RUN pip install --no-cache-dir -r requirements.txt + +COPY . . + +# Finally, run gunicorn. +CMD [ "python", "ws_user.py"] +# CMD [ "gunicorn", "--workers=5", "--threads=1", "-b 0.0.0.0:8000", "app:server"] \ No newline at end of file -- 2.49.1 From f5d803759484a8ac377b2481ffe37fccf5f86c80 Mon Sep 17 00:00:00 2001 From: stevekeyharvey Date: Thu, 2 Apr 2026 02:38:01 +0000 Subject: [PATCH 4/5] start of legs --- main.py | 356 +++++++++++++++++++++++++++++++++++------------------ ws_user.py | 8 +- 2 files changed, 237 insertions(+), 127 deletions(-) diff --git a/main.py b/main.py index 6ae4a8a..ac6e97f 100644 --- a/main.py +++ b/main.py @@ -19,7 +19,8 @@ from py_clob_client.clob_types import ( OrderType, PartialCreateOrderOptions, PostOrdersArgs, - BalanceAllowanceParams + BalanceAllowanceParams, + OpenOrderParams ) from py_clob_client.order_builder.constants import BUY, SELL from sqlalchemy import text @@ -31,6 +32,7 @@ import modules.api as api @dataclass class Custom_OrderArgs(OrderArgs): max_price: float = 0.00 + post_only: bool = False ### Database ### @@ -45,10 +47,10 @@ 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.1 # In Percent % Chg (e.g. 0.02 == 0.02%) +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.02 +TGT_PROFIT_CENTS = 0.04 CHASE_TO_BUY_CENTS = 0.05 MAX_ALLOWED_POLY_PX = 0.90 @@ -226,9 +228,9 @@ async def slope_decision() -> list[bool, str]: # 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%}') + # 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) @@ -263,12 +265,25 @@ async def cancel_single_order_by_id(CLIENT, order_id): 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 False + 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}') - LOCAL_ACTIVE_ORDERS.pop(idx) - return False + # 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') @@ -282,8 +297,12 @@ 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 up > MIN_ORDER_SIZE: + if abs(up) > MIN_ORDER_SIZE: logging.info(f'Flattening Up Position: {up}') await post_order( CLIENT = CLIENT, @@ -291,12 +310,12 @@ async def flatten_open_positions(CLIENT, token_id_up, token_id_down): neg_risk = POLY_CLOB['neg_risk'], OrderArgs_list = [Custom_OrderArgs( token_id=token_id_up, - price=float(POLY_CLOB['price'])-0.01, + price=float(POLY_CLOB['price'])-0.05, size=up, side=SELL, )] ) - if down > MIN_ORDER_SIZE: + if abs(down) > MIN_ORDER_SIZE: logging.info(f'Flattening Down Position: {down}') await post_order( CLIENT = CLIENT, @@ -304,7 +323,7 @@ async def flatten_open_positions(CLIENT, token_id_up, token_id_down): neg_risk = POLY_CLOB['neg_risk'], OrderArgs_list = [Custom_OrderArgs( token_id=token_id_down, - price=float(POLY_CLOB_DOWN['price'])-0.01, + price=float(POLY_CLOB_DOWN['price'])-0.05, size=down, side=SELL, @@ -354,7 +373,7 @@ async def check_for_open_positions(CLIENT, token_id_up, token_id_down): return False @async_timeit -async def post_order(CLIENT, OrderArgs_list: list[Custom_OrderArgs], tick_size: float | str, neg_risk: bool): +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 @@ -370,7 +389,7 @@ async def post_order(CLIENT, OrderArgs_list: list[Custom_OrderArgs], tick_size: ), ), orderType=OrderType.GTC, - postOnly=False, + postOnly=oa.post_only, ), ) @@ -379,7 +398,15 @@ async def post_order(CLIENT, OrderArgs_list: list[Custom_OrderArgs], tick_size: 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() @@ -387,8 +414,7 @@ async def post_order(CLIENT, OrderArgs_list: list[Custom_OrderArgs], tick_size: ### Order Immediately Matched, Can Put in Offsetting Order Depending on State ### print('******** ORDER APPEND TO LOCAL - MATCHED ********* ') LOCAL_ACTIVE_ORDERS.append(d) - - if d['status'].upper() == 'CONFIRMED': + 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']) @@ -400,15 +426,17 @@ async def post_order(CLIENT, OrderArgs_list: list[Custom_OrderArgs], tick_size: 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_no_positions_route(): +async def no_orders(entry_or_exit: str = 'ENTRY'): global ORDER_LOCK ### Check for Price Bands ### @@ -427,122 +455,217 @@ async def no_orders_no_positions_route(): 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 = POLY_CLOB.get('token_id_up', None) if slope_side=='UP' else POLY_CLOB.get('token_id_down', None) - + + token_id_up = POLY_CLOB.get('token_id_up', None) + token_id_down = POLY_CLOB.get('token_id_down', None) ### Order Entry ### - px = float(POLY_CLOB['price'])+0.01 - order = Custom_OrderArgs( - token_id=token_id, - price=px, - size=DEFAULT_ORDER_SIZE, - side=BUY, - max_price = px + CHASE_TO_BUY_CENTS + 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}') + logging.info(f'BUY ORDER BLOCKED BY LOCK: {order_list}') else: - logging.info(f'Attempting BUY Order {order}') + 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] + 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': - s_c = s_c + 1 - if (b_c > 1) or (s_c > 1): - logging.critical(f'More than one active buy or more than one active sell: b_c {b_c}; s_c{s_c}') - await kill_algo() + 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') - + # 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: - orig_px = float(o['price']) - orig_size = float(o['size']) - if o['side'] == 'BUY': - if POLY_CLOB['token_id_up'] == o['token_id']: - clob_px = float(POLY_CLOB['price']) - else: - clob_px = float(POLY_CLOB_DOWN['price']) + 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: + 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: + + 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' - else: - px = orig_px+0.01 - - 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=px, - size=orig_size, - side=BUY, - max_price=o['max_price'] - - )] - ) + 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: - await cancel_single_order_by_id(CLIENT=CLIENT, order_id=o['orderID']) - elif o['side'] == 'SELL': - if POLY_CLOB['token_id_up'] == o['token_id']: - clob_px = float(POLY_CLOB['price']) + 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: - clob_px = float(POLY_CLOB_DOWN['price']) + 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})") - 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 - )] - ) + 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(): @@ -587,7 +710,7 @@ async def no_orders_active_positions_route(): async def active_orders_active_positions_route(): pass -async def kill_algo(): +async def kill_algo(msg: str = 'No kill msg provided'): logging.info('Killing algo...') await cancel_all_orders(CLIENT=CLIENT) await flatten_open_positions( @@ -595,8 +718,8 @@ async def kill_algo(): token_id_up = POLY_CLOB.get('token_id_up', None), token_id_down = POLY_CLOB.get('token_id_down', None), ) - logging.info('...algo killed') - raise Exception('Algo Killed') + logging.info(f'...algo killed: {msg}') + raise Exception(f'Algo Killed: {msg}') async def run_algo(): global POLY_BINANCE @@ -617,8 +740,13 @@ async def run_algo(): 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), @@ -641,12 +769,13 @@ async def run_algo(): ### DO THIS TO AVOID DELAY WITH FILL CONFIRMS ### Manage Local vs User Stream Orders ### - print(f'LOCAL_ACTIVE_ORDERS: {LOCAL_ACTIVE_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}') + print(f'*****USER TRADE: {user_trade}') if user_trade is not None: trade_status = str(user_trade['status']).upper() @@ -676,43 +805,24 @@ async def run_algo(): 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']}') - # LOCAL_ACTIVE_ORDERS[idx]['status'] = order_status - # if order_status == 'MATCHED': - # LOCAL_ACTIVE_ORDERS.pop(idx) - - # token_id = user_order['asset_id'] - # current_balance = float(LOCAL_TOKEN_BALANCES.get(token_id, 0.00)) - - # if user_order['side'] == 'BUY': - # size = float(user_order['size_matched']) - # else: - # size = float(user_order['size_matched']) * -1 - # LOCAL_TOKEN_BALANCES[token_id] = current_balance + size + if order_status == 'MATCHED': + logging.info('Order MATCHED, awaiting confirm') - # # px = user_order['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!') - if order_status == 'CANCELED': + elif order_status == 'CANCELED': LOCAL_ACTIVE_ORDERS.pop(idx) logging.info('Order Canceled') else: - logging.info('Order Live or Trade Awaiting Confirm') - - ### UPDATES CAN COME THRU EITHER ORDER OR TRADE CHANNELS - NEED TO UPDATE TO HANDLE TRADE CHANNLE + logging.info('Order Live') token_id_up = POLY_CLOB.get('token_id_up', 0) token_id_down = POLY_CLOB.get('token_id_down', 0) @@ -749,7 +859,7 @@ async def run_algo(): ### 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_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 @@ -758,13 +868,13 @@ async def run_algo(): ### Open Positions Route ### elif not(LOCAL_ACTIVE_ORDERS) and ACTIVE_BALANCES_EXIST: # No Orders, Positions - print('ROUTE: no_orders_no_positions_route') - await no_orders_active_positions_route() + 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') - await active_orders_no_positions_route() # Orders and Positions + 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) diff --git a/ws_user.py b/ws_user.py index fbdda9e..0d68d20 100644 --- a/ws_user.py +++ b/ws_user.py @@ -299,7 +299,7 @@ async def polymarket_stream(): event_type = data.get('event_type', None) match event_type: case 'trade': - logging.info(f'TRADE: {data}') + # logging.info(f'TRADE: {data}') # trade_status = data.get('status') # match trade_status: # Raise TELEGRAM ALERT ??? # case 'MATCHED': @@ -326,9 +326,9 @@ async def polymarket_stream(): LOOKBACK_MIN_TS_MS = ts_arrival-LOCAL_RECENT_TRADES_LOOKBACK_SEC*1000 LOCAL_RECENT_TRADES = [t for t in LOCAL_RECENT_TRADES if t.get('timestamp_arrival', 0) >= LOOKBACK_MIN_TS_MS] - print("---------------------") - print(LOCAL_RECENT_TRADES) - print("---------------------") + # print("---------------------") + # print(LOCAL_RECENT_TRADES) + # print("---------------------") VAL_KEY_OBJ = json.dumps(LOCAL_RECENT_TRADES) # VAL_KEY.publish(VK_CHANNEL, VAL_KEY_OBJ) -- 2.49.1 From 27227709e673ecdaff822fcc4c2730c42cb0e028 Mon Sep 17 00:00:00 2001 From: stevekeyharvey Date: Sat, 25 Apr 2026 23:43:03 +0000 Subject: [PATCH 5/5] saving --- database.ipynb | 618 ++++++++++++++++++++++++++++-- main copy.py | 942 ++++++++++++++++++++++++++++++++++++++++++++++ main.py | 931 +++++++++++++++++++++++++-------------------- ng.py | 2 +- order_entry.ipynb | 226 ++++++++--- ws_binance.py | 5 +- ws_clob.py | 18 +- ws_user.py | 13 +- 8 files changed, 2257 insertions(+), 498 deletions(-) create mode 100644 main copy.py diff --git a/database.ipynb b/database.ipynb index 5c02c8e..9da64d1 100644 --- a/database.ipynb +++ b/database.ipynb @@ -2,7 +2,7 @@ "cells": [ { "cell_type": "code", - "execution_count": 2, + "execution_count": 1, "id": "4cae6bf1", "metadata": {}, "outputs": [], @@ -14,7 +14,7 @@ }, { "cell_type": "code", - "execution_count": 11, + "execution_count": 2, "id": "f5040527", "metadata": {}, "outputs": [ @@ -75,7 +75,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 3, "id": "5c23110d", "metadata": {}, "outputs": [], @@ -96,28 +96,28 @@ }, { "cell_type": "code", - "execution_count": 24, + "execution_count": 4, "id": "a866e9ca", "metadata": {}, "outputs": [], "source": [ "# df_binance = pd.read_sql(q_binance, con=engine)\n", - "df_coinbase = pd.read_sql(q_coinbase, con=engine)\n", - "df_rtds = pd.read_sql(q_rtds, con=engine)\n", + "# df_coinbase = pd.read_sql(q_coinbase, con=engine)\n", + "# df_rtds = pd.read_sql(q_rtds, con=engine)\n", "df_clob = pd.read_sql(q_clob, con=engine)" ] }, { "cell_type": "code", - "execution_count": null, + "execution_count": 5, "id": "954a3c3c", "metadata": {}, "outputs": [], "source": [ "# df_binance['timestamp_arrival'] = pd.to_datetime(df_binance['timestamp_arrival'], unit='ms')\n", - "df_coinbase['timestamp_arrival'] = pd.to_datetime(df_coinbase['timestamp_arrival'], unit='ms')\n", - "df_rtds['timestamp_arrival'] = pd.to_datetime(df_rtds['timestamp_arrival'], unit='ms')\n", - "df_clob['timestamp_arrival'] = pd.to_datetime(df_clob['timestamp_arrival'], unit='ms')" + "# df_coinbase['timestamp_arrival'] = pd.to_datetime(df_coinbase['timestamp_arrival'], unit='ms')\n", + "# df_rtds['timestamp_arrival'] = pd.to_datetime(df_rtds['timestamp_arrival'], unit='ms')\n", + "df_clob['timestamp_arrival_dt'] = pd.to_datetime(df_clob['timestamp_arrival'], unit='ms')" ] }, { @@ -225,19 +225,177 @@ }, { "cell_type": "code", - "execution_count": null, - "id": "cd0b40d2", + "execution_count": 75, + "id": "85555ab4", + "metadata": {}, + "outputs": [], + "source": [ + "sql = text('''\n", + "OPTIMIZE TABLE binance_btcusd_trades;\n", + "''')\n", + "sql = text('''\n", + "SELECT \n", + " table_name, \n", + " data_length, \n", + " index_length, \n", + " data_free \n", + "FROM information_schema.tables;\n", + "''')" + ] + }, + { + "cell_type": "code", + "execution_count": 70, + "id": "a665c36f", + "metadata": {}, + "outputs": [], + "source": [ + "with engine.connect() as conn:\n", + " conn.execute(sql)\n", + " conn.commit()" + ] + }, + { + "cell_type": "code", + "execution_count": 76, + "id": "db71f3b0", "metadata": {}, "outputs": [ { - "name": "stdout", - "output_type": "stream", - "text": [ - "SUCCESS COPIED 326007 to binance_btcusd_trades to INTERSERVER_STORAGE\n" - ] + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
TABLE_NAMEDATA_LENGTHINDEX_LENGTHDATA_FREE
0innodb_table_stats16384.00.04194304.0
1innodb_index_stats16384.00.04194304.0
2CHARACTER_SETS0.00.00.0
3CHECK_CONSTRAINTS0.00.00.0
4COLLATIONS0.00.00.0
...............
342user_stream_trades81920.00.00.0
343user_stream_orders16384.00.00.0
344executions_orders16384.00.00.0
345poly_btcusd_trades37289984.00.04194304.0
346binance_btcusd_trades58294272.00.04194304.0
\n", + "

347 rows × 4 columns

\n", + "
" + ], + "text/plain": [ + " TABLE_NAME DATA_LENGTH INDEX_LENGTH DATA_FREE\n", + "0 innodb_table_stats 16384.0 0.0 4194304.0\n", + "1 innodb_index_stats 16384.0 0.0 4194304.0\n", + "2 CHARACTER_SETS 0.0 0.0 0.0\n", + "3 CHECK_CONSTRAINTS 0.0 0.0 0.0\n", + "4 COLLATIONS 0.0 0.0 0.0\n", + ".. ... ... ... ...\n", + "342 user_stream_trades 81920.0 0.0 0.0\n", + "343 user_stream_orders 16384.0 0.0 0.0\n", + "344 executions_orders 16384.0 0.0 0.0\n", + "345 poly_btcusd_trades 37289984.0 0.0 4194304.0\n", + "346 binance_btcusd_trades 58294272.0 0.0 4194304.0\n", + "\n", + "[347 rows x 4 columns]" + ] + }, + "execution_count": 76, + "metadata": {}, + "output_type": "execute_result" } ], - "source": [] + "source": [ + "pd.read_sql(sql, con=engine)" + ] }, { "cell_type": "code", @@ -249,15 +407,420 @@ { "cell_type": "code", "execution_count": null, + "id": "b06c6a3e", + "metadata": {}, + "outputs": [], + "source": [] + }, + { + "cell_type": "code", + "execution_count": 6, + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
timestamp_arrivaltimestamp_msgtimestamp_valuepriceqtyside_takerup_or_downtimestamp_arrival_dt
01775064793645177506479363017750647936300.59477.003500BUYUP2026-04-01 17:33:13.645
11775064793763177506479375317750647937530.4323.255812BUYDOWN2026-04-01 17:33:13.763
21775064793843177506479383017750647938300.432.325580BUYDOWN2026-04-01 17:33:13.843
31775064793915177506479390517750647939050.5810.020000BUYUP2026-04-01 17:33:13.915
41775064794077177506479406417750647940640.435.000000BUYDOWN2026-04-01 17:33:14.077
...........................
6726291775158573032177515857302217751585730220.373.243242BUYDOWN2026-04-02 19:36:13.032
6726301775158573316177515857330417751585733040.6415.625000BUYUP2026-04-02 19:36:13.316
6726311775158573365177515857335217751585733520.648.200000BUYUP2026-04-02 19:36:13.365
6726321775158573672177515857366117751585736610.37200.000000BUYDOWN2026-04-02 19:36:13.672
6726331775158573933177515857392117751585739210.647.812500BUYUP2026-04-02 19:36:13.933
\n", + "

672634 rows × 8 columns

\n", + "
" + ], + "text/plain": [ + " timestamp_arrival timestamp_msg timestamp_value price qty \\\n", + "0 1775064793645 1775064793630 1775064793630 0.59 477.003500 \n", + "1 1775064793763 1775064793753 1775064793753 0.43 23.255812 \n", + "2 1775064793843 1775064793830 1775064793830 0.43 2.325580 \n", + "3 1775064793915 1775064793905 1775064793905 0.58 10.020000 \n", + "4 1775064794077 1775064794064 1775064794064 0.43 5.000000 \n", + "... ... ... ... ... ... \n", + "672629 1775158573032 1775158573022 1775158573022 0.37 3.243242 \n", + "672630 1775158573316 1775158573304 1775158573304 0.64 15.625000 \n", + "672631 1775158573365 1775158573352 1775158573352 0.64 8.200000 \n", + "672632 1775158573672 1775158573661 1775158573661 0.37 200.000000 \n", + "672633 1775158573933 1775158573921 1775158573921 0.64 7.812500 \n", + "\n", + " side_taker up_or_down timestamp_arrival_dt \n", + "0 BUY UP 2026-04-01 17:33:13.645 \n", + "1 BUY DOWN 2026-04-01 17:33:13.763 \n", + "2 BUY DOWN 2026-04-01 17:33:13.843 \n", + "3 BUY UP 2026-04-01 17:33:13.915 \n", + "4 BUY DOWN 2026-04-01 17:33:14.077 \n", + "... ... ... ... \n", + "672629 BUY DOWN 2026-04-02 19:36:13.032 \n", + "672630 BUY UP 2026-04-02 19:36:13.316 \n", + "672631 BUY UP 2026-04-02 19:36:13.365 \n", + "672632 BUY DOWN 2026-04-02 19:36:13.672 \n", + "672633 BUY UP 2026-04-02 19:36:13.933 \n", + "\n", + "[672634 rows x 8 columns]" + ] + }, + "execution_count": 6, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "df_clob" + ] + }, + { + "cell_type": "code", + "execution_count": 7, "id": "48b47799", "metadata": {}, - "outputs": [], - "source": [] + "outputs": [ + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
timestamp_arrivaltimestamp_msgtimestamp_valuepriceqtyside_takerup_or_downtimestamp_arrival_dt
6571181775157300177177515730016617751573001660.482.083332BUYUP2026-04-02 19:15:00.177
6571191775157300554177515730054017751573005400.476.000000SELLUP2026-04-02 19:15:00.554
6571201775157300575177515730056117751573005610.533.000000BUYDOWN2026-04-02 19:15:00.575
6571211775157300645177515730063417751573006340.4829.570000BUYUP2026-04-02 19:15:00.645
6571221775157300689177515730067717751573006770.5020.000000BUYUP2026-04-02 19:15:00.689
...........................
6571931775157304972177515730494017751573049400.5040.000000BUYDOWN2026-04-02 19:15:04.972
6571941775157304979177515730495517751573049550.5040.000000BUYDOWN2026-04-02 19:15:04.979
6571951775157304986177515730496517751573049650.5010.200000BUYDOWN2026-04-02 19:15:04.986
6571961775157304991177515730497317751573049730.506.000000BUYDOWN2026-04-02 19:15:04.991
6571971775157304999177515730498817751573049880.5040.000000BUYDOWN2026-04-02 19:15:04.999
\n", + "

80 rows × 8 columns

\n", + "
" + ], + "text/plain": [ + " timestamp_arrival timestamp_msg timestamp_value price qty \\\n", + "657118 1775157300177 1775157300166 1775157300166 0.48 2.083332 \n", + "657119 1775157300554 1775157300540 1775157300540 0.47 6.000000 \n", + "657120 1775157300575 1775157300561 1775157300561 0.53 3.000000 \n", + "657121 1775157300645 1775157300634 1775157300634 0.48 29.570000 \n", + "657122 1775157300689 1775157300677 1775157300677 0.50 20.000000 \n", + "... ... ... ... ... ... \n", + "657193 1775157304972 1775157304940 1775157304940 0.50 40.000000 \n", + "657194 1775157304979 1775157304955 1775157304955 0.50 40.000000 \n", + "657195 1775157304986 1775157304965 1775157304965 0.50 10.200000 \n", + "657196 1775157304991 1775157304973 1775157304973 0.50 6.000000 \n", + "657197 1775157304999 1775157304988 1775157304988 0.50 40.000000 \n", + "\n", + " side_taker up_or_down timestamp_arrival_dt \n", + "657118 BUY UP 2026-04-02 19:15:00.177 \n", + "657119 SELL UP 2026-04-02 19:15:00.554 \n", + "657120 BUY DOWN 2026-04-02 19:15:00.575 \n", + "657121 BUY UP 2026-04-02 19:15:00.645 \n", + "657122 BUY UP 2026-04-02 19:15:00.689 \n", + "... ... ... ... \n", + "657193 BUY DOWN 2026-04-02 19:15:04.972 \n", + "657194 BUY DOWN 2026-04-02 19:15:04.979 \n", + "657195 BUY DOWN 2026-04-02 19:15:04.986 \n", + "657196 BUY DOWN 2026-04-02 19:15:04.991 \n", + "657197 BUY DOWN 2026-04-02 19:15:04.999 \n", + "\n", + "[80 rows x 8 columns]" + ] + }, + "execution_count": 7, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "df_clob.loc[(df_clob['timestamp_arrival']>1775157300*1000)&(df_clob['timestamp_arrival']<1775157305*1000)]" + ] }, { "cell_type": "code", "execution_count": null, - "id": "ad030f88", + "id": "e7aa7cfd", "metadata": {}, "outputs": [], "source": [] @@ -265,7 +828,7 @@ { "cell_type": "code", "execution_count": null, - "id": "cafc5060", + "id": "9bc2cecb", "metadata": {}, "outputs": [], "source": [] @@ -273,10 +836,21 @@ { "cell_type": "code", "execution_count": null, + "id": "734c2302", "metadata": {}, "outputs": [], "source": [] }, + { + "cell_type": "code", + "execution_count": null, + "id": "8a293522", + "metadata": {}, + "outputs": [], + "source": [ + "a" + ] + }, { "cell_type": "code", "execution_count": null, diff --git a/main copy.py b/main copy.py new file mode 100644 index 0000000..62eeb0e --- /dev/null +++ b/main copy.py @@ -0,0 +1,942 @@ +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 = 5 # In USDe +MIN_ORDER_SIZE = 5 # In USDe +TGT_PROFIT_CENTS = 0.03 +# 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 +def upsert_list_of_dicts_by_id(list_of_dicts, new_dict, id='id'): + for index, item in enumerate(list_of_dicts): + if item.get(id) == new_dict.get(id): + list_of_dicts[index] = new_dict + return list_of_dicts + + list_of_dicts.append(new_dict) + return list_of_dicts + +# @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' + local_local = LOCAL_ACTIVE_ORDERS.copy() + local_local = local_local[idx] + local_local['status'] = 'MATCHED' + LOCAL_ACTIVE_ORDERS = upsert_list_of_dicts_by_id(LOCAL_ACTIVE_ORDERS, local_local) + 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, + + )] + ) + logging.info('**************************') + +# @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, + ) + ) + balance = float(collateral['balance']) / 1_000_000 + balance = balance if balance > 4.99 else 0.00 + return balance + +# @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" + raise ValueError(f'UNKNOWN outcome for order: {d}') + + 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": + await cancel_all_orders(CLIENT=CLIENT) + logging.info(f'invalid post-only order: order crosses book. posted: {OrderArgs_list[idx].price}') + else: + await cancel_all_orders(CLIENT=CLIENT) + 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 entry_or_exit == 'ENTRY': + 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 + + if entry_or_exit == 'ENTRY': + ### 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: # o['token_id']==POLY_CLOB['token_id_down'] + 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: # o['token_id']==POLY_CLOB['token_id_down'] + 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']) + else: + clob_px = float(POLY_CLOB_DOWN['price']) + + 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 ( 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) or order_matched: + if (clob_px <= orig_px): + logging.info(f"Market px: ({clob_px} is below sell order px: {orig_px:.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 ( active_buy_up and active_buy_down ): + logging.info('SELL UP AND SELL DOWN MATCHED - WAITING FOR CONFIRMS (IN LOOP)') + continue + + if not order_matched: + 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 + )] + ) + else: + await cancel_single_order_by_id(CLIENT=CLIENT, order_id=o['orderID']) + + +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 = VAL_KEY.get('poly_user_trades') + USER_TRADES = json.loads(USER_TRADES) if USER_TRADES is not None else [] + USER_ORDERS = VAL_KEY.get('poly_user_orders') + USER_ORDERS = json.loads(USER_ORDERS) if USER_ORDERS is not None else [] + + ### Manage Local vs User Stream Orders ### + # print(f'LOCAL_ACTIVE_ORDERS: {LOCAL_ACTIVE_ORDERS}') + # print(f'USER_TRADES: {USER_TRADES}') + print(f'Len of Active Orders/Matched: {len(LOCAL_ACTIVE_ORDERS)}; User Trades: {len(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 ) + ### ^ ASSUMPTION BROKEN - MANY IN THIS LIST SO CANT ASSUME FIRST MAKER + + 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_active_positions_route') + # await no_orders(entry_or_exit='EXIT') + + ### Open Orders and Open Positions Route ### + else: + print('ROUTE: active_orders_active_positions_route - BETA') + # 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()) + \ No newline at end of file diff --git a/main.py b/main.py index ac6e97f..5c4cf55 100644 --- a/main.py +++ b/main.py @@ -1,6 +1,6 @@ import asyncio import json -from dataclasses import dataclass +from dataclasses import dataclass, asdict import logging import math import os @@ -20,7 +20,8 @@ from py_clob_client.clob_types import ( PartialCreateOrderOptions, PostOrdersArgs, BalanceAllowanceParams, - OpenOrderParams + OpenOrderParams, + MarketOrderArgs ) from py_clob_client.order_builder.constants import BUY, SELL from sqlalchemy import text @@ -29,12 +30,20 @@ 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 + @dataclass -class Custom_OrderArgs(OrderArgs): - max_price: float = 0.00 - post_only: bool = False - +class Custom_PostOrdersArgs(PostOrdersArgs): + token_id: str = '' + price: float = 0.00 + # max_price: float = 0.00 + size: float = 0.00 + side: str = '' + ### Database ### CLIENT = None CON: AsyncContextManager | None = None @@ -42,20 +51,25 @@ VAL_KEY = None ### Logging ### load_dotenv() -LOG_FILEPATH: str = os.getenv("LOGS_PATH") + '/Polymarket_5min_Algo.log' +LOG_FILEPATH: str = os.getenv("LOGS_PATH") + '/Polymarket_Algo.log' ### ALGO CONFIG / CONSTANTS ### -SLOPE_YES_THRESH = 0.01 # In Percent % Chg (e.g. 0.02 == 0.02%) +SLOPE_YES_THRESH = 0.00750 # In Percent % Chg (e.g. 0.02 == 0.02%) +SLOPE_YES_THRESH_1 = 0.00750 # 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 +DEFAULT_ORDER_SIZE = 6 # In USDe MIN_ORDER_SIZE = 5 # In USDe -TGT_PROFIT_CENTS = 0.04 -CHASE_TO_BUY_CENTS = 0.05 +TGT_PROFIT_CENTS = 0.08 +# CHASE_TO_BUY_CENTS = 0.05 MAX_ALLOWED_POLY_PX = 0.90 +MAX_LEG_LIVE_SEC = 3.25 + ### GLOBALS ### +LOOP_LAST_ROUTE: str = '' ORDER_LOCK = 0 +FIRST_LOOP_NEW_MKT = False SLUG_END_TIME = 0 @@ -70,13 +84,13 @@ USER_ORDERS = {} SLOPE_HIST = [] LOCAL_ACTIVE_ORDERS = [] +LOCAL_MATCHED_ORDERS = [] LOCAL_TOKEN_BALANCES = {} # LOCAL_ACTIVE_POSITIONS = [] ACTIVE_BALANCES_EXIST = False ### REMOVE - ### Decorators ### def async_timeit(func): @wraps(func) @@ -87,7 +101,7 @@ def async_timeit(func): finally: end_time = time.perf_counter() total_time = (end_time - start_time)*1000 - print(f"Function '{func.__name__}' executed in {total_time:.4f} ms") + logging.info(f"Function '{func.__name__}' executed in {total_time:.4f} ms") return wrapper @@ -202,11 +216,23 @@ async def insert_executions_orders_table( ### Functions ### # @async_timeit -async def slope_decision() -> list[bool, str]: +def upsert_list_of_dicts_by_id(list_of_dicts, new_dict, id='id'): + for index, item in enumerate(list_of_dicts): + if item.get(id) == new_dict.get(id): + list_of_dicts[index] = new_dict + return list_of_dicts + + list_of_dicts.append(new_dict) + return list_of_dicts + +# @async_timeit +async def slope_decision(slope_yes_thresh) -> 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') + min_trade_hist_ts = np.min(hist_trades[:, 0]) + max_trade_hist_ts = np.max(hist_trades[:, 0] ) + if ( max_trade_hist_ts ) - ( min_trade_hist_ts ) < 5: + logging.info(f'Max - Min Trade In History is < 5 Seconds Apart: {max_trade_hist_ts} - {min_trade_hist_ts}') return False, '' last_px = POLY_BINANCE['value'] @@ -231,16 +257,18 @@ async def slope_decision() -> list[bool, str]: # 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};') + slope_1_buy = abs(slope) >= ( slope_yes_thresh / 100) + slope_5_buy = abs(slope_5) >= ( slope_yes_thresh / 100) ### DECISION ### if slope_1_buy and slope_5_buy: - print(f'🤑🤑🤑🤑🤑🤑🤑🤑🤑🤑 Slope: {slope_5:.4%};') side = 'UP' if slope > 0.00 else 'DOWN' + print(f'🤑 {round(datetime.now().timestamp()*1000)}: Slope_1: {slope_5:.4%}; Slope_5: {slope_5:.4%}; SIDE: {side}') + logging.info(f'🤑 {round(datetime.now().timestamp()*1000)}: Slope_1: {slope_5:.4%}; Slope_5: {slope_5:.4%}; SIDE: {side}') return True, side + elif abs(slope) >= ( 0.001 / 100): + print(f'{round(datetime.now().timestamp()*1000)}: SLOPE_1: {slope:.4%}; SLOPE_5: {slope_5:.4%};') + return False, '' else: return False, '' @@ -265,7 +293,11 @@ async def cancel_single_order_by_id(CLIENT, order_id): 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' + # LOCAL_ACTIVE_ORDERS[idx]['status'] = 'MATCHED' + local_local = LOCAL_ACTIVE_ORDERS.copy() + local_local = local_local[idx] + local_local['status'] = 'MATCHED' + LOCAL_ACTIVE_ORDERS = upsert_list_of_dicts_by_id(LOCAL_ACTIVE_ORDERS, local_local) 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": @@ -294,40 +326,47 @@ async def cancel_single_order_by_id(CLIENT, order_id): # @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) + up_size = await get_balance_by_token_id(CLIENT=CLIENT, token_id=token_id_up) + down_size = await get_balance_by_token_id(CLIENT=CLIENT, token_id=token_id_down) + + up_size = round(up_size, 2) + down_size = round(up_size, 2) logging.info('*********FLATTENING*********') - logging.info(f'UP BALANCE = {up}') - logging.info(f'DOWN BALANCE = {down}') + logging.info(f'UP BALANCE = {up_size}') + logging.info(f'DOWN BALANCE = {down_size}') ### Submit orders to flatten outstanding balances ### - if abs(up) > MIN_ORDER_SIZE: - logging.info(f'Flattening Up Position: {up}') + order_list = [] + + if up_size: + logging.info(f'Flattening Up Position: {up_size}') + # up_px_worst = round(float(POLY_CLOB['price'])-0.05, 2) + order_list.append(MarketOrderArgs( + token_id = token_id_up, + amount = up_size, + # size = up_size, + price = 0.01, + # max_price = 0.99, + side = SELL, + order_type = OrderType.FAK + )) + if down_size: + order_list.append(MarketOrderArgs( + token_id = token_id_down, + amount = down_size, + # size = down_size, + price = 0.01, + # max_price = 0.99, + side = SELL, + order_type = OrderType.FAK + )) + logging.info(f'Flattening Down Position: {down_size}') + if order_list: 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, - - )] + PostOrdersArgs_list = order_list, + is_mkt_order_list = True ) # @async_timeit @@ -338,7 +377,10 @@ async def get_balance_by_token_id(CLIENT, token_id): token_id=token_id, ) ) - return int(collateral['balance']) / 1_000_000 + balance = float(collateral['balance']) / 1_000_000 + logging.info(f'Balance: {balance}; Collateral: {collateral}') + balance = balance if balance >= 0.01 else 0.00 + return balance # @async_timeit async def get_usde_balance(CLIENT): @@ -373,47 +415,47 @@ async def check_for_open_positions(CLIENT, token_id_up, token_id_down): 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 +async def post_order(CLIENT, PostOrdersArgs_list: list[Custom_PostOrdersArgs], is_mkt_order_list: bool = False) -> list[dict]: # Returns order response dict global LOCAL_ACTIVE_ORDERS + global LOCAL_MATCHED_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) + ### POST + if is_mkt_order_list: + timestamp_post_sent = round(datetime.now().timestamp()*1000) + response = [] + for o in PostOrdersArgs_list: + response.append(CLIENT.post_order(o)) + else: + timestamp_post_sent = round(datetime.now().timestamp()*1000) + response = CLIENT.post_orders(PostOrdersArgs_list) for idx, d in enumerate(response): if d['errorMsg'] == '': - d['token_id'] = OrderArgs_list[idx].token_id + d['timestamp_post_sent'] = timestamp_post_sent + d['timestamp_post_resp_rec'] = round(datetime.now().timestamp()*1000) + + d['token_id'] = PostOrdersArgs_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" + raise ValueError(f'UNKNOWN outcome for order: {d}') - 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() - + d['price'] = PostOrdersArgs_list[idx].price + + # d['max_price'] = PostOrdersArgs_list[idx].max_price + if is_mkt_order_list: + d['size'] = PostOrdersArgs_list[idx].amount + else: + d['size'] = PostOrdersArgs_list[idx].size + d['side'] = str(PostOrdersArgs_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) + logging.info('******** ORDER APPEND TO LOCAL - MATCHED ********* ') + LOCAL_MATCHED_ORDERS.append(d) elif d['status'].upper() == 'CONFIRMED': current_balance = float(LOCAL_TOKEN_BALANCES.get(d['token_id'], 0.00)) if d['side'] == 'BUY': @@ -422,294 +464,303 @@ async def post_order(CLIENT, OrderArgs_list: list[Custom_OrderArgs], tick_size: size = float(d['size']) * -1 LOCAL_TOKEN_BALANCES[d['token_id']] = current_balance + size - print('******** TRADE FILLED, BAL UPDATED ********* ') + logging.info('******** TRADE FILLED, BAL UPDATED ********* ') else: - print('******** ORDER APPEND TO LOCAL - LIVE ********* ') + logging.info('******** 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}') + await cancel_all_orders(CLIENT=CLIENT) + logging.info(f'invalid post-only order: order crosses book. posted: {PostOrdersArgs_list[idx].price}') else: + await cancel_all_orders(CLIENT=CLIENT) raise ValueError(f'Order entry failed: {d}') - - logging.info(f'Order Posted Resp: {response}') - print(f'Order Posted Resp: {response}') + + logging.info(f'🚨 Order Posted Resp: {response}') return response + ### Routes ### -async def no_orders(entry_or_exit: str = 'ENTRY'): +# @async_timeit +async def no_orders_route(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)) + up_last_px = float(POLY_CLOB.get('price', 0)) + down_last_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 + if entry_or_exit == 'ENTRY': + if (up_last_px > MAX_ALLOWED_POLY_PX) or (down_last_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 + if entry_or_exit == 'ENTRY': + ### 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) + if entry_or_exit == 'EXIT': + # size_up = await get_balance_by_token_id(CLIENT=CLIENT, token_id=token_id_up) + size_up = LOCAL_TOKEN_BALANCES[token_id_up] + # size_down = await get_balance_by_token_id(CLIENT=CLIENT, token_id=token_id_down) + size_down = LOCAL_TOKEN_BALANCES[token_id_down] + size_up = round(size_up, 6) + size_down = round(size_down, 6) + size_less_than_min = (size_up < MIN_ORDER_SIZE) or (size_down < MIN_ORDER_SIZE) + # logging.info(f"EXITING: size_less_than_min: {size_less_than_min}; up: {size_up}; down: {size_down};") + else: + size_less_than_min = False + + if not size_less_than_min: + ### Check Slope ### + slope_bool, slope_side = await slope_decision(slope_yes_thresh=SLOPE_YES_THRESH) + if not slope_bool: + # logging.info('Failed Slope Check') + return False + else: + slope_bool, slope_side = False, 'MKT' ### Order Entry ### - if slope_side == 'UP': + if slope_side == 'MKT': + side = SELL + size = min([size_up, size_down]) + up_px = 0.01 + down_px = 0.01 + up_post_only = False + down_post_only = False # T + order_type = OrderType.FAK + logging.info(f'Flattening Residuals - Mkt Order: ({size} ({side}) ({slope_side}))') + elif 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_px = round(up_last_px + 0.01, 2) + down_px = round(down_last_px - TGT_PROFIT_CENTS + 0.01, 2) up_post_only = False down_post_only = False # T + order_type = OrderType.GTC 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 + size = size_up + logging.info(f'Flattening Residuals - Limit Order: ({size} ({side}) ({slope_side}))') + up_px = round(up_last_px + TGT_PROFIT_CENTS + 0.01, 2) + down_px = round(down_last_px - 0.01, 2) + order_type = OrderType.GTC 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_px = round(up_last_px - TGT_PROFIT_CENTS + 0.01, 2) + down_px = round(down_last_px + 0.01, 2) up_post_only = False # T down_post_only = False - else: + order_type = OrderType.GTC + 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 - 0.01 - down_px = down_px + TGT_PROFIT_CENTS + size = size_down + logging.info(f'Flattening Residuals - Limit Order: ({size} ({side}) ({slope_side}))') + up_px = round(up_last_px - 0.01, 2) + down_px = round(down_last_px + TGT_PROFIT_CENTS + 0.01, 2) + order_type = OrderType.GTC 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 + up_leg = Custom_PostOrdersArgs( + order=CLIENT.create_order( + order_args=OrderArgs( + token_id=token_id_up, + price=up_px, + size=size, + side=side, + ), + options=PartialCreateOrderOptions( + tick_size=str(POLY_CLOB['tick_size']), + neg_risk=POLY_CLOB['neg_risk'] + ), + ), + orderType = order_type, + postOnly = up_post_only, + token_id = token_id_up, + price = up_px, + # max_price = 0.99, + size = size, + side = side ) - 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 + down_leg = Custom_PostOrdersArgs( + order=CLIENT.create_order( + order_args=OrderArgs( + token_id=token_id_down, + price=down_px, + size=size, + side=side, + ), + options=PartialCreateOrderOptions( + tick_size=str(POLY_CLOB['tick_size']), + neg_risk=POLY_CLOB['neg_risk'] + ), + ), + orderType = order_type, + postOnly = down_post_only, + token_id = token_id_down, + price = down_px, + # max_price = 0.99, + size = size, + side = side ) - order_list = [buy_up_leg, buy_down_leg] - ### ADD CHECK FOR MKT MOVED AWAY FROM OPPORTNITY ### + ### ADD CHECK FOR MKT MOVED AWAY FROM OPPORTUNITY ### + if slope_side == 'MKT': + order_list = [up_leg, down_leg] + elif slope_side == 'UP': + order_list = [up_leg, down_leg] + vk_px = float( json.loads( VAL_KEY.get('poly_5min_btcusd') )['price'] ) + if up_px < vk_px: + logging.info(f'ABANDONED BUY ORDERS: UP px moved from {up_px} -> {vk_px}') + return False + else: + logging.info(f'NOT ABANDONED BUY ORDERS: UP px moved from {up_px} -> {vk_px}') + else: + order_list = [down_leg, up_leg] + vk_px = float( json.loads( VAL_KEY.get('poly_5min_btcusd_down') )['price'] ) + if down_px < vk_px: + logging.info(f'ABANDONED BUY ORDERS: DOWN px moved from {down_px} -> {vk_px}') + return False + else: + logging.info(f'NOT ABANDONED BUY ORDERS: UP px moved from {up_px} -> {vk_px}') + + logging.info('PRICES AT TIME OF ORDER:') + logging.info(f'Current TS: {round(datetime.now().timestamp()*1000)}') + logging.info(f'up_last_px: {up_last_px}; order up_px: {up_px}') + logging.info(f'down_last_px: {down_last_px}; order down_px: {down_px}') + logging.info(f'TGT_PROFIT_CENTS: {TGT_PROFIT_CENTS}') if ORDER_LOCK: logging.info(f'BUY ORDER BLOCKED BY LOCK: {order_list}') else: - logging.info(f'Attempting BUY Order {order_list}') + logging.info(f'Attempting {entry_or_exit} Orders {order_list}') await post_order( CLIENT = CLIENT, - tick_size = POLY_CLOB['tick_size'], - neg_risk = POLY_CLOB['neg_risk'], - OrderArgs_list = order_list + PostOrdersArgs_list = order_list ) # ORDER_LOCK = ORDER_LOCK + 1 -async def active_orders_no_positions_route(): +# @async_timeit +async def active_orders_route(): global LOCAL_ACTIVE_ORDERS + global LOCAL_MATCHED_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 + await kill_algo('More than two active orders, shutting down') - active_buy_up = False - active_buy_down = False + if len(LOCAL_MATCHED_ORDERS) > 2: + logging.critical('More than two matched orders, shutting down') + await kill_algo('More than two matched orders, shutting down') - 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']}') + for idx, o in enumerate(LOCAL_ACTIVE_ORDERS): + replace_w_order_at_mkt = False 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 + logging.info(f'Active Order MATCHED. Moving to LOCAL_MATCHED_ORDERS. Order Id: {o['orderID']}') + LOCAL_MATCHED_ORDERS.append(o) + LOCAL_ACTIVE_ORDERS.pop(idx) + continue + elif o.get('status').upper() == 'LIVE': + ts_now = round(datetime.now().timestamp()*1000) + ts_order_post = o['timestamp_post_sent'] + sec_order_live = (ts_now - ts_order_post) / 1000 + logging.info(f'Working on order ({o['side']}) ({o['outcome']}): {o['orderID']}; SEC ALIVE: {sec_order_live:.2f}') + + ### Check Conditions to Immediately Replace Order at Mkt (Abandon Target Px) ### + if (sec_order_live > MAX_LEG_LIVE_SEC): # Abandon if lived longer than x seconds + logging.info(f'Order live > max sec ({sec_order_live} > {MAX_LEG_LIVE_SEC}); {o['side']}) ({o['outcome']}): {o['orderID']}') + replace_w_order_at_mkt = True + + if not replace_w_order_at_mkt: + slope_bool, slope_side = await slope_decision(slope_yes_thresh=SLOPE_YES_THRESH / 2) # Abandon if slope has reversed + if slope_bool and (slope_side == o['outcome']): + logging.info(f'SLOPE MOVED AWAY FROM TGT ORDER, replacing at mkt; ({o['side']}) ({o['outcome']}): {o['orderID']}') + replace_w_order_at_mkt = True + + if replace_w_order_at_mkt: + order_matched = await cancel_single_order_by_id(CLIENT=CLIENT, order_id=o['orderID']) + if order_matched: + logging.info(f'Order is MATCHED after being worked: {o}') + LOCAL_MATCHED_ORDERS.append(o) + LOCAL_ACTIVE_ORDERS.pop(idx) + continue + else: + token_id = o['token_id'] + if POLY_CLOB['token_id_up'] == token_id: + clob_px = float(POLY_CLOB['price']) + else: + clob_px = float(POLY_CLOB_DOWN['price']) + + ### BUY + if o['side'] == 'BUY': + px = round(clob_px + 0.02, 2) + side = BUY + # max_price = o['max_price'] + size = float(o['size']) + post_only = False + + ### SELL + elif o['side'] == 'SELL': + px = round(clob_px - 0.02, 2) + side = SELL + # max_price = o['max_price'] + size = float(o['size']) + post_only = False + + if size < MIN_ORDER_SIZE: + order_type = OrderType.FAK + else: + order_type = OrderType.GTC + + logging.info(f'REPLACING ORDER. Orig Px {o['price']} -> Mkt Px: {clob_px}; {o['side']}) ({o['outcome']}): {o['orderID']}') + + order = Custom_PostOrdersArgs( + order=CLIENT.create_order( + order_args=OrderArgs( + token_id=token_id, + price=px, + size=size, + side=side, + ), + options=PartialCreateOrderOptions( + tick_size=str(POLY_CLOB['tick_size']), + neg_risk=POLY_CLOB['neg_risk'] + ), + ), + orderType = order_type, + postOnly = post_only, + token_id = token_id, + price = px, + # max_price = max_price, + size = size, + side = side + ) + + await post_order( + CLIENT = CLIENT, + PostOrdersArgs_list = [order] + ) 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 + raise ValueError(f'Unexpected Order Status: {o}') +@async_timeit async def kill_algo(msg: str = 'No kill msg provided'): logging.info('Killing algo...') await cancel_all_orders(CLIENT=CLIENT) @@ -721,6 +772,33 @@ async def kill_algo(msg: str = 'No kill msg provided'): logging.info(f'...algo killed: {msg}') raise Exception(f'Algo Killed: {msg}') +@async_timeit +async def clob_client_caching(token_id): + # logging.info('CLIENT CACHING') + tick_size = CLIENT.get_tick_size(token_id=token_id) + # logging.info(f'Tick Size: {tick_size}') + neg_risk = CLIENT.get_neg_risk(token_id=token_id) + # logging.info(f'Is Negative Risk: {neg_risk}') + fee_rate_bps = CLIENT.get_fee_rate_bps(token_id=token_id) + # logging.info(f'Fee Rate Bps: {fee_rate_bps}') + # logging.info('CLIENT CACHING COMPLETE') + +# @async_timeit +async def loop_check_route_switch(route_name): + global LOOP_LAST_ROUTE + + if LOOP_LAST_ROUTE != route_name: + print(f'SWITCHING ROUTES: {LOOP_LAST_ROUTE} -> {route_name}') + logging.info(f'SWITCHING ROUTES: {LOOP_LAST_ROUTE} -> {route_name}') + if route_name == 'no_orders_route_EXIT': + 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), + ) + LOOP_LAST_ROUTE = route_name + + async def run_algo(): global POLY_BINANCE global POLY_REF @@ -731,116 +809,167 @@ async def run_algo(): global SLOPE_HIST global ACTIVE_BALANCES_EXIST + global FIRST_LOOP_NEW_MKT global LOCAL_ACTIVE_ORDERS + global LOCAL_MATCHED_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('') + ### Get Token IDs ### + token_id_up = POLY_CLOB.get('token_id_up', None) + token_id_down = POLY_CLOB.get('token_id_down', None) + + if (token_id_up is None) or (token_id_down is None): + raise ValueError(f'Token ID is None: UP: {token_id_up}; DOWN: {token_id_down}') + logging.info(f'token_id_up: {POLY_CLOB.get('token_id_up', None)}') + logging.info(f'token_id_down: {POLY_CLOB.get('token_id_down', None)}') + + ### CLOB Client Caching ### + await clob_client_caching(token_id=token_id_up) + await clob_client_caching(token_id=token_id_down) + + ### Get Initial Balances ### 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), ) + ### Check for missing target px (Poly 5min Target BTC Px Target) ### + if POLY_CLOB.get('target_price', 0) <= 1.00: + kill_algo('Missing target_price, check CLOB feedhandler') + try: while True: - loop_start = time.time() - print('__________Start___________') + # 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_TRADES = VAL_KEY.get('poly_user_trades') + USER_TRADES = json.loads(USER_TRADES) if USER_TRADES is not None else [] 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) + ### Check for Token Id + token_id_up = POLY_CLOB.get('token_id_up', None) + token_id_down = POLY_CLOB.get('token_id_down', None) if (token_id_up is None) or (token_id_down is None): - print('Missing Token Ids for Market, sleeping 1 sec and retrying...') + logging.info(f'Missing Token Ids for Market (token_id_up: {token_id_up}; token_id_down: {token_id_down}), 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) + + if FIRST_LOOP_NEW_MKT: + ### CLOB Client Caching ### + await clob_client_caching(token_id=token_id_up) + await clob_client_caching(token_id=token_id_down) + FIRST_LOOP_NEW_MKT = False + + for idx, o in enumerate(LOCAL_ACTIVE_ORDERS): + if USER_TRADES: + for t in USER_TRADES: + if t['trader_side']=='MAKER': + user_trade = next( ( item for item in json.loads(t['maker_orders']) if ( o['orderID'] == item['order_id'] ) ), None ) + if user_trade: + user_trade['status'] = t['status'] + user_trade['size'] = float(user_trade['matched_amount']) + # logging.info(f'********** MAKER TRADE IN USER TRADES: {user_trade} *******') + elif t['taker_order_id'] == o["orderID"]: + user_trade = t + else: + user_trade = None + + if user_trade: + trade_status = str(user_trade['status']).upper() + if trade_status != o['status'].upper(): + logging.info(f'Updated Trade Status: {o['status'].upper()} --> {trade_status}; {o['orderID']}') + logging.info(f'Trade Details: {user_trade}') + o['status'] = trade_status + + 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 + + logging.info('Order FILLED! - IN LOCAL_ACTIVE_ORDERS') + elif trade_status == 'MATCHED': + logging.info(f'Order MATCHED. Moving to LOCAL_MATCHED_ORDERS. Trade Status: {trade_status}') + LOCAL_MATCHED_ORDERS.append(o) + LOCAL_ACTIVE_ORDERS.pop(idx) + 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}') + + for idx, o in enumerate(LOCAL_MATCHED_ORDERS): + if USER_TRADES: + for t in USER_TRADES: + if t['trader_side']=='MAKER': + user_trade = next( ( item for item in json.loads(t['maker_orders']) if ( o['orderID'] == item['order_id'] ) ), None ) + if user_trade: + user_trade['status'] = t['status'] + user_trade['size'] = float(user_trade['matched_amount']) + # logging.info(f'********** MAKER TRADE IN USER TRADES: {user_trade} *******') + elif t['taker_order_id'] == o["orderID"]: + user_trade = t + else: + user_trade = None + + if user_trade: + trade_status = str(user_trade['status']).upper() + if trade_status != o['status'].upper(): + logging.info(f'Updated Trade Status: {o['status']} --> {trade_status}; {o['orderID']}') + o['status'] = trade_status + + if trade_status == 'CONFIRMED': + LOCAL_MATCHED_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 + + logging.info('Matched order CONFIRMED! - IN LOCAL_MATCHED_ORDERS') + elif trade_status == 'MATCHED': + # logging.info(f'Order Matched...awaiting confirm: {trade_status}') + pass + elif trade_status == 'MINED': + # logging.info(f'Order Mined...awaiting confirm: {trade_status}') + pass + else: + logging.info(f'Trade status but not filled: trade= {user_trade}; order={o}') + + + ### CHECK BALANCES ### + 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 = (abs(LOCAL_TOKEN_BALANCES.get(token_id_up)) > 0) or abs((LOCAL_TOKEN_BALANCES.get(token_id_down)) > 0) ### Check for Endtime Buffer ### if ENDTIME_BUFFER_SEC > POLY_CLOB.get('sec_remaining', 0): + FIRST_LOOP_NEW_MKT = True if LOCAL_ACTIVE_ORDERS: print('buffer zone - orders cancel') await cancel_all_orders(CLIENT=CLIENT) @@ -851,33 +980,35 @@ async def run_algo(): 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() + ### ENTRY Route ### + if not(LOCAL_ACTIVE_ORDERS) and not(LOCAL_MATCHED_ORDERS) and not(ACTIVE_BALANCES_EXIST): # No Orders, No Matched, No Positions + await loop_check_route_switch('no_orders_route_ENTRY') + await no_orders_route(entry_or_exit='ENTRY') - ### 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 Route ### + elif LOCAL_ACTIVE_ORDERS: # Any Active Orders + await loop_check_route_switch('active_orders_route') + await active_orders_route() - ### Open Orders and Open Positions Route ### + ### Matched Orders Route - Waiting ### + elif LOCAL_MATCHED_ORDERS: # Any Active Orders + await loop_check_route_switch('matched_orders_awaiting_confirms') + # await active_orders_route() + + ### Open Positions Route - EXIT ### + elif not(LOCAL_ACTIVE_ORDERS) and not(LOCAL_MATCHED_ORDERS) and ACTIVE_BALANCES_EXIST: # No Orders, No Matches, Positions + await loop_check_route_switch('no_orders_route_EXIT') + await no_orders_route(entry_or_exit='EXIT') + # time.sleep(0.5) else: - print('ROUTE: active_orders_active_positions_route - OFF') - # await active_orders_no_positions_route() # Orders and Positions + print('ROUTE: NOT IMPLEMENTED') - print(f'__________________________ (Algo Engine ms: {(time.time() - loop_start)*1000})') - time.sleep(1) + # print(f'__________________________ (Algo Engine ms: {(time.time() - loop_start)*1000})') + # time.sleep(3) except KeyboardInterrupt: print('...algo stopped') await cancel_all_orders(CLIENT=CLIENT) diff --git a/ng.py b/ng.py index 7cd0a31..2d44cd8 100644 --- a/ng.py +++ b/ng.py @@ -21,7 +21,7 @@ RH_PAIR = 'USD' DEFAULT_TO_DARKMODE: bool = True ALLOW_BODY_SCROLL: bool = True LOOKBACK: int = 60 -LOOKBACK_RT_TV_MAX_POINTS: int = 300 +LOOKBACK_RT_TV_MAX_POINTS: int = 3000 REFRESH_INTERVAL_SEC: int = 10 REFRESH_INTERVAL_RT_SEC: int = 1/30 diff --git a/order_entry.ipynb b/order_entry.ipynb index 7028783..e0628c4 100644 --- a/order_entry.ipynb +++ b/order_entry.ipynb @@ -2,7 +2,7 @@ "cells": [ { "cell_type": "code", - "execution_count": 134, + "execution_count": 1, "id": "c0bfb3b5", "metadata": {}, "outputs": [], @@ -15,13 +15,13 @@ "import json\n", "from dataclasses import dataclass\n", "\n", - "from py_clob_client.clob_types import OrderArgs, OrderType, PostOrdersArgs, PartialCreateOrderOptions, BalanceAllowanceParams\n", + "from py_clob_client.clob_types import OrderArgs, OrderType, PostOrdersArgs, PartialCreateOrderOptions, BalanceAllowanceParams, OpenOrderParams\n", "from py_clob_client.order_builder.constants import BUY, SELL\n" ] }, { "cell_type": "code", - "execution_count": 135, + "execution_count": 2, "id": "7d7dc787", "metadata": {}, "outputs": [], @@ -49,17 +49,17 @@ }, { "cell_type": "code", - "execution_count": 136, + "execution_count": 3, "id": "c3e07e21", "metadata": {}, "outputs": [ { "data": { "text/plain": [ - "Timestamp('2026-03-31 05:45:00')" + "Timestamp('2026-04-04 05:15:00')" ] }, - "execution_count": 136, + "execution_count": 3, "metadata": {}, "output_type": "execute_result" } @@ -74,74 +74,74 @@ }, { "cell_type": "code", - "execution_count": 137, + "execution_count": 4, "id": "10671da4", "metadata": {}, "outputs": [ { "data": { "text/plain": [ - "{'id': '1787714',\n", - " 'question': 'Bitcoin Up or Down - March 31, 1:45AM-1:50AM ET',\n", - " 'conditionId': '0xd1773b412dacad884c202a7b14f0197918b1e22028ce2b5737fbd659bbe150f0',\n", - " 'slug': 'btc-updown-5m-1774935900',\n", + "{'id': '1840714',\n", + " 'question': 'Bitcoin Up or Down - April 4, 1:15AM-1:20AM ET',\n", + " 'conditionId': '0xa71ed436d160cc45bf182b9004e0c10b16322ea1b41375182b747c2629223ecc',\n", + " 'slug': 'btc-updown-5m-1775279700',\n", " 'resolutionSource': 'https://data.chain.link/streams/btc-usd',\n", - " 'endDate': '2026-03-31T05:50:00Z',\n", - " 'liquidity': '12631.9812',\n", - " 'startDate': '2026-03-30T05:53:51.811773Z',\n", + " 'endDate': '2026-04-04T05:20:00Z',\n", + " 'liquidity': '19009.3694',\n", + " 'startDate': '2026-04-03T05:23:41.149918Z',\n", " 'image': 'https://polymarket-upload.s3.us-east-2.amazonaws.com/BTC+fullsize.png',\n", " 'icon': 'https://polymarket-upload.s3.us-east-2.amazonaws.com/BTC+fullsize.png',\n", " 'description': 'This market will resolve to \"Up\" if the Bitcoin price at the end of the time range specified in the title is greater than or equal to the price at the beginning of that range. Otherwise, it will resolve to \"Down\".\\nThe resolution source for this market is information from Chainlink, specifically the BTC/USD data stream available at https://data.chain.link/streams/btc-usd.\\nPlease note that this market is about the price according to Chainlink data stream BTC/USD, not according to other sources or spot markets.',\n", " 'outcomes': '[\"Up\", \"Down\"]',\n", - " 'outcomePrices': '[\"0.875\", \"0.125\"]',\n", - " 'volume': '94249.88157999996',\n", + " 'outcomePrices': '[\"0.505\", \"0.495\"]',\n", + " 'volume': '1149.1770909999996',\n", " 'active': True,\n", " 'closed': False,\n", " 'marketMakerAddress': '',\n", - " 'createdAt': '2026-03-30T05:52:34.18484Z',\n", - " 'updatedAt': '2026-03-31T05:48:51.799961Z',\n", + " 'createdAt': '2026-04-03T05:22:26.236646Z',\n", + " 'updatedAt': '2026-04-04T05:14:58.886334Z',\n", " 'new': False,\n", " 'featured': False,\n", " 'archived': False,\n", " 'restricted': True,\n", " 'groupItemThreshold': '0',\n", - " 'questionID': '0x33d208062bed0d8fcf68d2f3899e528f103ad7e7763efc763f71c065ea6c842c',\n", + " 'questionID': '0x1f7b9fd2711422d90794895b99edcb93a81246afd56b08ee4736c9b57565f8f7',\n", " 'enableOrderBook': True,\n", " 'orderPriceMinTickSize': 0.01,\n", " 'orderMinSize': 5,\n", - " 'volumeNum': 94249.88157999996,\n", - " 'liquidityNum': 12631.9812,\n", - " 'endDateIso': '2026-03-31',\n", - " 'startDateIso': '2026-03-30',\n", + " 'volumeNum': 1149.1770909999996,\n", + " 'liquidityNum': 19009.3694,\n", + " 'endDateIso': '2026-04-04',\n", + " 'startDateIso': '2026-04-03',\n", " 'hasReviewedDates': True,\n", - " 'volume24hr': 93644.03624199987,\n", - " 'volume1wk': 93644.03624199987,\n", - " 'volume1mo': 93644.03624199987,\n", - " 'volume1yr': 93644.03624199987,\n", - " 'clobTokenIds': '[\"13157292356296687506747919717798752029699544499054519087985411865141996614822\", \"70507961363566124468475538524172170043725846352735387705515911177933084557518\"]',\n", - " 'volume24hrClob': 93644.03624199987,\n", - " 'volume1wkClob': 93644.03624199987,\n", - " 'volume1moClob': 93644.03624199987,\n", - " 'volume1yrClob': 93644.03624199987,\n", - " 'volumeClob': 94249.88157999996,\n", - " 'liquidityClob': 12631.9812,\n", + " 'volume24hr': 1149.1770909999998,\n", + " 'volume1wk': 1149.1770909999998,\n", + " 'volume1mo': 1149.1770909999998,\n", + " 'volume1yr': 1149.1770909999998,\n", + " 'clobTokenIds': '[\"111612048087962925846397645788043113901565915142888535086349545305985085081594\", \"12874695654084258339187997850118078073676562138805309130550135898707856234061\"]',\n", + " 'volume24hrClob': 1149.1770909999998,\n", + " 'volume1wkClob': 1149.1770909999998,\n", + " 'volume1moClob': 1149.1770909999998,\n", + " 'volume1yrClob': 1149.1770909999998,\n", + " 'volumeClob': 1149.1770909999996,\n", + " 'liquidityClob': 19009.3694,\n", " 'makerBaseFee': 1000,\n", " 'takerBaseFee': 1000,\n", " 'acceptingOrders': True,\n", " 'negRisk': False,\n", " 'ready': False,\n", " 'funded': False,\n", - " 'acceptingOrdersTimestamp': '2026-03-30T05:52:46Z',\n", + " 'acceptingOrdersTimestamp': '2026-04-03T05:22:35Z',\n", " 'cyom': False,\n", - " 'competitive': 0.8767123287671234,\n", + " 'competitive': 0.9999750006249843,\n", " 'pagerDutyNotificationEnabled': False,\n", " 'approved': True,\n", " 'rewardsMinSize': 50,\n", " 'rewardsMaxSpread': 4.5,\n", " 'spread': 0.01,\n", - " 'lastTradePrice': 0.89,\n", - " 'bestBid': 0.87,\n", - " 'bestAsk': 0.88,\n", + " 'lastTradePrice': 0.51,\n", + " 'bestBid': 0.5,\n", + " 'bestAsk': 0.51,\n", " 'automaticallyActive': True,\n", " 'clearBookOnStart': False,\n", " 'showGmpSeries': False,\n", @@ -152,7 +152,7 @@ " 'pendingDeployment': False,\n", " 'deploying': False,\n", " 'rfqEnabled': False,\n", - " 'eventStartTime': '2026-03-31T05:45:00Z',\n", + " 'eventStartTime': '2026-04-04T05:15:00Z',\n", " 'holdingRewardsEnabled': False,\n", " 'feesEnabled': True,\n", " 'requiresTranslation': False,\n", @@ -164,7 +164,7 @@ " 'rebateRate': 0.2}}" ] }, - "execution_count": 137, + "execution_count": 4, "metadata": {}, "output_type": "execute_result" } @@ -175,22 +175,22 @@ }, { "cell_type": "code", - "execution_count": 132, + "execution_count": 5, "id": "5ba43ffc", "metadata": {}, "outputs": [ { "data": { "text/plain": [ - "{'Up': '29663568421665501825278796284809925893978140634751674792176179244450686939029',\n", - " 'Down': '17234806616394620691850452772328817031586809400835664344689152810552758258009',\n", + "{'Up': '111612048087962925846397645788043113901565915142888535086349545305985085081594',\n", + " 'Down': '12874695654084258339187997850118078073676562138805309130550135898707856234061',\n", " 'isActive': False,\n", " 'MinTickSize': 0.01,\n", " 'isNegRisk': False,\n", - " 'ConditionId': '0x1778b4a38f2ce99260f9d3d0d78729d37fbd24439e06e6c92aad429439e0f7b2'}" + " 'ConditionId': '0xa71ed436d160cc45bf182b9004e0c10b16322ea1b41375182b747c2629223ecc'}" ] }, - "execution_count": 132, + "execution_count": 5, "metadata": {}, "output_type": "execute_result" } @@ -201,7 +201,7 @@ }, { "cell_type": "code", - "execution_count": 124, + "execution_count": 6, "id": "5d356d3b", "metadata": {}, "outputs": [ @@ -210,7 +210,7 @@ "output_type": "stream", "text": [ "creating client...\n", - "You've made 55 trades\n", + "You've made 297 trades\n", "client created successfully!\n" ] } @@ -221,17 +221,51 @@ }, { "cell_type": "code", - "execution_count": 128, + "execution_count": null, + "id": "22eb81de", + "metadata": {}, + "outputs": [], + "source": [ + "# Filtered by market\n", + "order_status = client.get_orders(\n", + " OpenOrderParams(id=\"0x9249ce4a8bc67de355b487b00eaa6ce25c1b451867f9350672b25eaa1de08494\")\n", + ")[0]['status']" + ] + }, + { + "cell_type": "code", + "execution_count": 14, + "id": "fb3b8151", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "'CANCELED'" + ] + }, + "execution_count": 14, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "order" + ] + }, + { + "cell_type": "code", + "execution_count": 8, "id": "de5ccc3a", "metadata": {}, "outputs": [ { "data": { "text/plain": [ - "7.6829" + "0.0" ] }, - "execution_count": 128, + "execution_count": 8, "metadata": {}, "output_type": "execute_result" } @@ -366,11 +400,12 @@ "name": "stdout", "output_type": "stream", "text": [ - "[{'errorMsg': '', 'orderID': '0x575c43f35b1f5e3e01779df5293d987dc81a347a4e09423a856ecb45555e30cb', 'takingAmount': '', 'makingAmount': '', 'status': 'live', 'success': True}]\n" + "[{'errorMsg': '', 'orderID': '0x0c701329ddd7881505648c5ebdd03cd6f86ae3a70b0cf150bcb53946e043e6bf', 'takingAmount': '', 'makingAmount': '', 'status': 'live', 'success': True}]\n" ] } ], "source": [ + "%%time\n", "### POST \n", "response = client.post_orders([\n", " PostOrdersArgs(\n", @@ -1465,25 +1500,100 @@ }, { "cell_type": "code", - "execution_count": 1, - "id": "56b8e1cc", + "execution_count": 35, + "id": "4d524867", "metadata": {}, "outputs": [], "source": [ - "o = {'type': 'TRADE', 'id': '1de1c277-d281-4be5-a388-305beb95e713', 'taker_order_id': '0xc77331796ad40133a2b4d9a387c10d326c180ad1195306240416150e337f0376', 'market': '0x1cdd4f44f73da702120f02962bf02a2599a7573a39b05ac8ca12a24301542e3e', 'asset_id': '108194336731497303295771496985972597853032131719077291707737599874190461476381', 'side': 'BUY', 'size': '5', 'fee_rate_bps': '1000', 'price': '0.7', 'status': 'CONFIRMED', 'match_time': '1775025187', 'last_update': '1775025253', 'outcome': 'Up', 'owner': '00e5d36c-6c46-77e1-a436-0f0a4bfbfdd8', 'trade_owner': '00e5d36c-6c46-77e1-a436-0f0a4bfbfdd8', 'maker_address': '0xb2967A7e578E700E27611238B7F762BdADC72CcB', 'transaction_hash': '0x3654fac56e1548bcc14d01ce4888ea0c3cec29d553009e054fc67088ece5f5b3', 'bucket_index': 0, 'maker_orders': [{'order_id': '0xb9a39a5e8082846bfdd92d33fba5112466b939ad7f33b76905982549767acf0a', 'owner': '70dd335a-6089-6022-6726-565c7480b291', 'maker_address': '0x010138da36CF100c00a5A59C8643E4A4A55DF19C', 'matched_amount': '5', 'price': '0.3', 'fee_rate_bps': '1000', 'asset_id': '26175835004768067043908573558643253609047604517388456461023977022509795794691', 'outcome': 'Down', 'outcome_index': 0, 'side': 'BUY'}], 'trader_side': 'TAKER', 'timestamp': '1775025253290', 'event_type': 'trade'}" + "o = {'orderID': '0xaebd4053bd167eb7a7fc48ae29036829582787a73d8cc7b8b1afa7294c474972',\n", + " 'owner': '00e5d36c-6c46-77e1-a436-0f0a4bfbfdd8',\n", + " 'market': '0xf856ff89eb9ed2ab393a1bbc496b5db539b4d7f9cec91202b13c97379e9e58d6',\n", + " 'asset_id': '39378292107289994981363071337831788209917841322683175778910172170676449806535',\n", + " 'side': 'BUY',\n", + " 'order_owner': '00e5d36c-6c46-77e1-a436-0f0a4bfbfdd8',\n", + " 'original_size': '10',\n", + " 'size_matched': '0',\n", + " 'price': '0.2',\n", + " 'associate_trades': [],\n", + " 'outcome': 'Up',\n", + " 'type': 'CANCELLATION',\n", + " 'created_at': '1774818626',\n", + " 'expiration': '0',\n", + " 'order_type': 'GTC',\n", + " 'status': 'CANCELED',\n", + " 'maker_address': '0xb2967A7e578E700E27611238B7F762BdADC72CcB',\n", + " 'timestamp': '1774818630291',\n", + " 'event_type': 'order'}" + ] + }, + { + "cell_type": "code", + "execution_count": 47, + "id": "43a68ee8", + "metadata": {}, + "outputs": [], + "source": [ + "ut = {'trade_side':'MAKER', 'taker_order_id': None, 'maker_orders': [{\"side\": \"BUY\", \"owner\": \"f44394d9-7782-e5e5-af99-dd515edd7fd9\", \"price\": \"0.44\", \"outcome\": \"Up\", \"asset_id\": \"55598909213399021321632159985071802901933471635502091907595118948537230110477\", \"order_id\": \"0x7eed6fa75cd49680b738e1563fc9e5dc0f7095c8a7dd3ee1a496c01f4ad2f610\", \"fee_rate_bps\": \"1000\", \"maker_address\": \"0xE29042f5D913DCC4015aaB3455C13C58514CA33F\", \"outcome_index\": 0, \"matched_amount\": \"15.25\"}, {\"side\": \"BUY\", \"owner\": \"520cb8f0-a71b-1c49-8dd6-751e682ee7f8\", \"price\": \"0.44\", \"outcome\": \"Up\", \"asset_id\": \"55598909213399021321632159985071802901933471635502091907595118948537230110477\", \"order_id\": \"0xf7c0a9e322fbf232dce6290f36bbe5e484ccec7fda599ddeb98867f241df6e37\", \"fee_rate_bps\": \"1000\", \"maker_address\": \"0x74a6364297292774c7f9a16B925207E77eEE262D\", \"outcome_index\": 0, \"matched_amount\": \"5\"}, {\"side\": \"BUY\", \"owner\": \"ae0f07ea-bcbe-3d54-6841-f220e89794ae\", \"price\": \"0.44\", \"outcome\": \"Up\", \"asset_id\": \"55598909213399021321632159985071802901933471635502091907595118948537230110477\", \"order_id\": \"0xe321494520de4737980a15160bfae94cb48791a2dc978aa843e7a8504e815771\", \"fee_rate_bps\": \"1000\", \"maker_address\": \"0xDba9C86F8d20ac73BcBf4dedaA6ADbd26A0a1303\", \"outcome_index\": 0, \"matched_amount\": \"5\"}, {\"side\": \"BUY\", \"owner\": \"00e5d36c-6c46-77e1-a436-0f0a4bfbfdd8\", \"price\": \"0.44\", \"outcome\": \"Up\", \"asset_id\": \"55598909213399021321632159985071802901933471635502091907595118948537230110477\", \"order_id\": \"0xaebd4053bd167eb7a7fc48ae29036829582787a73d8cc7b8b1afa7294c474972\", \"fee_rate_bps\": \"1000\", \"maker_address\": \"0xb2967A7e578E700E27611238B7F762BdADC72CcB\", \"outcome_index\": 0, \"matched_amount\": \"4.75\"}]}" + ] + }, + { + "cell_type": "code", + "execution_count": 45, + "id": "ae50f082", + "metadata": {}, + "outputs": [], + "source": [ + "USER_TRADES = [ut]\n", + "user_trade = next( ( item for item in USER_TRADES if ( o['orderID'] == item['taker_order_id'] ) or ( o[\"orderID\"] == item['maker_orders'][0]['order_id'] ) ), None )" ] }, { "cell_type": "code", "execution_count": null, - "id": "ae50f082", + "id": "11c111eb", "metadata": {}, "outputs": [], "source": [ - "USER_TRADES = [o]\n", - "user_trade = next( ( item for item in USER_TRADES if ( o['orderID'] == item['taker_order_id'] ) or ( o[\"orderID\"] == item['maker_orders'][0]['order_id'] ) ), None )" + "for t in USER_TRADES:\n", + " if t['trade_side']=='MAKER':\n", + " pass\n", + " elif t['taker_order_id'] == o[\"orderID\"]:\n", + " pass\n" ] }, + { + "cell_type": "code", + "execution_count": 46, + "id": "de38feda", + "metadata": {}, + "outputs": [], + "source": [ + "user_trade" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "67b7b730", + "metadata": {}, + "outputs": [], + "source": [] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "d15d92d3", + "metadata": {}, + "outputs": [], + "source": [] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [] + }, { "cell_type": "code", "execution_count": null, diff --git a/ws_binance.py b/ws_binance.py index 64dc0c4..42645b7 100644 --- a/ws_binance.py +++ b/ws_binance.py @@ -36,7 +36,7 @@ LOG_FILEPATH: str = os.getenv("LOGS_PATH") + '/Polymarket_Binance_Trades.log' ### Globals ### WSS_URL = "wss://stream.binance.com:9443/ws/BTCUSDT@aggTrade" HIST_TRADES = np.empty((0, 3)) -HIST_TRADES_LOOKBACK_SEC = 5 +HIST_TRADES_LOOKBACK_SEC = 6 ### Database Funcs ### async def create_rtds_btcusd_table( @@ -132,7 +132,8 @@ async def binance_trades_stream(): last_px = float(data['p']) qty = float(data['q']) # print(f'🤑 BTC Binance Last Px: {last_px:_.4f}; TS: {pd.to_datetime(data['T'], unit='ms')}') - HIST_TRADES = np.append(HIST_TRADES, np.array([[timestamp_value, last_px, qty]]), axis=0) + # HIST_TRADES = np.append(HIST_TRADES, np.array([[timestamp_value, last_px, qty]]), axis=0) + HIST_TRADES = np.append(HIST_TRADES, np.array([[ts_arrival, last_px, qty]]), axis=0) hist_trades_lookback_ts_ms = round(datetime.now().timestamp() - HIST_TRADES_LOOKBACK_SEC)*1000 HIST_TRADES = HIST_TRADES[HIST_TRADES[:, 0] >= hist_trades_lookback_ts_ms] VAL_KEY_OBJ = json.dumps({ diff --git a/ws_clob.py b/ws_clob.py index edef2ad..ed381ea 100644 --- a/ws_clob.py +++ b/ws_clob.py @@ -13,6 +13,7 @@ from typing import AsyncContextManager from sqlalchemy.ext.asyncio import create_async_engine from sqlalchemy import text import valkey +import time import os from dotenv import load_dotenv @@ -27,7 +28,7 @@ VAL_KEY = None ### Logging ### load_dotenv() -LOG_FILEPATH: str = os.getenv("LOGS_PATH") + '/Polymarket_5min.log' +LOG_FILEPATH: str = os.getenv("LOGS_PATH") + '/Polymarket_CLOB.log' WSS_URL = "wss://ws-subscriptions-clob.polymarket.com/ws/market" SLUG_END_TIME = 0 @@ -67,10 +68,10 @@ def get_mkt_details_by_slug(slug: str) -> dict[str, str, str]: # {'Up' : 123, 'D d['ConditionId'] = market['conditionId'] d['EndDateTime'] = market['endDate'] # d['Liquidity'] = market['liquidity'] - d['LiquidityClob'] = market['liquidityClob'] - d['VolumeNum'] = market['volumeNum'] - d['Volume24hr'] = market['volume24hr'] - print(market) + # d['LiquidityClob'] = market['liquidityClob'] + # d['VolumeNum'] = market['volumeNum'] + # d['Volume24hr'] = market['volume24hr'] + logging.info(f'MARKET CHANGED: {market}') return d, market @@ -197,6 +198,7 @@ async def polymarket_stream(): sec_remaining = SLUG_END_TIME - round(datetime.now().timestamp()) if sec_remaining <= 0: + time.sleep(0.1) ref_data = json.loads(VAL_KEY.get('poly_rtds_cl_btcusd')) TARGET_PX = float(ref_data['value']) HIST_TRADES = np.empty((0, 2)) @@ -316,15 +318,15 @@ async def polymarket_stream(): continue elif event_type == "market_resolved": print(f"Received: {event_type}") - print(data) + # print(data) continue elif event_type == "tick_size_change": # may want for CLOB order routing print(f"Received: {event_type}") - print(data) + # print(data) continue else: print(f"*********** REC UNMAPPED EVENT: {event_type}") - print(data) + # print(data) continue elif isinstance(data, dict): continue diff --git a/ws_user.py b/ws_user.py index 0d68d20..b1eeb88 100644 --- a/ws_user.py +++ b/ws_user.py @@ -246,7 +246,7 @@ async def insert_user_orders_table( ### Helpers ### def live_orders_only(orders: list[dict]) -> list[dict]: - return [d for d in orders if d.get('status')=='LIVE'] + return [d for d in orders if d.get('status') in ['LIVE','MATCHED']] def upsert_list_of_dicts_by_id(list_of_dicts, new_dict): for index, item in enumerate(list_of_dicts): @@ -284,7 +284,6 @@ async def polymarket_stream(): await websocket.send(json.dumps(subscribe_msg)) print("Subscribed to User Data") - try: async for message in websocket: ts_arrival = round(datetime.now().timestamp()*1000) @@ -294,8 +293,8 @@ async def polymarket_stream(): await websocket.send(json.dumps({})) print('SENT HEARTBEAT PING') continue - data['timestamp_arrival'] = ts_arrival + data['timestamp_arrival'] = ts_arrival event_type = data.get('event_type', None) match event_type: case 'trade': @@ -326,15 +325,15 @@ async def polymarket_stream(): LOOKBACK_MIN_TS_MS = ts_arrival-LOCAL_RECENT_TRADES_LOOKBACK_SEC*1000 LOCAL_RECENT_TRADES = [t for t in LOCAL_RECENT_TRADES if t.get('timestamp_arrival', 0) >= LOOKBACK_MIN_TS_MS] - # print("---------------------") - # print(LOCAL_RECENT_TRADES) - # print("---------------------") VAL_KEY_OBJ = json.dumps(LOCAL_RECENT_TRADES) # VAL_KEY.publish(VK_CHANNEL, VAL_KEY_OBJ) + logging.info("----------LOCAL_RECENT_TRADES-VK-----------") + logging.info(VAL_KEY_OBJ) + logging.info("-------------------------------------------") VAL_KEY.set(VK_RECENT_TRADES, VAL_KEY_OBJ) - logging.info(f'User Trade Update: {data}') + # logging.info(f'User Trade Update: {data}') ### Insert into DB ### await insert_user_trades_table( -- 2.49.1