Trading Bot

In this guide, we will construct the trading bot which will be transacting on the QUANTA blockchain. By the end of this guide, you will have the trading bot functioning. The language chosen is javascript, with es6, async.

The core control code will boot up, cancel all existing orders, and execute orders every 8 seconds. In the stock market, typical traders choose the frequency when they'd like to trade such as 1m, 5m, 30m and so on.

Control Logic

class TradeAgent {
constructor(key, spread, levels) {
}
async cancelAll() {
}
async runMarket(market) {
}
async runOnce() {
for (const m of this.quantaMarket) {
console.log("running for market ", m.ticker);
await this.runMarket(m)
}
}
async run() {
const self = this;
await sleep(1000);
await self.cancelAll();
setInterval(async () => {
await self.runOnce();
}, 8000)
}
}
new TradeAgent(process.env.KEY, 0.05, 5).run()

Creating the Client

The client will leverage two pre-built npm libraries which can be found here: quantajs , quantajs-ws

npm i --save @quantadex/bitsharesjs
npm i --save @quantadex/bitsharesjs-ws

The client has the following interface:

class QuantaClient {
constructor(ws_url, onReady) {
}
async setupUser(key) {
}
close() {
Apis.close()
}
async sendLimitOrder(user, base, counter, orders) {
}
async openOrders(user, base) {
}
async cancelOrder(user, orders) {
}
}

Connecting to the websocket

We depend on the @quantadex/bitsharesjs-ws library for websocket connection. QUANTA uses bi-directional communication to send commands, retrieve information, as well as getting pushed messages to the user. In the connection below, we will retrieve all of the assets information so we know how to reference buy orders.

constructor(ws_url, onReady) {
Apis.instance(ws_ur, true, 3000, { enableOrders: true }).init_promise.then((res) => {
console.log("connected to ", ws);
return Apis.instance().db_api().exec("list_assets", ["A", 100]).then((assets) => {
self.assets = lodash.keyBy(assets, "id")
self.assetsBySymbol = lodash.keyBy(assets, "symbol")
if (onReady) {
onReady()
}
});
});
}

Retrieving account information

Often functions require user to reference their userId so here we look up the public key for the account information. In QUANTA blockchain, account system is independent from the key-signature. An account has userid, username, balances, orders, and assigns public key authorized to sign it. If you ever want to change the key, you can retain all the account information.

async setupUser(key) {
const privateKey = PrivateKey.fromWif(key);
const publicKey = privateKey.toPublicKey().toString()
const result = await Apis.instance()
.db_api()
.exec("get_key_references", [[publicKey]]);
return {
privateKey: privateKey,
userId: result[0][0],
publicKey: publicKey
}
}

Send Limit Order

The send limit order takes in a user (from the retrieve account information section), base, counter asset, price, and amount. Orders are always specified from the seller. So we have a conversion function to make this easier for us.

  • If you were buying BTC with USD, then you are selling USD and receiving BTC.

  • If you were selling BTC and receive USD, then you are selling BTC, and receive USD.

async sendLimitOrder(user, base, counter, price, amount) {
const tr = new TransactionBuilder();
const expiration = new Date();
expiration.setYear(expiration.getFullYear() + 5);
const { forSale, toReceive } = this.calculatePrice(is_buy, base, counter, price, amount)
tr.add_type_operation("limit_order_create", {
fee: {
amount: 0,
asset_id: "1.3.0"
},
seller: user.userId,
amount_to_sell: {
amount: forSale.getAmount(),
asset_id: forSale.asset_id
},
min_to_receive: {
amount: toReceive.getAmount(),
asset_id: toReceive.asset_id
},
expiration: expiration,
fill_or_kill: false
});
return await signAndBroadcast(tr, user);
}

Signing and Broadcasting

Signing is straight froward with the user object built earlier, and adding a signature to it, then broadcast to the network.

export function signAndBroadcast(tr, user) {
return tr.set_required_fees().then(() => {
const pKey = user.privateKey;
tr.add_signer(pKey, pKey.toPublicKey().toPublicKeyString());
return tr.broadcast()
.then((res) => {
return res;
})
});
}

Cancel order

Cancel order is similar to limit order, which needs to be signed.

async cancelOrder(user, orders) {
if (orders.length == 0) {
return
}
const fee_asset_id = "1.3.0";
const tr = new TransactionBuilder();
for (var order_id of orders) {
tr.add_type_operation("limit_order_cancel", {
fee: {
amount: 0,
asset_id: fee_asset_id
},
fee_paying_account: user.userId,
order: order_id
});
}
const res = await signAndBroadcast(tr, user)
return res;
}

Putting it together

class TradeAgent {
constructor(key, spread, levels) {
this.quantaClient = new Client(default_ws, key, userId, null);
this.lastOrders= {};
this.quantaMarket = [{ ticker: "QDEX/USD",
price: new ConstantPrice(0.30), clear: false, placed: false },
{ ticker: "ETH/USD",
price: new GdaxPrice("ETH-USD", spread), clear: true, placed: false }
];
}
async cancelAll() {
const orders = await this.quantaClient.openOrders("USD")
for (const o of orders) {
console.log("cancel order ", o.id);
await this.quantaClient.cancelOrder(o.id)
}
}
async runMarket(market) {
if (market.clear) {
const orders = this.lastOrders[market.ticker] || []
for (const o of orders) {
const result = await this.quantaClient.cancelOrder(o.id);
console.log("cancel ",o, result.status);
}
}
const parts = market.ticker.split("/")
if (!market.clear) {
if (!market.placed) {
market.placed = true;
await this.quantaClient.sendLimitOrder(true, parts[0], parts[1], market.price.getBid(), market.price.getAmount());
await this.quantaClient.sendLimitOrder(false, parts[0], parts[1], market.price.getAsk(), market.price.getAmount());
}
} else {
market.price.update()
await this.cancelAll()
await this.quantaClient.sendLimitOrder(true, parts[0], parts[1], market.price.getBid(), market.price.getAmount());
await this.quantaClient.sendLimitOrder(false, parts[0], parts[1], market.price.getAsk(), market.price.getAmount());
}
}
async runOnce() {
for (const m of this.quantaMarket) {
console.log("running for market ", m.ticker);
await this.runMarket(m)
}
}
async run() {
const self = this;
await sleep(1000);
await self.cancelAll();
setInterval(async () => {
await self.runOnce();
}, 8000)
}
}

Source code on Github