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()