Futures Trading Bot Example

The template presented below serves as a starting point for the development of a trading algorithm for trading futures contracts on the cryptocurrency platform Kraken using the python-kraken-sdk.

The ManagedBot class is a helper class that instantiates the trading strategy. The trading strategy can be implemented in the TradingBot class. This class has access to all REST clients and gets gets via the on_message method all messages that are sent via the subscribed websocket feeds.

This is the starting point from which a strategy can be implemented and applied.

  1#!/usr/bin/env python
  2# -*- coding: utf-8 -*-
  3# Copyright (C) 2023 Benjamin Thomas Schwertfeger
  4# GitHub: https://github.com/btschwertfeger
  5#
  6
  7"""Module that provides an example Futures trading bot data structure."""
  8
  9from __future__ import annotations
 10
 11import asyncio
 12import logging
 13import logging.config
 14import os
 15import sys
 16import traceback
 17from typing import Optional, Union
 18
 19import requests
 20import urllib3
 21
 22from kraken.exceptions import KrakenException
 23from kraken.futures import Funding, KrakenFuturesWSClient, Market, 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().setLevel(logging.INFO)
 31logging.getLogger("requests").setLevel(logging.WARNING)
 32logging.getLogger("urllib3").setLevel(logging.WARNING)
 33
 34
 35class TradingBot(KrakenFuturesWSClient):
 36    """
 37    Class that implements the trading strategy
 38
 39    * The on_message function gets all events from the websocket feed
 40    * Decisions can be made based on these events
 41    * Can place trades using the self.__trade client
 42    * Do everything you want
 43
 44    ====== P A R A M E T E R S ======
 45    config: dict
 46        configuration like: {
 47            'key' 'kraken-futures-key',
 48            'secret': 'kraken-secret-key',
 49            'products': ['PI_XBTUSD']
 50        }
 51    """
 52
 53    def __init__(self: "TradingBot", config: dict) -> None:
 54        super().__init__(
 55            key=config["key"], secret=config["secret"]
 56        )  # initialize the KrakenFuturesWSClient
 57        self.__config: dict = config
 58
 59        self.__user: User = User(key=config["key"], secret=config["secret"])
 60        self.__trade: Trade = Trade(key=config["key"], secret=config["secret"])
 61        self.__market: Market = Market(key=config["key"], secret=config["secret"])
 62        self.__funding: Funding = Funding(key=config["key"], secret=config["secret"])
 63
 64    async def on_message(self: "TradingBot", event: Union[list, dict]) -> None:
 65        """Receives all events that came form the websocket feed(s)"""
 66        logging.info(event)
 67        # ... apply your trading strategy here
 68
 69        # call functions from self.__trade and other clients if conditions met...
 70
 71        # response = self.__trade.create_order(
 72        #     orderType='lmt',
 73        #     size=2,
 74        #     symbol='PI_XBTUSD',
 75        #     side='buy',
 76        #     limitPrice=10000
 77        # )
 78        # ...
 79
 80        # you can also un-/subscribe here using `self.subscribe(...)` or `self.unsubscribe(...)`
 81
 82    # add more functions to customize the trading strategy
 83    # ...
 84    # ...
 85
 86    def save_exit(self: "TradingBot", reason: Optional[str] = "") -> None:
 87        """Controlled shutdown of the strategy"""
 88        logging.warning(f"Save exit triggered, reason: {reason}")
 89        # ideas:
 90        #   * save the bots data
 91        #   * maybe close trades
 92        #   * enable dead man's switch
 93        sys.exit(1)
 94
 95
 96class ManagedBot:
 97    """
 98    Class to manage the trading strategy
 99
100    Subscribes to desired feeds, instantiates the strategy and runs until condition met
101
102    ====== P A R A M E T E R S ======
103    config: dict
104        bot configuration like: {
105            'key' 'kraken-futures-key',
106            'secret': 'kraken-secret-key',
107            'products': ['PI_XBTUSD']
108        }
109    """
110
111    def __init__(self: "ManagedBot", config: dict) -> None:
112        self.__config: dict = config
113        self.__trading_strategy: Optional[TradingBot] = None
114
115    def run(self: "ManagedBot") -> None:
116        """Runner function"""
117        if not self.__check_credentials():
118            sys.exit(1)
119
120        try:
121            asyncio.run(self.__main())
122        except KeyboardInterrupt:
123            pass
124        finally:
125            if self.__trading_strategy is not None:
126                self.__trading_strategy.save_exit(reason="Asyncio loop left")
127
128    async def __main(self: "ManagedBot") -> None:
129        """
130        Instantiates the trading strategy/algorithm and subscribes to the
131        desired websocket feeds. Run the loop while no exception occur.
132
133        Thi variable `exception_occur` which is an attribute of the KrakenFuturesWSClient
134        can be set individually but is also being set to `True` if the websocket connection
135        has some fatal error. This is used to exit the asyncio loop.
136        """
137        self.__trading_strategy = TradingBot(config=self.__config)
138
139        await self.__trading_strategy.subscribe(
140            feed="ticker", products=self.__config["products"]
141        )
142        await self.__trading_strategy.subscribe(
143            feed="book", products=self.__config["products"]
144        )
145
146        await self.__trading_strategy.subscribe(feed="fills")
147        await self.__trading_strategy.subscribe(feed="open_positions")
148        await self.__trading_strategy.subscribe(feed="open_orders")
149        await self.__trading_strategy.subscribe(feed="balances")
150
151        while not self.__trading_strategy.exception_occur:
152            try:
153                # check if bot feels good
154                # maybe send a status update every day
155                # ...
156                pass
157
158            except Exception as exc:
159                message: str = f"Exception in main: {exc} {traceback.format_exc()}"
160                logging.error(message)
161                self.__trading_strategy.save_exit(reason=message)
162
163            await asyncio.sleep(6)
164        self.__trading_strategy.save_exit(
165            reason="Left main loop because of exception in strategy."
166        )
167        return
168
169    def __check_credentials(self: "ManagedBot") -> bool:
170        """Checks the user credentials and the connection to Kraken"""
171        try:
172            User(self.__config["key"], self.__config["secret"]).get_wallets()
173            logging.info("Client credentials are valid")
174            return True
175        except urllib3.exceptions.MaxRetryError:
176            logging.error("MaxRetryError, cannot connect.")
177            return False
178        except requests.exceptions.ConnectionError:
179            logging.error("ConnectionError, Kraken not available.")
180            return False
181        except KrakenException.KrakenAuthenticationError:
182            logging.error("Invalid credentials!")
183            return False
184
185    def save_exit(self: "ManagedBot", reason: str = "") -> None:
186        """Calls the save exit function of the trading strategy"""
187        print(f"Save exit triggered - {reason}")
188        if self.__trading_strategy is not None:
189            self.__trading_strategy.save_exit(reason=reason)
190
191
192def main() -> None:
193    """Main"""
194    bot_config: dict = {
195        "key": os.getenv("FUTURES_API_KEY"),
196        "secret": os.getenv("FUTURES_SECRET_KEY"),
197        "products": ["PI_XBTUSD", "PF_SOLUSD"],
198    }
199    try:
200        managed_bot: ManagedBot = ManagedBot(config=bot_config)
201        managed_bot.run()
202    except Exception:
203        managed_bot.save_exit(
204            reason=f"manageBot.run() has ended: {traceback.format_exc()}"
205        )
206
207
208if __name__ == "__main__":
209    main()