Welcome to DEXBot’s documentation!

Basics

Setup

Requirements – Linux

To run in the background you need systemd and lingering enabled:

sudo loginctl enable-linger $USER

On some systems, such as the Raspberry Pi, you need to reboot for this to take effect.

You need to have python3 installed, including the pip tool, and the development tools for C extensions, and the OpenSSL libraries.

Plus for the easy configuration you need the whiptail command.

On Ubuntu/Debian/Raspian

Do:

sudo apt-get update
sudo apt-get install -y --install-recommends gcc libssl-dev python3-pip python3-dev whiptail inetutils-ping

On some Ubuntu systems, it will complain about missing packages: you first need to make sure you have the universe repository:

sudo apt-get install -y software-properties-common
sudo add-apt-repository universe

NOTE: you don’t need to upgrade the system: the issue here is about the range of packages available, not how new/old they are.

Fedora

This has been tested on Fedora 27:

sudo yum install -y gcc openssl-devel python3-pip python3-devel newt
Arch

As root, do:

pacman -S libnewt python-pip gcc
Other Distros

On other distros you need to check the documentation for how to install these packages, the names should be very similar.

Installation

::
sudo -H pip3 install https://github.com/Codaone/DEXBot/archive/master.zip

If you want the latest development version (which may not be tested at all), use git to download:

git clone git://github.com/ihaywood3/DEXBot/
cd DEXBot
sudo -H pip3 install -e .

Do not use the --user flag unless you understand its implications.

pip3 may complain about wanting to upgrade: this can be ignored.

Adding Keys

It is important to install the private key of your bot’s account into a local wallet. This can be done using uptick which is installed as a dependency of dexbot:

uptick addkey

uptick will ask you for a passphrase to protect private keys stored in its wallet. This has no relation to any passphrase used in the web wallet.

You can get your private key from the BitShares Web Wallet: click the menu on the top right, then “Settings”, “Accounts”, “View keys”, then tab “Owner Permissions”, click on the public key, then “Show”.

Look for the private key in Wallet Import Format (WIF), it’s a “5” followed by a long list of letters. Select, copy and paste this into the screen where uptick asks for the key.

Check uptick successfully imported the key with:

uptick listaccounts

Yes, this process is a pain but for security reasons this part probably won’t ever be “easy”.

Configuration

dexbot can be configured using:

dexbot-cli configure

This will walk you through the configuration process. Read more about this in the Configuration Questions.

Configuration Questions

The configuration consists of a series of questions about the bots you wish to configure.

  1. The Bot Name.

    Choose a unique name for your bot, DEXBot doesn’t care what you call it. It is used to identify the bot in the logs so should be fairly short.

  2. The Bot Strategy

    DEXBot provides a number of different bot strategies. They can be quite different in how they behave (i.e. spend your money) so it is important you understand the strategy before deploying a bot.

    1. Simple Echo Strategy For testing this just logs events on a market, does no trading.
    2. Follow Orders Strategy My (Ian Haywood) main bot, an extension of stakemachine’s wall, it has been used to provide liquidity on AUD:BTS. Does function but by no mean perfect, see caveats in the docs.
  3. Strategy-specific questions

    The questions that follow are determined by the strategy chosen, and each strategy will have its own questions around amounts to trade, spreads etc. See the strategy documentations linked above. But the first two strategy questions are nearly universal amongst the strategies so are documented here:

    1. The Account.

      This is the same account name as the one where you entered the keys into uptick earlier on: the bot must always have the private key so it can execute trades.

    2. The Market.

      This is the main market the bot trade on. They are specified by the quote asset, a colon (:), and the base asset, for example the market for BitShares priced in US dollars is called BTS:USD. BitShares always provides a “reverse” market so there will be a USD:BTS with the same trades, the only difference is the prices will be the inverse (1/x) of BTS:USD.

  4. the Node.

    DEXBot needs to have a public node (also called “witness”) that gives access to the BitShares blockchain.

    DEXBot uses wss://status200.bitshares.apasia.tech/ws as its default node If you run your own witness node then you can edit config.yml to change the node value.

  5. Reporting

    DEXBot can send regular reports via e-mail of its activities. See DEXBot E-mail Reports

  6. Systemd.

    If the configuration tool detects systemd (the process control system on most modern Linux systems) it will offer to install dexbot as a background service, this will run continuously in the background whenever you are logged in. if you enabled lingering as described, it will run whenever the computer is turned on.

  7. The Passphrase

    If you select yes above, the final question will be the password you entered to protect the private key with uptick. Entering it here is a security risk: the configuration tool will save the password to a file on the computer. This means anyone with access to the computer can access your private key and spend the money in your account.

    There is no alternative to enable 24/7 bot trading without you being physically present to enter the password every time the bot wnats to execute a trade (which defeats the purpose of using a bot). It does mean you need to think carefully where dexbot is installed: my advice is on the computer in a secure location that you control behind a properly- configured firewall/router.

Manual Running

If you are not using systemd, the bot can be run manually by:

dexbot-cli run

It will ask for your wallet passphrase (that you provided when adding your private key using uptick addkey).

DEXBot E-mail Reports

DEXBot can send e-mail reports at regular intervals when its running in the background.

Configuration Questions

  1. Report frequency.

    You get several options of number of days up to a week. If you want a different timeframe look in config.yml for the days option under reporter, can be any integer number of days. If you select “Never” then no reports are sent at all.

  2. Send to address

    The address to send reports to, must be in the traditional username@server format.

  3. Send from address

    The address the DEXBot e-mails will appear to be from. By default DEXBot uses the name of the user it’s running as, and the name of the server it’s running on. (Note this default may not work depending on your setup, a lot of e-mail servers will check the sending server name is valid from its point of view). use the same e-mail as “send to” above if you are unsure.

  4. SMTP Server.

    The hostname of the e-mail server to use. Blank means the local server (so this has to be setup).

  5. SMTP Port.

    Traditionally this is always “25”. Some public e-mail setups (such as Gmail) require you to use the “submission” port (587): check the documentation of the e-mail service you are trying to use.

  6. Login

    Use if the SMTP server requires a login name (most public ones do, but an ISP-provided or local network one may not), otherwise leave blank.

  7. Password

    If you need to provide a login then a password is usually required too.

Reports

Reports have the subject “DEXBot Regular Report” and are HTML e-mails with a section for each bot, in each section the configuration values are quoted, then a graph is supplied by the bot.

Mst trading bots will provide a graph of the base and quote account balances and the total value (hopefully going up). These graph lines are all in the “quote” unit, using the price at the end of the reporting period (so hopefully factoring out shifts in capital value and you can actually see the effect of the bots trading).

Finally the log entries for each bot over the reporting period are supplied.

Strategies

Simple Echo Strategy

API

Full Source Code

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
from dexbot.basestrategy import BaseStrategy


class Strategy(BaseStrategy):
    """
    Echo strategy
    Strategy that logs all events within the blockchain
    """

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)

        """ set call backs for events
        """
        self.onOrderMatched += self.print_orderMatched
        self.onOrderPlaced += self.print_orderPlaced
        self.onUpdateCallOrder += self.print_UpdateCallOrder
        self.onMarketUpdate += self.print_marketUpdate
        self.ontick += self.print_newBlock
        self.onAccount += self.print_accountUpdate
        self.error_ontick = self.error
        self.error_onMarketUpdate = self.error
        self.error_onAccount = self.error

    def error(self, *args, **kwargs):
        """ What to do on an error
        """
        # Cancel all future execution
        self.disabled = True

    def print_orderMatched(self, i):
        """ Is called when an order in the market is matched

            A developer may want to filter those to identify
            own orders.

            :param bitshares.price.FilledOrder i: Filled order details
        """
        self.log.info("order matched: %s" % i)

    def print_orderPlaced(self, i):
        """ Is called when a new order in the market is placed

            A developer may want to filter those to identify
            own orders.

            :param bitshares.price.Order i: Order details
        """
        self.log.info("order placed:  %s" % i)

    def print_UpdateCallOrder(self, i):
        """ Is called when a call order for a market pegged asset is updated

            A developer may want to filter those to identify
            own orders.

            :param bitshares.price.CallOrder i: Call order details
        """
        self.log.info("call update:   %s" % i)

    def print_marketUpdate(self, i):
        """ Is called when Something happens in your market.

            This method is actually called by the backend and is
            dispatched to ``onOrderMatched``, ``onOrderPlaced`` and
            ``onUpdateCallOrder``.

            :param object i: Can be instance of ``FilledOrder``, ``Order``, or ``CallOrder``
        """
        self.log.info("marketupdate:  %s" % i)

    def print_newBlock(self, i):
        """ Is called when a block is received

            :param str i: The hash of the block

            .. note:: Unfortunately, it is currently not possible to
                      identify the block number for ``i`` alone. If you
                      need to know the most recent block number, you
                      need to use ``bitshares.blockchain.Blockchain``
        """
        self.log.info("new1 block:     %s" % i)
        # raise ValueError("Testing disabling")

    def print_accountUpdate(self, i):
        """ This method is called when the worker's account name receives
            any update. This includes anything that changes
            ``2.6.xxxx``, e.g., any operation that affects your account.
        """
        self.log.info("account:       %s" % i)

Follow Orders Strategy

This strategy places a buy and a sell walls into a specific market. It buys below a base price, and sells above the base price.

It then reacts to orders filled (hence the name), when one order is completely filled, new walls are constructed using the filled order as the new base price.

Configuration Questions

Spread

Percentage difference between buy and sell price. So a spread of 5% means the bot sells at 2.5% above the base price and buys 2.5% below.

Wall Percent

This is the “wall height”: the default amount to buy/sell, expressed as a percentage of the account balance. So for sells, its that percentage of your balance of the ‘quote’ asset, and for buys its that same percentage of your balance of the ‘base’ asset.

The advantage of this method (a change from early versions with ‘fixed’ wall heights) is it makes the bot ‘self-correcting’ if it sells to much of one asset, it will enter small orders for that asset, until the market gives it and opposing trade and it can rebalance itself.

Remember if you are using ‘staggers’ (see below) each order is the same: so you probably need to use quite a small percentage here.

Max

The bot will stop trading if the base price goes above this value, it will shut down until you manually restart it. (i.e. it won’t restart itself if the price drops)

Remember prices are base/quote, so on AUD:BTS, that’s the price of 1 AUD in BTS.

Min

Same in reverse: stop running if prices go below this value.

Start

The initial price, as a percentage of the spread between the highest bid and lowest ask. So “0” means the highest bid, “100” means the lowest ask, 50 means the midpoint between them, and so on.

Important: you need to look at your chosen market carefully and get a sense of its orderbook before setting this, justing setting “50” blind can lead to stupid prices especially in illiquid markets.

Reset

Normally the bot checks if it has previously placed orders in the market and uses those. If true, this option forces the bot to cancel any existing orders when it starts up, and re-calculate the starting price as above.

Staggers

Number of additional (or “staggered”) orders to place. By default this is “1” (so no additional orders). 2 or more means multiple sell and buy orders at different prices.

Stagger Spread

The gap between each staggered order (as a percentage of the base price).

So say the spread is 5%, staggers is “2”, and “stagger spread” is 4%, then there will be 2 buy orders: 2.5% and 6.5% (4% + 2.5%) below the base price, and two sells 2.5% and 6.5% above. The order amounts are all the same (see wall percent option).

Bot problems

Like all liquidity bots, the bot really works best with a “even” market, i.e. where are are counterparties both buying and selling.

With a severe “bear” market, the bot can be stuck being the sole participant that is still buying against a herd of panicked humans frantically selling. It repeatingly buys into the market and so it can run out of one asset quite quickly (although it will have bought it at lower and lower prices).

I don’t have a simple good solution (and happy to hear suggestions from experienced traders)

Developing own Strategies

Manual Configuration

The configuration of dexbot internally happens through a YAML formatted file. Unless you are developing or want to use a custom strategy, you don’t need to edit this.

The default file name is config.yml, and dexbot only seeks the file in the current directory.

Otherwise you can specify a different config file using the --configfile X parameter when calling dexbot run.

The config.yml file

# The BitShares endpoint to talk to
node: "wss://node.testnet.bitshares.eu"

# List of bots
bots:

    # Name of the bot. This is mostly for logging and internal
    # use to distinguish different bots
    NAME_OF_BOT:

        # Python module to look for the strategy (can be custom)
        # dexbot will search in ~/bots as well as standard dirs
        module: "dexbot.strategies.echo"

        # The bot class in that module to use
        bot: Echo

        # The market to subscribe to
        market: GOLD:TEST

        # The account to use for this bot
        account: xeroc

        # Custom bot configuration
        foo: bar

Using the configuration in custom strategies

The bot’s configuration is available to in each strategy as dictionary in self.bot. The whole configuration is avaialable in self.config. The name of your bot can be found in self.name.

Base Strategy

All strategies should inherit dexbot.basestrategy.BaseStrategy which simplifies and unifies the development of new strategies.

API

Storage

This class allows to permanently store bot-specific data in a sqlite database (dexbot.sqlite) using:

self["key"] = "value"

Note

Here, self refers to the instance of your bot’s strategy when coding yaour own strategy.

The value is persistently stored and can be access later on using:

print(self["key"]).

Note

This applies a json.loads(json.dumps(value))!

SQLite database

The user’s data is stored in its OS protected user directory:

OSX:

  • ~/Library/Application Support/<AppName>

Windows:

  • C:Documents and Settings<User>Application DataLocal Settings<AppAuthor><AppName>
  • C:Documents and Settings<User>Application Data<AppAuthor><AppName>

Linux:

  • ~/.local/share/<AppName>

Where <AppName> is dexbot and <AppAuthor> is ChainSquad GmbH.

Simple example

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
from dexbot.basestrategy import BaseStrategy


class Strategy(BaseStrategy):
    """
    Storage demo strategy
    Strategy that prints all new blocks in the blockchain
    """

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.ontick += self.tick

    def tick(self, i):
        print("previous block: %s" % self["block"])
        print("new block: %s" % i)
        self["block"] = i

Example Output:

Current Wallet Passphrase:
previous block: None
new block: 008c4c2424e6394ad4bf5a9756ae2ee883b0e049
previous block: 008c4c2424e6394ad4bf5a9756ae2ee883b0e049
new block: 008c4c257a76671144fdba251e4ebbe61e4593a4
previous block: 008c4c257a76671144fdba251e4ebbe61e4593a4
new block: 008c4c2617851b31d0b872e32fbff6f8248663a3

Statemachine

The base strategy comes with a state machine that can be used by your strategy.

Similar to Storage, the methods of this class can be used in your strategy directly, e.g., via self.get_state(), since the class is inherited by Base Strategy.

API

class dexbot.statemachine.StateMachine(*args, **kwargs)

Generic state machine

add_state(state)

Add a new state to the state machine

Parameters:state (str) – Name of the state
get_state()

Return state of state machine

set_state(state)

Change state of the state machine

Parameters:state (str) – Name of the new state

Events

The websocket endpoint of BitShares has notifications that are subscribed to and dispatched by dexbot. This uses python’s native Events. The following events are available in your strategies and depend on the configuration of your bot/strategy:

  • onOrderMatched: Called when orders in your market are matched
  • onOrderPlaced: Called when a new order in your market is placed
  • onUpdateCallOrder: Called if one of the assets in your market is a market-pegged asset and someone updates his call position
  • onMarketUpdate: Called whenever something happens in your market (includes matched orders, placed orders and call order updates!)
  • ontick: Called when a new block is received
  • onAccount: Called when your account’s statistics is updated (changes to 2.6.xxxx with xxxx being your account id number)
  • error_ontick: Is called when an error happend when processing ontick
  • error_onMarketUpdate: Is called when an error happend when processing onMarketUpdate
  • error_onAccount: Is called when an error happend when processing onAccount

Simple Example

class Simple(BaseStrategy):
    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)

        """ set call backs for events
        """
        self.onOrderMatched += print
        self.onOrderPlaced += print
        self.onUpdateCallOrder += print
        self.onMarketUpdate += print
        self.ontick += print
        self.onAccount += print

Wall Strategy

This strategy simply places a buy and a sell wall into a specific market using a specified account.

Example Configuration

# BitShares end point
node: "wss://node.bitshares.eu"

# List of Bots
bots:

        # Only a single Walls Bot
        Walls:

             # The Walls strategy module and class
             module: dexbot.strategies.walls
             bot: Walls

             # The market to serve
             market: HERO:BTS

             # The account to sue
             account: hero-market-maker

             # We shall bundle operations into a single transaction
             bundle: True

             # Test your conditions every x blocks
             test:
                     blocks: 10

             # Where the walls should be
             target:

                     # They relate to the price feed
                     reference: feed

                     # There should be an offset
                     offsets:
                         buy: 2.5
                         sell: 2.5

                     # We'd like to use x amount of quote (here: HERO)
                     # in the walls
                     amount:
                         buy: 5.0
                         sell: 5.0

             # When the price moves by more than 2%, update the walls
             threshold: 2

Source Code

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
from math import fabs
from collections import Counter
from bitshares.amount import Amount
from dexbot.basestrategy import BaseStrategy, ConfigElement
from dexbot.errors import InsufficientFundsError


class Strategy(BaseStrategy):
    """
    Walls strategy
    """

    @classmethod
    def configure(cls):

        return BaseStrategy.configure() + [
            ConfigElement(
                "spread",
                "int",
                5,
                "the spread between sell and buy as percentage",
                (0,
                 100)),
            ConfigElement(
                "threshold",
                "int",
                5,
                "percentage the feed has to move before we change orders",
                (0,
                 100)),
            ConfigElement(
                "buy", "float", 0.0, "the default amount to buy", (0.0, None)),
            ConfigElement("sell", "float", 0.0,
                          "the default amount to sell", (0.0, None)),
            ConfigElement(
                "blocks",
                "int",
                20,
                "number of blocks to wait before re-calculating",
                (0,
                 10000)),
            ConfigElement(
                "dry_run",
                "bool",
                False,
                "Dry Run Mode\nIf Yes the bot won't buy or sell anything, just log what it would do.\nIf No, the bot will buy and sell for real.",
                None)
        ]

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)

        # Define Callbacks
        self.onMarketUpdate += self.test
        self.ontick += self.tick
        self.onAccount += self.test

        self.error_ontick = self.error
        self.error_onMarketUpdate = self.error
        self.error_onAccount = self.error

        # Counter for blocks
        self.counter = Counter()

        # Tests for actions
        self.test_blocks = self.worker.get("test", {}).get("blocks", 0)

    def error(self, *args, **kwargs):
        self.disabled = True
        self.cancelall()
        self.log.info(self.execute())

    def updateorders(self):
        """ Update the orders
        """
        self.log.info("Replacing orders")

        # Canceling orders
        self.cancelall()

        # Target
        target = self.worker.get("target", {})
        price = self.getprice()

        # prices
        buy_price = price * (1 - target["offsets"]["buy"] / 100)
        sell_price = price * (1 + target["offsets"]["sell"] / 100)

        # Store price in storage for later use
        self["feed_price"] = float(price)

        # Buy Side
        if float(self.balance(self.market["base"])
                 ) < buy_price * target["amount"]["buy"]:
            InsufficientFundsError(
                Amount(
                    target["amount"]["buy"] *
                    float(buy_price),
                    self.market["base"]))
            self["insufficient_buy"] = True
        else:
            self["insufficient_buy"] = False
            self.market.buy(
                buy_price,
                Amount(target["amount"]["buy"], self.market["quote"]),
                account=self.account
            )

        # Sell Side
        if float(self.balance(self.market["quote"])
                 ) < target["amount"]["sell"]:
            InsufficientFundsError(
                Amount(
                    target["amount"]["sell"],
                    self.market["quote"]))
            self["insufficient_sell"] = True
        else:
            self["insufficient_sell"] = False
            self.market.sell(
                sell_price,
                Amount(target["amount"]["sell"], self.market["quote"]),
                account=self.account
            )

        self.log.info(self.execute())

    def getprice(self):
        """ Here we obtain the price for the quote and make sure it has
            a feed price
        """
        target = self.worker.get("target", {})
        if target.get("reference") == "feed":
            assert self.market == self.market.core_quote_market(
            ), "Wrong market for 'feed' reference!"
            ticker = self.market.ticker()
            price = ticker.get("quoteSettlement_price")
            assert abs(price["price"]) != float(
                "inf"), "Check price feed of asset! (%s)" % str(price)
        return price

    def tick(self, d):
        """ ticks come in on every block
        """
        if self.test_blocks:
            if not (self.counter["blocks"] or 0) % self.test_blocks:
                self.test()
            self.counter["blocks"] += 1

    def test(self, *args, **kwargs):
        """ Tests if the orders need updating
        """
        orders = self.orders

        # Test if still 2 orders in the market (the walls)
        if len(orders) < 2 and len(orders) > 0:
            if (
                not self["insufficient_buy"] and
                not self["insufficient_sell"]
            ):
                self.log.info("No 2 orders available. Updating orders!")
                self.updateorders()
        elif len(orders) == 0:
            self.updateorders()

        # Test if price feed has moved more than the threshold
        if (
            self["feed_price"] and
            fabs(1 - float(self.getprice()) / self["feed_price"]) > self.worker["threshold"] / 100.0
        ):
            self.log.info(
                "Price feed moved by more than the threshold. Updating orders!")
            self.updateorders()

Indices and tables