From f7f91a8a0438158b5ca5d2ed0d04a347dd1b2a4f Mon Sep 17 00:00:00 2001 From: Ryan Watts Date: Sun, 3 Aug 2025 16:18:59 -0600 Subject: [PATCH] feat: add order management and connection checks Introduced get_open_orders and cancel_all_open_orders methods to KrakenPythonClient for improved order management. Added check_connection method to KrakenWebSocket for connection status verification. Updated settings.yaml to version 2.2.1 with version notes. Added sample trade and account data for account 5. --- data/trades/5_trades.json | 1 + kraken_ws/kraken_ws.py | 23 ++++ resources/data/settings/settings.yaml | 8 +- .../subaccounts/data/accounts/account_5.json | 14 +++ src/clients/kraken_python_client.py | 108 ++++++++++++++++++ 5 files changed, 153 insertions(+), 1 deletion(-) create mode 100644 data/trades/5_trades.json create mode 100644 src/apps/subaccounts/data/accounts/account_5.json diff --git a/data/trades/5_trades.json b/data/trades/5_trades.json new file mode 100644 index 0000000..ae65d44 --- /dev/null +++ b/data/trades/5_trades.json @@ -0,0 +1 @@ +{"timestamp": "2025-08-03 13:06:22.359621", "side": "BUY", "pair": "EURQ/USD", "quantity": 1.0074, "price": 1.1536, "total_value": 1.16213664, "account_id": 5, "base_currency": "EURQ", "quote_currency": "ZUSD", "balances_after": {"EURQ": 6.0074000000000005, "ZUSD": 48.1963899}} diff --git a/kraken_ws/kraken_ws.py b/kraken_ws/kraken_ws.py index 999ccc4..ad61263 100644 --- a/kraken_ws/kraken_ws.py +++ b/kraken_ws/kraken_ws.py @@ -39,6 +39,29 @@ def __init__(self, api_key: Optional[str] = None, api_secret: Optional[str] = No self._running = False + def check_connection(self) -> bool: + """ + Check if the WebSocket connections are active. + + Returns: + bool: True if public connection is active, False otherwise. + Also considers account connection status if account has credentials. + """ + # Check public connection + public_connected = ( + self.public_ws is not None and + not self.public_ws.closed and + self.is_connected + ) + + # If no API credentials, only check public connection + if not (hasattr(self.account, 'api_key') and self.account.api_key): + return public_connected + + # If API credentials exist, check both public and private connections + account_connected = self.account.connected() if hasattr(self.account, 'connected') else False + + return public_connected and account_connected async def connect(self, private: bool = False): """ diff --git a/resources/data/settings/settings.yaml b/resources/data/settings/settings.yaml index 35021a4..ef4a27b 100644 --- a/resources/data/settings/settings.yaml +++ b/resources/data/settings/settings.yaml @@ -1,4 +1,10 @@ program: name: "TradeByte" - version: "2.2.0" + version: "2.2.1" debug: false + version_notes: + - Version 2.2.1 8/2/2025 + - Added check_connection() function to kraken_ws.py + - Added check_connection() function to account.py + - Added get_open_orders() function to kraken_python_client.py + - Added cancel_all_open_orders() function to kraken_python_client.py \ No newline at end of file diff --git a/src/apps/subaccounts/data/accounts/account_5.json b/src/apps/subaccounts/data/accounts/account_5.json new file mode 100644 index 0000000..188dc0e --- /dev/null +++ b/src/apps/subaccounts/data/accounts/account_5.json @@ -0,0 +1,14 @@ +{ + "account_id": 5, + "nick_name": "Sample Account", + "balance": { + "ZUSD": 0.0, + "ZEUR": 0.0 + }, + "created_date": "2025-08-03", + "active": true, + "balances": { + "ZUSD": 50.0, + "EURQ": 0.0 + } +} \ No newline at end of file diff --git a/src/clients/kraken_python_client.py b/src/clients/kraken_python_client.py index cae6adb..55295bf 100644 --- a/src/clients/kraken_python_client.py +++ b/src/clients/kraken_python_client.py @@ -3,6 +3,11 @@ import pandas as pd import requests import time +import urllib.parse +import hashlib +import hmac +import base64 +from typing import Dict """ This client is used to interact with the rust kraken API. @@ -16,6 +21,7 @@ def __init__(self, asset='XBTUSD', error_message=False, api_key=None, api_secret self.base_url = "https://api.kraken.com/0/public" self.api_key = api_key self.api_secret = api_secret + self.api_url = "https://api.kraken.com" def test_connection(self): try: @@ -333,6 +339,108 @@ def get_my_recent_trades(self, pair=None, since=None, count=None): if self.error_message: print(f"KrakenPythonClient.get_my_recent_trades: {e}") return False + def get_open_orders(self, pair: str = None) -> Dict: + """Get open orders, optionally filtered by trading pair""" + params = {} + if pair: + params['pair'] = pair + + return self._kraken_request("/0/private/OpenOrders", params, self.api_key, self.api_secret) + + def _get_kraken_signature(self, urlpath: str, data: Dict, secret: str) -> str: + """Generate Kraken API signature""" + postdata = urllib.parse.urlencode(data) + encoded = (str(data['nonce']) + postdata).encode() + message = urlpath.encode() + hashlib.sha256(encoded).digest() + + mac = hmac.new(base64.b64decode(secret), message, hashlib.sha512) + sigdigest = base64.b64encode(mac.digest()) + return sigdigest.decode() + + def _kraken_request(self, uri_path: str, data: Dict, api_key: str, api_secret: str) -> Dict: + """Make authenticated request to Kraken API""" + headers = {} + if api_key and api_secret: + data['nonce'] = str(int(1000 * time.time())) + headers['API-Key'] = api_key + headers['API-Sign'] = self._get_kraken_signature(uri_path, data, api_secret) + + req = requests.post(f"{self.api_url}{uri_path}", headers=headers, data=data) + return req.json() + + def cancel_all_open_orders(self, pair: str) -> Dict: + """Cancel all open orders for a specific trading pair""" + # First get all open orders for the pair + open_orders_response = self.get_open_orders(pair) + + # Check if the request was successful + if 'error' in open_orders_response and open_orders_response['error']: + return open_orders_response + + # Extract order IDs from the response + open_orders = open_orders_response.get('result', {}).get('open', {}) + + if not open_orders: + return { + 'error': [], + 'result': { + 'count': 0, + 'message': f'No open orders found for pair {pair}' + } + } + + # Collect all order IDs for the specified pair + order_ids = [] + for order_id, order_data in open_orders.items(): + # Double-check the pair matches (in case API filtering didn't work perfectly) + order_pair = order_data.get('descr', {}).get('pair', '') + if order_pair == pair or not pair: # Include all if pair filtering failed + order_ids.append(order_id) + + if not order_ids: + return { + 'error': [], + 'result': { + 'count': 0, + 'message': f'No open orders found for pair {pair}' + } + } + + # Cancel orders one by one (more reliable than batch) + cancelled_orders = [] + failed_orders = [] + + for order_id in order_ids: + cancel_params = { + 'txid': order_id + } + + cancel_response = self._kraken_request( + "/0/private/CancelOrder", + cancel_params, + self.api_key, + self.api_secret + ) + + if 'error' in cancel_response and cancel_response['error']: + failed_orders.append({ + 'order_id': order_id, + 'error': cancel_response['error'] + }) + else: + cancelled_orders.append(order_id) + + # Return summary response + return { + 'error': [], + 'result': { + 'cancelled_count': len(cancelled_orders), + 'failed_count': len(failed_orders), + 'cancelled_orders': cancelled_orders, + 'failed_orders': failed_orders, + 'pair': pair + } + } def cancel_all_orders(self, asset=None): """