@saturnnetwork/market-maker-strategy
Version:
Market Making Strategy for Saturn Network DEX
386 lines (385 loc) • 18 kB
JavaScript
"use strict";
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
function step(result) { result.done ? resolve(result.value) : new P(function (resolve) { resolve(result.value); }).then(fulfilled, rejected); }
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
};
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
const contract_1 = require("ethers/contract");
const bignumber_js_1 = require("bignumber.js");
const lodash_1 = __importDefault(require("lodash"));
const orderbook_1 = require("../charts/orderbook");
const erc20_1 = __importDefault(require("./erc20"));
const EtherAddress = '0x0000000000000000000000000000000000000000';
const PRICEDECIMALS = 6;
class MarketMaker {
constructor(config, owner) {
this.saturn = config.saturn;
this.blockchain = config.blockchain.toUpperCase();
this.token = config.token.toLowerCase();
this.config = config;
this.botAddress = owner;
}
getActions() {
return __awaiter(this, void 0, void 0, function* () {
yield this.ensureValidOrderBook();
yield this.printMarketHealth();
let arbs = yield this.checkArbOpportunity();
if (arbs.length) {
return arbs;
}
let cleanup = yield this.cleanupOrders();
if (cleanup.length) {
return cleanup;
}
let newOrders = yield this.newOrders();
if (newOrders.length) {
return newOrders;
}
console.log(`Market Maker is watching 👀. No actions required at this time.`);
return [];
});
}
newOrders() {
return __awaiter(this, void 0, void 0, function* () {
let spread = yield this.spread();
if (spread.lte(this.config.spread)) {
return [];
}
let buys = yield this.newBuys();
let sells = yield this.newSells();
return buys.concat(sells);
});
}
newBuys() {
return __awaiter(this, void 0, void 0, function* () {
let alreadyInMarket = yield this.etherLockedForAddress(this.botAddress);
let funds = (yield this.availableEther());
if (funds.isLessThanOrEqualTo(this.config.dustCutoff)) {
if (alreadyInMarket.isEqualTo(0)) {
console.log(`
Not enough ${this.blockchain} in the wallet in order to create buy orders.
Please send more ether to ${this.botAddress}
`);
}
return [];
}
let bbp = yield this.bestBuyPrice();
let optimalPrice = (yield this.weightedMidMarketPrice())
.minus((yield this.spread()).dividedBy(new bignumber_js_1.BigNumber(2)));
if (optimalPrice.lte(bbp)) {
return [];
}
let tokenAmount = funds.dividedBy(optimalPrice);
let decimals = yield this.tokenDecimals();
return [{
type: 'NewOrder',
blockchain: this.blockchain,
order_type: 'buy',
amount: tokenAmount.toFixed(decimals),
price: optimalPrice.toFixed(PRICEDECIMALS)
}];
});
}
newSells() {
return __awaiter(this, void 0, void 0, function* () {
let alreadyInMarket = yield this.tokensLockedForAddress(this.botAddress);
let funds = (yield this.availableTokens()).minus(alreadyInMarket);
if (funds.isLessThanOrEqualTo(new bignumber_js_1.BigNumber(0))) {
if (alreadyInMarket.isEqualTo(0)) {
console.log(`
Not enough tokens (${this.token}:${this.blockchain}) in
the wallet in order to create sell orders.
Please send more tokens to ${this.botAddress}
`);
}
return [];
}
let bsp = yield this.bestSellPrice();
let optimalPrice = (yield this.weightedMidMarketPrice())
.plus((yield this.spread()).dividedBy(new bignumber_js_1.BigNumber(2)));
if (optimalPrice.gte(bsp)) {
return [];
}
let decimals = yield this.tokenDecimals();
return [{
type: 'NewOrder',
blockchain: this.blockchain,
order_type: 'sell',
amount: funds.toFixed(decimals),
price: optimalPrice.toFixed(PRICEDECIMALS)
}];
});
}
cleanupOrders() {
return __awaiter(this, void 0, void 0, function* () {
let myorders = yield this.fetchOrdersFor(this.botAddress);
let tocancel = []
.concat(yield this.pruneSells(myorders.sells))
.concat(yield this.pruneBuys(myorders.buys));
return lodash_1.default.map(tocancel, x => {
return {
type: 'CancelOrder',
blockchain: this.blockchain,
contract: x.contract,
order_tx: x.order_tx
};
});
});
}
pruneSells(orders) {
return __awaiter(this, void 0, void 0, function* () {
let dust = lodash_1.default.chain(orders)
.filter(x => { return new bignumber_js_1.BigNumber(x.balance).times(new bignumber_js_1.BigNumber(x.price)).lte(this.config.dustCutoff); })
.map(x => { return { 'contract': x.contract, 'order_tx': x.transaction }; })
.value();
let cutoff = (yield this.weightedMidMarketPrice())
.plus(this.config.bandSize.times(this.config.spread));
let outsiders = lodash_1.default.chain(orders)
.filter(x => { return new bignumber_js_1.BigNumber(x.price).gt(cutoff); })
.map(x => { return { 'contract': x.contract, 'order_tx': x.transaction, 'price': x.price }; })
.value();
if (outsiders.length) {
console.log(`Will attempt to cancel ${this.pluralizedOrders(outsiders)} with price above desired cutoff at ${cutoff} ${this.blockchain}`);
}
if (dust.length) {
console.log(`Will attempt to cancel ${this.pluralizedOrders(dust)} with order balance below dust cutoff of ${this.config.dustCutoff}`);
}
return dust.concat(outsiders);
});
}
pruneBuys(orders) {
return __awaiter(this, void 0, void 0, function* () {
let dust = lodash_1.default.chain(orders)
.filter(x => { return new bignumber_js_1.BigNumber(x.balance).times(new bignumber_js_1.BigNumber(x.price)).lte(this.config.dustCutoff); })
.map(x => { return { 'contract': x.contract, 'order_tx': x.transaction }; })
.value();
let cutoff = (yield this.weightedMidMarketPrice())
.minus(this.config.bandSize.times(this.config.spread));
let outsiders = lodash_1.default.chain(orders)
.filter(x => { return new bignumber_js_1.BigNumber(x.price).lt(cutoff); })
.map(x => { return { 'contract': x.contract, 'order_tx': x.transaction, 'price': x.price }; })
.value();
if (outsiders.length) {
console.log(`Will attempt to cancel ${this.pluralizedOrders(outsiders)} with price below desired cutoff at ${cutoff} ${this.blockchain}`);
}
if (dust.length) {
console.log(`Will attempt to cancel ${this.pluralizedOrders(dust)} with order balance below dust cutoff of ${this.config.dustCutoff}`);
}
return dust.concat(outsiders);
});
}
pluralizedOrders(orders) {
return orders.length === 1 ? `${orders.length} order` : `${orders.length} orders`;
}
checkArbOpportunity() {
return __awaiter(this, void 0, void 0, function* () {
let result = [];
let spread = yield this.spread();
if (spread.lte(new bignumber_js_1.BigNumber(0))) {
console.log(`Arbitrage opportunity detected!`);
let bbo = yield this.bestBuyOrder();
let bso = yield this.bestSellOrder();
let available = yield this.availableTokens();
if (available.isEqualTo(new bignumber_js_1.BigNumber(0))) {
let tokenAmount = bignumber_js_1.BigNumber.min(new bignumber_js_1.BigNumber(bbo.balance), new bignumber_js_1.BigNumber(bso.balance));
let potentialProfit = tokenAmount.times(spread).times(new bignumber_js_1.BigNumber(-1));
console.log(`
Detected opportunity to buy ${tokenAmount} tokens for ${bso.price}
and sell for ${bbo.price} to earn ${potentialProfit} ${this.blockchain},
but unable to execute due to low funds. Please send more tokens
to ${this.botAddress}
`);
return [];
}
let tokenAmount = bignumber_js_1.BigNumber.min(new bignumber_js_1.BigNumber(bbo.balance), new bignumber_js_1.BigNumber(bso.balance), available);
let potentialProfit = tokenAmount.times(spread).times(new bignumber_js_1.BigNumber(-1));
console.log(`
Will attempt to sell ${tokenAmount} tokens for ${bbo.price}
and buy for ${bso.price} to earn ${potentialProfit} ${this.blockchain}
`);
result.push({
type: 'Trade',
blockchain: this.blockchain,
contract: bbo.contract,
order_tx: bbo.transaction,
amount: tokenAmount.toFixed()
});
result.push({
type: 'Trade',
blockchain: this.blockchain,
contract: bso.contract,
order_tx: bso.transaction,
amount: tokenAmount.toFixed()
});
}
return result;
});
}
ensureValidOrderBook() {
return __awaiter(this, void 0, void 0, function* () {
let healthyCutoff = 2;
let ob = yield this.orderBook();
if (ob.buys.length < healthyCutoff || ob.sells.length < healthyCutoff) {
throw new Error(`
The order book for token ${this.blockchain}::${this.token} is too thin
for this bot to properly work. Consider manually creating orders first.
The bot needs at least ${healthyCutoff} buy and sell orders.
`);
}
});
}
printMarketHealth() {
return __awaiter(this, void 0, void 0, function* () {
let spread = yield this.spread();
let wmm = yield this.weightedMidMarketPrice();
let sd = yield this.sellDepth();
let bd = yield this.buyDepth();
let bsp = yield this.bestSellPrice();
let bbp = yield this.bestBuyPrice();
console.log(`Best buy price: ${bbp}`);
console.log(`Best sell price: ${bsp}`);
console.log(`Spread: ${spread}`);
console.log(`Weighted Mid Market Price: ${wmm}`);
console.log(`Buy Depth: ${sd}`);
console.log(`Sell Depth: ${bd}`);
yield this.plotOrderBook();
});
}
plotOrderBook() {
return __awaiter(this, void 0, void 0, function* () {
let ob = yield this.orderBook();
let printer = new orderbook_1.OrderBookPrinter(ob);
printer.print();
});
}
orderBook() {
return __awaiter(this, void 0, void 0, function* () {
return yield this.saturn.query.orderbook(this.token, this.blockchain);
});
}
fetchOrdersFor(address) {
return __awaiter(this, void 0, void 0, function* () {
let allOrders = yield this.saturn.query.ordersForAddress(address);
let buys = lodash_1.default.filter(allOrders, (x) => {
return x.buytoken.address === this.token && x.selltoken.address === EtherAddress;
});
let sells = lodash_1.default.filter(allOrders, (x) => {
return x.selltoken.address === this.token && x.buytoken.address === EtherAddress;
});
return { buys: buys, sells: sells };
});
}
weightedMidMarketPrice() {
return __awaiter(this, void 0, void 0, function* () {
let bestSellPrice = yield this.bestSellPrice();
let bestBuyPrice = yield this.bestBuyPrice();
let sellDepth = yield this.sellDepth();
let buyDepth = yield this.buyDepth();
return bestSellPrice.times(buyDepth)
.plus(bestBuyPrice.times(sellDepth))
.dividedBy(sellDepth.plus(buyDepth));
});
}
bestSellOrder() {
return __awaiter(this, void 0, void 0, function* () {
let tokenInfo = yield this.saturn.query.getTokenInfo(this.token, this.blockchain);
return yield this.saturn.query.getOrderByTx(tokenInfo.best_sell_order_tx, this.blockchain);
});
}
bestBuyOrder() {
return __awaiter(this, void 0, void 0, function* () {
let tokenInfo = yield this.saturn.query.getTokenInfo(this.token, this.blockchain);
return yield this.saturn.query.getOrderByTx(tokenInfo.best_buy_order_tx, this.blockchain);
});
}
bestSellPrice() {
return __awaiter(this, void 0, void 0, function* () {
let bestOrder = yield this.bestSellOrder();
return new bignumber_js_1.BigNumber(bestOrder.price);
});
}
bestBuyPrice() {
return __awaiter(this, void 0, void 0, function* () {
let bestOrder = yield this.bestBuyOrder();
return new bignumber_js_1.BigNumber(bestOrder.price);
});
}
spread() {
return __awaiter(this, void 0, void 0, function* () {
let bsp = yield this.bestSellPrice();
let bbp = yield this.bestBuyPrice();
return bsp.minus(bbp);
});
}
sellDepth() {
return __awaiter(this, void 0, void 0, function* () {
let ob = yield this.orderBook();
return lodash_1.default.reduce(lodash_1.default.map(ob.sells, (order) => {
return new bignumber_js_1.BigNumber(order.balance).times(new bignumber_js_1.BigNumber(order.price));
}), (x, y) => x.plus(y), new bignumber_js_1.BigNumber(0));
});
}
buyDepth() {
return __awaiter(this, void 0, void 0, function* () {
let ob = yield this.orderBook();
return lodash_1.default.reduce(lodash_1.default.map(ob.buys, (order) => {
return new bignumber_js_1.BigNumber(order.balance).times(new bignumber_js_1.BigNumber(order.price));
}), (x, y) => x.plus(y), new bignumber_js_1.BigNumber(0));
});
}
availableEther() {
return __awaiter(this, void 0, void 0, function* () {
let myBalance = (yield this.config.provider.getBalance(this.botAddress)).toString();
let weiMinimum = this.pow(10, 18).times(this.config.fundMinimum);
let ether = new bignumber_js_1.BigNumber(myBalance).minus(weiMinimum).dividedBy(this.pow(10, 18));
return bignumber_js_1.BigNumber.max(0, ether);
});
}
availableTokens() {
return __awaiter(this, void 0, void 0, function* () {
let contract = new contract_1.Contract(this.token, erc20_1.default, this.config.provider);
let tokenbalance = (yield contract.balanceOf(this.botAddress)).valueOf();
let decimals = yield this.tokenDecimals();
return bignumber_js_1.BigNumber.min(new bignumber_js_1.BigNumber(tokenbalance).dividedBy(this.pow(10, decimals)), this.config.tokenLimit);
});
}
etherLockedForAddress(address) {
return __awaiter(this, void 0, void 0, function* () {
let orders = (yield this.fetchOrdersFor(address)).buys;
return lodash_1.default.reduce(lodash_1.default.map(orders, (order) => {
return new bignumber_js_1.BigNumber(order.balance).times(new bignumber_js_1.BigNumber(order.price));
}), (x, y) => x.plus(y), new bignumber_js_1.BigNumber(0));
});
}
tokensLockedForAddress(address) {
return __awaiter(this, void 0, void 0, function* () {
let orders = (yield this.fetchOrdersFor(address)).sells;
return lodash_1.default.reduce(lodash_1.default.map(orders, (order) => {
return new bignumber_js_1.BigNumber(order.balance);
}), (x, y) => x.plus(y), new bignumber_js_1.BigNumber(0));
});
}
tokenDecimals() {
return __awaiter(this, void 0, void 0, function* () {
let contract = new contract_1.Contract(this.token, erc20_1.default, this.config.provider);
let decimals = (yield contract.decimals()).valueOf();
return Number(decimals);
});
}
pow(n, p) {
let multiplier = n instanceof bignumber_js_1.BigNumber ? n : new bignumber_js_1.BigNumber(n);
let out = new bignumber_js_1.BigNumber(1);
for (let i = 0; i < p; ++i) {
out = out.times(multiplier);
}
return out;
}
}
exports.MarketMaker = MarketMaker;