Spot Trading Bot Templates

The templates presented below serve as starting points for the development of a trading algorithms for Spot trading on the cryptocurrency exchange Kraken using the python-kraken-sdk.

The trading strategy can be implemented using the TradingBot class. This class has access to all REST clients and receives all messages that are sent by the subscribed websocket feeds via the on_message function.

Template to build a trading bot using the Kraken Spot Websocket API v2
  1#!/usr/bin/env python
  2# Copyright (C) 2023 Benjamin Thomas Schwertfeger
  3# GitHub: https://github.com/btschwertfeger
  4# ruff: noqa: RUF027
  5"""
  6Module that provides a template to build a Spot trading algorithm using the
  7python-kraken-sdk and Kraken Spot websocket API v2.
  8"""
  9
 10from __future__ import annotations
 11
 12import asyncio
 13import logging
 14import logging.config
 15import os
 16import sys
 17import traceback
 18
 19import requests
 20import urllib3
 21
 22from kraken.exceptions import KrakenAuthenticationError  # , KrakenPermissionDeniedError
 23from kraken.spot import Funding, Market, SpotWSClient, Trade, User
 24
 25logging.basicConfig(
 26    format="%(asctime)s %(module)s,line: %(lineno)d %(levelname)8s | %(message)s",
 27    datefmt="%Y/%m/%d %H:%M:%S",
 28    level=logging.INFO,
 29)
 30logging.getLogger("requests").setLevel(logging.WARNING)
 31logging.getLogger("urllib3").setLevel(logging.WARNING)
 32
 33LOG: logging.Logger = logging.getLogger(__name__)
 34
 35
 36class TradingBot(SpotWSClient):
 37    """
 38    Class that implements the trading strategy
 39
 40    * The on_message function gets all messages sent by the websocket feeds.
 41    * Decisions can be made based on these messages
 42    * Can place trades using the self.__trade client or self.send_message
 43    * Do everything you want
 44
 45    ====== P A R A M E T E R S ======
 46    config: dict
 47        configuration like: {
 48            "key": "kraken-spot-key",
 49            "secret": "kraken-spot-secret",
 50            "pairs": ["DOT/USD", "BTC/USD"],
 51        }
 52    """
 53
 54    def __init__(
 55        self: TradingBot,
 56        config: dict,
 57        **kwargs: object | dict | set | tuple | list | str | float | None,
 58    ) -> None:
 59        super().__init__(
 60            key=config["key"],
 61            secret=config["secret"],
 62            **kwargs,
 63        )
 64        self.__config: dict = config
 65
 66        self.__user: User = User(key=config["key"], secret=config["secret"])
 67        self.__trade: Trade = Trade(key=config["key"], secret=config["secret"])
 68        self.__market: Market = Market(key=config["key"], secret=config["secret"])
 69        self.__funding: Funding = Funding(key=config["key"], secret=config["secret"])
 70
 71    async def on_message(self: TradingBot, message: dict) -> None:
 72        """Receives all messages of the websocket connection(s)"""
 73        if message.get("method") == "pong" or message.get("channel") == "heartbeat":
 74            return
 75        if "error" in message:
 76            # handle exceptions/errors sent by websocket connection …
 77            pass
 78
 79        LOG.info(message)
 80
 81        # == apply your trading strategy here ==
 82
 83        # Call functions of `self.__trade` and other clients if conditions met …
 84        # try:
 85        #     print(self.__trade.create_order(
 86        #         ordertype='limit',
 87        #         side='buy',
 88        #         volume=2,
 89        #         pair='XBTUSD',
 90        #         price=12000
 91        #     ))
 92        # except KrakenPermissionDeniedError:
 93        #    # … handle exceptions
 94        #    pass
 95
 96        # The spot websocket client also allow sending orders via websockets
 97        # this is way faster than using REST endpoints.
 98        # await self.send_message(
 99        #     message={
100        #         "method": "add_order",
101        #         "params": {
102        #             "limit_price": 1234.56,
103        #             "order_type": "limit",
104        #             "order_userref": 123456789,
105        #             "order_qty": 1.0,
106        #             "side": "buy",
107        #             "symbol": "BTC/USD",
108        #             "validate": True,
109        #         },
110        #     }
111        # )
112
113        # You can also un-/subscribe here using `self.subscribe(...)` or
114        # `self.unsubscribe(...)`.
115        #
116        # … more can be found in the documentation
117        #        (https://python-kraken-sdk.readthedocs.io/en/stable/).
118
119    # Add more functions to customize the trading strategy …
120
121    def save_exit(self: TradingBot, reason: str | None = "") -> None:
122        """controlled shutdown of the strategy"""
123        LOG.warning(
124            "Save exit triggered, reason: {reason}",
125            extra={"reason": reason},
126        )
127        # some ideas:
128        #   * save the current data
129        #   * maybe close trades
130        #   * enable dead man's switch
131        sys.exit(1)
132
133
134class Manager:
135    """
136    Class to manage the trading strategy
137
138    … subscribes to desired feeds, instantiates the strategy and runs as long
139    as there is no error.
140
141    ====== P A R A M E T E R S ======
142    config: dict
143        configuration like: {
144            "key" "kraken-spot-key",
145            "secret": "kraken-secret-key",
146            "pairs": ["DOT/USD", "BTC/USD"],
147        }
148    """
149
150    def __init__(self: Manager, config: dict) -> None:
151        self.__config: dict = config
152        self.__trading_strategy: TradingBot | None = None
153
154    def run(self: Manager) -> None:
155        """Starts the event loop and bot"""
156        if not self.__check_credentials():
157            sys.exit(1)
158
159        try:
160            asyncio.run(self.__main())
161        except KeyboardInterrupt:
162            self.save_exit(reason="KeyboardInterrupt")
163        else:
164            self.save_exit(reason="Asyncio loop left")
165
166    async def __main(self: Manager) -> None:
167        """
168        Instantiates the trading strategy and subscribes to the desired
169        websocket feeds. While no exception within the strategy occur run the
170        loop.
171
172        The variable `exception_occur` which is an attribute of the SpotWSClient
173        can be set individually but is also being set to `True` if the websocket
174        connection has some fatal error. This is used to exit the asyncio loop -
175        but you can also apply your own reconnect rules.
176        """
177        self.__trading_strategy = TradingBot(config=self.__config)
178        await self.__trading_strategy.start()
179
180        await self.__trading_strategy.subscribe(
181            params={"channel": "ticker", "symbol": self.__config["pairs"]},
182        )
183        await self.__trading_strategy.subscribe(
184            params={
185                "channel": "ohlc",
186                "interval": 15,
187                "symbol": self.__config["pairs"],
188            },
189        )
190
191        await self.__trading_strategy.subscribe(params={"channel": "executions"})
192
193        while not self.__trading_strategy.exception_occur:
194            try:
195                # check if the algorithm feels good
196                # maybe send a status update every day via Telegram or Mail
197                # …
198                pass
199
200            except Exception as exc:
201                message: str = f"Exception in main: {exc} {traceback.format_exc()}"
202                LOG.error(message)
203                self.__trading_strategy.save_exit(reason=message)
204
205            await asyncio.sleep(6)
206        self.__trading_strategy.save_exit(
207            reason="Left main loop because of exception in strategy.",
208        )
209
210    def __check_credentials(self: Manager) -> bool:
211        """Checks the user credentials and the connection to Kraken"""
212        try:
213            User(self.__config["key"], self.__config["secret"]).get_account_balance()
214            LOG.info("Client credentials are valid.")
215            return True
216        except urllib3.exceptions.MaxRetryError:
217            LOG.error("MaxRetryError, cannot connect.")
218            return False
219        except requests.exceptions.ConnectionError:
220            LOG.error("ConnectionError, Kraken not available.")
221            return False
222        except KrakenAuthenticationError:
223            LOG.error("Invalid credentials!")
224            return False
225
226    def save_exit(self: Manager, reason: str = "") -> None:
227        """Invoke the save exit function of the trading strategy"""
228        print(f"Save exit triggered - {reason}")
229        if self.__trading_strategy is not None:
230            self.__trading_strategy.save_exit(reason=reason)
231        else:
232            sys.exit(1)
233
234
235def main() -> None:
236    """Example main - load environment variables and run the strategy."""
237    manager: Manager = Manager(
238        config={
239            "key": os.getenv("SPOT_API_KEY"),
240            "secret": os.getenv("SPOT_SECRET_KEY"),
241            "pairs": ["DOT/USD", "BTC/USD"],
242        },
243    )
244
245    try:
246        manager.run()
247    except Exception:
248        manager.save_exit(
249            reason=f"manageBot.run() has ended: {traceback.format_exc()}",
250        )
251
252
253if __name__ == "__main__":
254    main()