UNPKG

@saturnnetwork/market-maker-strategy

Version:
386 lines (385 loc) 18 kB
"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;