Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -65,3 +65,5 @@ src/user-applications/notebook/main.ipynb
# Sub-account data
#src/apps/sub-accounts/data/accounts/
#src/apps/sub-accounts/data/trades/
/src/logs
/data/accounts
164 changes: 164 additions & 0 deletions kraken_ws/account.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,10 @@ def __init__(self, api_key: Optional[str] = None, api_secret: Optional[str] = No
# Add handlers for private data streams
self._private_handlers: Dict[str, List[Callable]] = {}
self._subscriptions: Dict[str, Dict] = {}

# Cancel on disconnect settings
self._cancel_on_disconnect_enabled = False
self._cancel_on_disconnect_timeout = None

@classmethod
async def create(cls, api_key: Optional[str] = None, api_secret: Optional[str] = None):
Expand Down Expand Up @@ -239,6 +243,9 @@ async def _handle_ws_messages_v2(self):

except websockets.exceptions.ConnectionClosed:
logger.warning("WebSocket v2 connection closed.")
# Trigger cancel on disconnect if enabled
if self._cancel_on_disconnect_enabled:
logger.info("Connection lost - cancel_on_disconnect is enabled, orders should be cancelled automatically")
except Exception as e:
logger.error(f"Error in WebSocket v2 message handler: {e}", exc_info=True)
finally:
Expand Down Expand Up @@ -291,6 +298,158 @@ async def _send_subscription_v2(self, subscription: Dict):
await self._ws_connection.send(json.dumps(subscription))
logger.debug(f"WS v2 Subscription Sent: {subscription}")

# --- Cancel All Orders After (Dead Man's Switch) Methods ---

async def set_cancel_all_orders_after(self, timeout: int) -> Dict:
"""
Set up a "Dead Man's Switch" that will cancel all orders after the specified timeout.
This must be called periodically to reset the timer and prevent cancellation.

Args:
timeout: Duration in seconds to set/extend the timer (max 86400 seconds).
Set to 0 to disable the mechanism.

Returns:
Response from the server with currentTime and triggerTime.
"""
if timeout < 0 or timeout > 86400:
raise ValueError("Timeout must be between 0 and 86400 seconds")

payload = {
"method": "cancel_all_orders_after",
"params": {
"timeout": timeout
}
}

try:
result = await self._send_request_v2(payload)

if timeout > 0:
self._cancel_on_disconnect_enabled = True
self._cancel_on_disconnect_timeout = timeout
logger.info(f"Cancel all orders after timer set to {timeout} seconds")
else:
self._cancel_on_disconnect_enabled = False
self._cancel_on_disconnect_timeout = None
logger.info("Cancel all orders after timer disabled")

return result

except Exception as e:
logger.error(f"Failed to set cancel all orders after: {e}")
raise

async def enable_cancel_on_disconnect(self, timeout: int = 60) -> Dict:
"""
Enable the "Dead Man's Switch" mechanism with the specified timeout.

Args:
timeout: Duration in seconds (recommended: 60). Must be > 0.

Returns:
Response from the server.
"""
if timeout <= 0:
raise ValueError("Timeout must be greater than 0 to enable")

return await self.set_cancel_all_orders_after(timeout)

async def disable_cancel_on_disconnect(self) -> Dict:
"""
Disable the "Dead Man's Switch" mechanism.

Returns:
Response from the server.
"""
return await self.set_cancel_all_orders_after(0)

async def reset_cancel_timer(self, timeout: int = 60) -> Dict:
"""
Reset the cancel timer to prevent order cancellation.
Should be called periodically (every 15-30 seconds recommended).

Args:
timeout: Duration in seconds to reset the timer to.

Returns:
Response from the server.
"""
return await self.set_cancel_all_orders_after(timeout)

async def get_cancel_on_disconnect_status(self) -> Dict:
"""
Get the current cancel on disconnect status.
Note: This returns local state. The actual timer status is on the server.

Returns:
Dict containing enabled status and timeout (if applicable).
"""
return {
"enabled": self._cancel_on_disconnect_enabled,
"timeout": self._cancel_on_disconnect_timeout,
"note": "This shows local state. Call set_cancel_all_orders_after() to get server response with current/trigger times."
}

async def set_cancel_on_disconnect(self, enabled: bool, timeout: int = 60) -> Dict:
"""
Convenience method to enable or disable the cancel mechanism.

Args:
enabled: Whether to enable or disable the mechanism
timeout: Duration in seconds when enabling (ignored when disabling)

Returns:
Response from the server.
"""
if enabled:
return await self.enable_cancel_on_disconnect(timeout)
else:
return await self.disable_cancel_on_disconnect()

async def start_cancel_timer_task(self, timeout: int = 60, reset_interval: int = 30) -> asyncio.Task:
"""
Start a background task that automatically resets the cancel timer.

Args:
timeout: Duration in seconds for the cancel timer
reset_interval: How often to reset the timer (in seconds)

Returns:
The asyncio Task that can be cancelled to stop the auto-reset
"""
async def reset_timer_loop():
try:
# Initial setup
await self.set_cancel_all_orders_after(timeout)
logger.info(f"Started auto-reset timer: {timeout}s timeout, reset every {reset_interval}s")

while True:
await asyncio.sleep(reset_interval)
if self._ws_authenticated and self._ws_connection:
try:
result = await self.set_cancel_all_orders_after(timeout)
logger.debug(f"Timer reset successful: {result.get('result', {}).get('triggerTime', 'N/A')}")
except Exception as e:
logger.error(f"Failed to reset cancel timer: {e}")
else:
logger.warning("WebSocket not connected, skipping timer reset")

except asyncio.CancelledError:
logger.info("Cancel timer auto-reset task cancelled")
# Try to disable the timer on cancellation
try:
if self._ws_authenticated and self._ws_connection:
await self.set_cancel_all_orders_after(0)
logger.info("Disabled cancel timer on task cancellation")
except:
pass
raise
except Exception as e:
logger.error(f"Error in cancel timer task: {e}")

return asyncio.create_task(reset_timer_loop())

# --- Private Data Subscriptions (v2 format) ---

async def subscribe_own_trades(self,
Expand Down Expand Up @@ -662,6 +821,11 @@ async def query_trades_info(self, txid: Union[str, List[str]]) -> Dict:

async def close(self):
"""Closes the WebSocket connection and cleans up resources."""
try:
self.cancel_all_orders()
except:
pass

if self._message_handler_task:
self._message_handler_task.cancel()
try:
Expand Down
105 changes: 104 additions & 1 deletion kraken_ws/kraken_ws.py
Original file line number Diff line number Diff line change
Expand Up @@ -150,7 +150,7 @@ async def subscribe_own_trades(self,
await self.account.connect_v2()

await self.account.subscribe_own_trades(
snapshot=snapshot,
snap_trades=snapshot,
consolidate_taker=consolidate_taker,
handler=handler
)
Expand All @@ -170,6 +170,109 @@ async def unsubscribe_open_orders(self):
"""Unsubscribe from open orders data stream."""
await self.account.unsubscribe_open_orders()

# --- Cancel All Orders After (Dead Man's Switch) Methods ---

async def set_cancel_all_orders_after(self, timeout: int) -> Dict:
"""
Set up a "Dead Man's Switch" that will cancel all orders after the specified timeout.
This must be called periodically to reset the timer and prevent cancellation.

Args:
timeout: Duration in seconds to set/extend the timer (max 86400 seconds).
Set to 0 to disable the mechanism.

Returns:
Response from the server with currentTime and triggerTime.
"""
if not self.account.connected():
await self.account.connect_v2()

return await self.account.set_cancel_all_orders_after(timeout)

async def enable_cancel_on_disconnect(self, timeout: int = 60) -> Dict:
"""
Enable the "Dead Man's Switch" mechanism with the specified timeout.

Args:
timeout: Duration in seconds (recommended: 60). Must be > 0.

Returns:
Response from the server.
"""
if not self.account.connected():
await self.account.connect_v2()

return await self.account.enable_cancel_on_disconnect(timeout)

async def disable_cancel_on_disconnect(self) -> Dict:
"""
Disable the "Dead Man's Switch" mechanism.

Returns:
Response from the server.
"""
if not self.account.connected():
await self.account.connect_v2()

return await self.account.disable_cancel_on_disconnect()

async def reset_cancel_timer(self, timeout: int = 60) -> Dict:
"""
Reset the cancel timer to prevent order cancellation.
Should be called periodically (every 15-30 seconds recommended).

Args:
timeout: Duration in seconds to reset the timer to.

Returns:
Response from the server.
"""
if not self.account.connected():
await self.account.connect_v2()

return await self.account.reset_cancel_timer(timeout)

async def get_cancel_on_disconnect_status(self) -> Dict:
"""
Get the current cancel on disconnect status.

Returns:
Dict containing enabled status and timeout (if applicable).
"""
return await self.account.get_cancel_on_disconnect_status()

async def set_cancel_on_disconnect(self, enabled: bool, timeout: int = 60) -> Dict:
"""
Convenience method to enable or disable the cancel mechanism.

Args:
enabled: Whether to enable or disable the mechanism
timeout: Duration in seconds when enabling (ignored when disabling)

Returns:
Response from the server.
"""
if not self.account.connected():
await self.account.connect_v2()

return await self.account.set_cancel_on_disconnect(enabled, timeout)

async def start_cancel_timer_task(self, timeout: int = 60, reset_interval: int = 30) -> asyncio.Task:
"""
Start a background task that automatically resets the cancel timer.

Args:
timeout: Duration in seconds for the cancel timer
reset_interval: How often to reset the timer (in seconds)

Returns:
The asyncio Task that can be cancelled to stop the auto-reset
"""
if not self.account.connected():
await self.account.connect_v2()

return await self.account.start_cancel_timer_task(timeout, reset_interval)

# --- Enhanced Trading Methods (v2 API) ---

async def add_order_v2(self, symbol: str, side: str, order_type: str,
Expand Down
2 changes: 1 addition & 1 deletion resources/data/settings/settings.yaml
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
program:
name: "TradeByte"
version: "2.1.2"
version: "2.1.3"
debug: false
Loading
Loading