first commit
This commit is contained in:
commit
aa7869eb33
386 files changed
+69375
No files matched your search
@@ -0,0 +1,754 @@
|
||||
/*
|
||||
The sticky order is an advanced order:
|
||||
- It is created at a limit price of X
|
||||
- if limit is not specified always at bbo.
|
||||
- if limit is specified the price is either limit or the bbo (whichever is more favorable)
|
||||
- it will readjust the order:
|
||||
- if outbid is true it will outbid the current bbo (on supported exchanges)
|
||||
- if outbid is false it will stick to current bbo when this moves
|
||||
- If the price moves away from the order it will "stick to" the top
|
||||
|
||||
TODO:
|
||||
- native move
|
||||
*/
|
||||
|
||||
const _ = require('lodash');
|
||||
const async = require('async');
|
||||
const events = require('events');
|
||||
const moment = require('moment');
|
||||
const errors = require('../exchangeErrors');
|
||||
const BaseOrder = require('./order');
|
||||
const states = require('./states');
|
||||
|
||||
class StickyOrder extends BaseOrder {
|
||||
constructor({api, marketConfig, capabilities}) {
|
||||
super(api);
|
||||
|
||||
this.market = marketConfig;
|
||||
this.capabilities = capabilities;
|
||||
|
||||
// global async lock
|
||||
this.sticking = false;
|
||||
|
||||
// bound helpers
|
||||
this.roundPrice = this.api.roundPrice.bind(this.api);
|
||||
this.roundAmount = this.api.roundAmount.bind(this.api);
|
||||
if(_.isFunction(this.api.outbidPrice)) {
|
||||
this.outbidPrice = this.api.outbidPrice.bind(this.api);
|
||||
}
|
||||
|
||||
this.debug = this.api.name === 'Deribit2';
|
||||
this.log = m => this.debug && console.log(new Date, m);
|
||||
}
|
||||
|
||||
create(side, rawAmount, params = {}) {
|
||||
if(this.completed || this.completing) {
|
||||
return false;
|
||||
}
|
||||
|
||||
console.log(new Date, 'sticky create', side);
|
||||
|
||||
this.side = side;
|
||||
|
||||
this.amount = this.roundAmount(rawAmount);
|
||||
|
||||
this.initialLimit = params.initialLimit;
|
||||
|
||||
if(side === 'buy') {
|
||||
if(params.limit) {
|
||||
this.limit = this.roundPrice(params.limit);
|
||||
} else {
|
||||
this.noLimit = true;
|
||||
this.limit = Infinity;
|
||||
}
|
||||
} else {
|
||||
if(params.limit) {
|
||||
this.limit = this.roundPrice(params.limit);
|
||||
} else {
|
||||
this.noLimit = true;
|
||||
this.limit = -Infinity;
|
||||
}
|
||||
}
|
||||
|
||||
this.status = states.SUBMITTED;
|
||||
this.emitStatus();
|
||||
|
||||
this.orders = {};
|
||||
|
||||
this.outbid = params.outbid && _.isFunction(this.outbidPrice);
|
||||
|
||||
if(this.data && this.data.ticker) {
|
||||
this.price = this.calculatePrice(this.data.ticker);
|
||||
this.createOrder();
|
||||
} else {
|
||||
this.api.getTicker((err, ticker) => {
|
||||
if(this.handleError(err)) {
|
||||
console.log(new Date, 'error get ticker');
|
||||
return;
|
||||
}
|
||||
|
||||
this.price = this.calculatePrice(ticker);
|
||||
this.createOrder();
|
||||
});
|
||||
}
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
calculatePrice(ticker) {
|
||||
|
||||
const r = this.roundPrice;
|
||||
|
||||
if(this.initialLimit && !this.id) {
|
||||
console.log('passing initial limit of:', this.limit);
|
||||
return r(this.limit);
|
||||
}
|
||||
|
||||
if(this.side === 'buy') {
|
||||
|
||||
if(!this.noLimit && ticker.bid >= this.limit) {
|
||||
return r(this.limit);
|
||||
}
|
||||
|
||||
if(!this.outbid) {
|
||||
return r(ticker.bid);
|
||||
}
|
||||
|
||||
const outbidPrice = this.outbidPrice(ticker.bid, true);
|
||||
|
||||
if(outbidPrice <= this.limit && outbidPrice < ticker.ask) {
|
||||
return r(outbidPrice);
|
||||
} else {
|
||||
return r(this.limit);
|
||||
}
|
||||
|
||||
} else if(this.side === 'sell') {
|
||||
|
||||
if(!this.noLimit && ticker.ask <= this.limit) {
|
||||
return r(this.limit);
|
||||
}
|
||||
|
||||
if(!this.outbid) {
|
||||
return r(ticker.ask);
|
||||
}
|
||||
|
||||
const outbidPrice = this.outbidPrice(ticker.ask, false);
|
||||
|
||||
if(outbidPrice >= this.limit && outbidPrice > ticker.bid) {
|
||||
return r(outbidPrice);
|
||||
} else {
|
||||
return r(this.limit);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
createOrder() {
|
||||
if(this.completed || this.completing) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const alreadyFilled = this.calculateFilled();
|
||||
|
||||
this.submit({
|
||||
side: this.side,
|
||||
amount: this.roundAmount(this.amount - alreadyFilled),
|
||||
price: this.price,
|
||||
alreadyFilled
|
||||
});
|
||||
}
|
||||
|
||||
// check if the last order was partially filled
|
||||
// on an exchange that does not pass fill data on cancel
|
||||
// see https://github.com/askmike/gekko/pull/2450
|
||||
handleInsufficientFundsError(err) {
|
||||
if(
|
||||
!err ||
|
||||
err.type !== 'insufficientFunds' ||
|
||||
!this.capabilities.limitedCancelConfirmation ||
|
||||
!this.id
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const id = this.id;
|
||||
|
||||
setTimeout(
|
||||
() => {
|
||||
this.api.getOrder(id, (innerError, res) => {
|
||||
if(this.handleError(innerError)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const amount = res.amount;
|
||||
|
||||
if(this.orders[id].filled === amount) {
|
||||
// handle original error
|
||||
return this.handleError(err);
|
||||
}
|
||||
|
||||
this.orders[id].filled = amount;
|
||||
this.emit('fill', this.calculateFilled());
|
||||
if(this.calculateFilled() >= this.amount) {
|
||||
return this.filled(this.price);
|
||||
}
|
||||
|
||||
setTimeout(this.createOrder, this.checkInterval);
|
||||
});
|
||||
},
|
||||
this.checkInterval
|
||||
);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
handleCreate(err, id) {
|
||||
|
||||
if(this.handleInsufficientFundsError(err)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if(this.handleError(err)) {
|
||||
console.log(new Date, 'handleCreate error');
|
||||
return;
|
||||
}
|
||||
|
||||
if(!id) {
|
||||
console.log('BLUP! no id...');
|
||||
}
|
||||
|
||||
// potentailly clean up old order
|
||||
if(
|
||||
this.id &&
|
||||
this.orders[this.id] &&
|
||||
this.orders[this.id].filled === 0
|
||||
)
|
||||
delete this.orders[this.id];
|
||||
|
||||
// register new order
|
||||
this.log(`${this.side} old id: ${this.id} new id: ${id}`);
|
||||
this.id = id;
|
||||
|
||||
this.orders[id] = {
|
||||
price: this.price,
|
||||
filled: 0
|
||||
}
|
||||
|
||||
this.emit('new order', this.id);
|
||||
this.emit('movelimit handled', new Date);
|
||||
|
||||
this.status = states.OPEN;
|
||||
this.emitStatus();
|
||||
|
||||
this.scheduleNextCheck();
|
||||
}
|
||||
|
||||
initiateDefferedAction() {
|
||||
// check whether we had an action pending
|
||||
if(this.cancelling) {
|
||||
this.cancel();
|
||||
return true;
|
||||
}
|
||||
|
||||
if(this.movingLimit && this.moveLimit()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if(this.movingAmount) {
|
||||
this.moveAmount();
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
scheduleNextCheck() {
|
||||
|
||||
// remove lock
|
||||
this.sticking = false;
|
||||
|
||||
if(this.initiateDefferedAction()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// register check
|
||||
this.timeout = setTimeout(this.checkOrder, this.checkInterval);
|
||||
|
||||
}
|
||||
|
||||
checkOrder() {
|
||||
|
||||
if(this.completed || this.completing) {
|
||||
return console.log(new Date, this.side, 'checkOrder called on completed/completing order..', this.completed, this.completing);
|
||||
}
|
||||
|
||||
if(this.status === states.MOVING) {
|
||||
return console.log(new Date, this.side, 'refusing to check, in the middle of move');
|
||||
}
|
||||
|
||||
if(this.initiateDefferedAction()) {
|
||||
console.log(new Date, this.side, 'skipping check logic, better things to do - 0');
|
||||
return;
|
||||
}
|
||||
|
||||
this.sticking = true;
|
||||
const checkId = this.id;
|
||||
|
||||
this.api.checkOrder(this.id, (err, result) => {
|
||||
|
||||
if(!this.debug) {
|
||||
if(this.ignoreCheckResult) {
|
||||
this.log(this.side + ' debug ignoring check result');
|
||||
this.ignoreCheckResult = false;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if(this.status === states.MOVING) {
|
||||
this.log(`${this.side} ${this.id} debug ignoring check result - in the middle of move`);
|
||||
this.ignoreCheckResult = false;
|
||||
return;
|
||||
}
|
||||
|
||||
if(checkId !== this.id) {
|
||||
this.log(this.side + ' debug got check on old id ' + checkId + ', ' + this.id);
|
||||
this.ignoreCheckResult = false;
|
||||
return;
|
||||
}
|
||||
|
||||
this.status = states.CHECKED;
|
||||
this.emitStatus();
|
||||
|
||||
if(this.handleError(err)) {
|
||||
console.log(new Date, 'checkOrder error');
|
||||
return;
|
||||
}
|
||||
|
||||
if(result.open) {
|
||||
if(result.filledAmount !== this.orders[this.id].filled) {
|
||||
this.orders[this.id].filled = result.filledAmount;
|
||||
this.emit('fill', this.calculateFilled());
|
||||
}
|
||||
|
||||
// if we are already at limit we dont care where the top is
|
||||
// note: might be string VS float
|
||||
if(this.price == this.limit) {
|
||||
this.scheduleNextCheck();
|
||||
return;
|
||||
}
|
||||
|
||||
if(this.initiateDefferedAction()) {
|
||||
console.log(new Date, this.side, 'skipping check ticker logic, better things to do 1');
|
||||
return;
|
||||
}
|
||||
|
||||
this.api.getTicker((err, ticker) => {
|
||||
if(this.handleError(err)) {
|
||||
console.log(new Date, 'getTicker error');
|
||||
return;
|
||||
}
|
||||
|
||||
if(this.initiateDefferedAction()) {
|
||||
console.log(new Date, this.side, 'skipping check ticker logic, better things to do 2');
|
||||
return;
|
||||
}
|
||||
|
||||
if(this.status === states.MOVING) {
|
||||
this.log(`${this.side} ${this.id} debug ignoring check result - in the middle of move ---- 2`);
|
||||
this.ignoreCheckResult = false;
|
||||
return;
|
||||
}
|
||||
|
||||
this.ticker = ticker;
|
||||
this.emit('ticker', ticker);
|
||||
|
||||
const bookSide = this.side === 'buy' ? 'bid' : 'ask';
|
||||
// note: might be string VS float
|
||||
if(ticker[bookSide] != this.price) {
|
||||
return this.move(this.calculatePrice(ticker));
|
||||
} else {
|
||||
this.scheduleNextCheck();
|
||||
}
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// it's not open right now
|
||||
// meaning we are done
|
||||
this.sticking = false;
|
||||
|
||||
if(!result.executed) {
|
||||
// not open and not executed means it never hit the book
|
||||
console.log(this.side, this.status, this.id, 'not open not executed!', result);
|
||||
this.rejected();
|
||||
throw 'a';
|
||||
return;
|
||||
}
|
||||
|
||||
// order got filled!
|
||||
this.orders[this.id].filled = this.amount;
|
||||
this.emit('fill', this.amount);
|
||||
this.filled(this.price);
|
||||
});
|
||||
|
||||
|
||||
this.status = states.CHECKING;
|
||||
this.emitStatus();
|
||||
}
|
||||
|
||||
// global error handler
|
||||
handleError(error) {
|
||||
if(!error) {
|
||||
return false;
|
||||
}
|
||||
|
||||
console.log(new Date, '[sticky order] FATAL ERROR', error.message);
|
||||
console.log(new Date, error);
|
||||
this.status = states.ERROR;
|
||||
this.emitStatus();
|
||||
this.error = error;
|
||||
|
||||
this.emit('error', error);
|
||||
return true;
|
||||
}
|
||||
|
||||
// returns true if the order was fully filled
|
||||
// handles partial fills on cancels calls
|
||||
// on exchanges that support it.
|
||||
handleCancel(filled, data) {
|
||||
// it got filled before we could cancel
|
||||
if(filled) {
|
||||
this.orders[this.id].filled = this.amount;
|
||||
this.emit('fill', this.amount);
|
||||
this.filled(this.price);
|
||||
return;
|
||||
}
|
||||
|
||||
// if we have data on partial fills
|
||||
// check whether we had a partial fill
|
||||
if(_.isObject(data)) {
|
||||
let amountFilled = data.filled;
|
||||
|
||||
if(!amountFilled && data.remaining) {
|
||||
const alreadyFilled = this.calculateFilled();
|
||||
const orderAmount = this.roundAmount(this.amount - alreadyFilled);
|
||||
amountFilled = this.roundAmount(orderAmount - data.remaining);
|
||||
}
|
||||
|
||||
if(amountFilled > this.orders[this.id].filled) {
|
||||
this.orders[this.id].filled = amountFilled;
|
||||
this.emit('fill', this.calculateFilled());
|
||||
}
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
move(price) {
|
||||
if(this.completed || this.completing) {
|
||||
return false;
|
||||
}
|
||||
|
||||
this.status = states.MOVING;
|
||||
this.emitStatus();
|
||||
|
||||
this.log(`${this.side} ${this.id} this.move 1`);
|
||||
|
||||
const cancelId = this.id;
|
||||
|
||||
this.api.cancelOrder(cancelId, (err, filled, data) => {
|
||||
this.log(`${this.side} ${this.id} this.move 2`);
|
||||
|
||||
if(this.handleError(err)) {
|
||||
console.log(new Date, 'error move');
|
||||
return;
|
||||
}
|
||||
|
||||
// it got filled before we could cancel
|
||||
if(cancelId === this.id && this.handleCancel(filled, data)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// update to new price
|
||||
this.price = this.roundPrice(price);
|
||||
|
||||
this.createOrder();
|
||||
});
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
calculateFilled() {
|
||||
let totalFilled = 0;
|
||||
_.each(this.orders, (order, id) => totalFilled += order.filled);
|
||||
|
||||
return totalFilled;
|
||||
}
|
||||
|
||||
moveLimit(limit) {
|
||||
if(this.completed || this.completing) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if(!limit) {
|
||||
limit = this.moveLimitTo;
|
||||
// console.log(new Date, this.side, 'debug resuming move!');
|
||||
} else {
|
||||
this.limitRequested = new Date;
|
||||
this.log(this.side + ' received move limit');
|
||||
}
|
||||
|
||||
if(this.limit === this.roundPrice(limit)) {
|
||||
// effectively nothing changed
|
||||
return false;
|
||||
}
|
||||
|
||||
if(this.cancelling) {
|
||||
return false;
|
||||
}
|
||||
|
||||
this.emit('movelimit', new Date);
|
||||
|
||||
if(
|
||||
this.status === states.INITIALIZING ||
|
||||
this.status === states.SUBMITTED ||
|
||||
this.status === states.MOVING ||
|
||||
this.status === states.CHECKING ||
|
||||
this.sticking
|
||||
) {
|
||||
|
||||
if(
|
||||
!this.api.fillDataOnCancel ||
|
||||
this.status === states.MOVING ||
|
||||
this.status === states.SUBMITTED
|
||||
) {
|
||||
// console.log(new Date, '[sticky]', this.side, 'skipping because:', this.status);
|
||||
this.moveLimitTo = limit;
|
||||
this.movingLimit = true;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
this.limit = this.roundPrice(limit);
|
||||
this.movingLimit = false;
|
||||
|
||||
const moveBuy = this.side === 'buy' && this.limit < this.price;
|
||||
const moveSell = this.side === 'sell' && this.limit > this.price;
|
||||
|
||||
if(moveBuy || moveSell) {
|
||||
this.sticking = true;
|
||||
clearTimeout(this.timeout);
|
||||
|
||||
if(this.api.fillDataOnCancel && this.status === states.CHECKING) {
|
||||
this.log(this.side + 'debug move overwrites running check ' + this.status + ' ' + this.id);
|
||||
this.ignoreCheckResult = true;
|
||||
}
|
||||
|
||||
this.log(this.side + ' moving ' + this.id);
|
||||
|
||||
const timeToMove = new Date - this.limitRequested;
|
||||
if(timeToMove > 300) {
|
||||
console.log(new Date, this.side, 'long time to move:', timeToMove);
|
||||
}
|
||||
this.move(this.limit);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
this.emit('movelimit handled', new Date);
|
||||
return false;
|
||||
}
|
||||
|
||||
moveAmount(amount) {
|
||||
if(this.completed || this.completing)
|
||||
return false;
|
||||
|
||||
if(!amount)
|
||||
amount = this.moveAmountTo;
|
||||
|
||||
if(this.amount === this.roundAmount(amount))
|
||||
// effectively nothing changed
|
||||
return true;
|
||||
|
||||
if(this.calculateFilled() > this.roundAmount(amount)) {
|
||||
// the amount is now below how much we have
|
||||
// already filled.
|
||||
this.filled();
|
||||
return false;
|
||||
}
|
||||
|
||||
if(
|
||||
this.status === states.INITIALIZING ||
|
||||
this.status === states.SUBMITTED ||
|
||||
this.status === states.MOVING ||
|
||||
this.sticking
|
||||
) {
|
||||
this.moveAmountTo = amount;
|
||||
this.movingAmount = true;
|
||||
return;
|
||||
}
|
||||
|
||||
this.amount = this.roundAmount(amount - this.calculateFilled());
|
||||
|
||||
if(this.amount < this.market.minimalOrder.amount) {
|
||||
if(this.calculateFilled()) {
|
||||
// we already filled enough of the order!
|
||||
this.filled();
|
||||
return false;
|
||||
} else {
|
||||
this.handleError(new Error("The amount " + this.amount + " is too small."));
|
||||
}
|
||||
}
|
||||
|
||||
clearTimeout(this.timeout);
|
||||
|
||||
this.movingAmount = false;
|
||||
this.sticking = true;
|
||||
|
||||
this.api.cancelOrder(this.id, (err, filled, data) => {
|
||||
if(this.handleError(err)) {
|
||||
console.log(new Date, 'error cancel');
|
||||
return;
|
||||
}
|
||||
|
||||
// it got filled before we could cancel
|
||||
if(this.handleCancel(filled, data)) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.createOrder();
|
||||
});
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
cancel() {
|
||||
if(this.completed)
|
||||
return;
|
||||
|
||||
if(
|
||||
this.status === states.SUBMITTED ||
|
||||
this.status === states.MOVING ||
|
||||
this.sticking
|
||||
) {
|
||||
this.cancelling = true;
|
||||
return;
|
||||
}
|
||||
|
||||
this.completing = true;
|
||||
clearTimeout(this.timeout);
|
||||
|
||||
this.api.cancelOrder(this.id, (err, filled, data) => {
|
||||
if(this.handleError(err)) {
|
||||
console.log(new Date, 'error cancel');
|
||||
return;
|
||||
}
|
||||
|
||||
this.cancelling = false;
|
||||
|
||||
// it got filled before we could cancel
|
||||
if(this.handleCancel(filled, data)) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.status = states.CANCELLED;
|
||||
this.emitStatus();
|
||||
|
||||
this.finish(false);
|
||||
})
|
||||
}
|
||||
|
||||
createSummary(next) {
|
||||
if(!this.completed)
|
||||
console.log(new Date, 'createSummary BUT ORDER NOT COMPLETED!');
|
||||
|
||||
if(!next)
|
||||
next = _.noop;
|
||||
|
||||
const checkOrders = _.keys(this.orders)
|
||||
.map(id => next => {
|
||||
|
||||
if(!this.orders[id].filled) {
|
||||
return next();
|
||||
}
|
||||
|
||||
setTimeout(() => this.api.getOrder(id, next), this.checkInterval);
|
||||
});
|
||||
|
||||
async.series(checkOrders, (err, trades) => {
|
||||
// note this is a standalone function after the order is
|
||||
// completed, as such we do not use the handleError flow.
|
||||
if(err) {
|
||||
console.log(new Date, 'error createSummary (checkOrder)')
|
||||
return next(err);
|
||||
}
|
||||
|
||||
let price = 0;
|
||||
let amount = 0;
|
||||
let date = moment(0);
|
||||
|
||||
_.each(trades, trade => {
|
||||
if(!trade) {
|
||||
return;
|
||||
}
|
||||
|
||||
// last fill counts
|
||||
date = moment(trade.date);
|
||||
price = ((price * amount) + (+trade.price * trade.amount)) / (+trade.amount + amount);
|
||||
amount += +trade.amount;
|
||||
});
|
||||
|
||||
const summary = {
|
||||
price,
|
||||
amount,
|
||||
date,
|
||||
side: this.side,
|
||||
orders: trades.length
|
||||
}
|
||||
|
||||
const first = _.first(trades);
|
||||
|
||||
if(first && first.fees) {
|
||||
summary.fees = {};
|
||||
|
||||
_.each(trades, trade => {
|
||||
if(!trade) {
|
||||
return;
|
||||
}
|
||||
|
||||
_.each(trade.fees, (amount, currency) => {
|
||||
if(!_.isNumber(summary.fees[currency])) {
|
||||
summary.fees[currency] = amount;
|
||||
} else {
|
||||
summary.fees[currency] += amount;
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
if(first && !_.isUndefined(first.feePercent)) {
|
||||
summary.feePercent = 0;
|
||||
let amount = 0;
|
||||
|
||||
_.each(trades, trade => {
|
||||
if(!trade || _.isUndefined(trade.feePercent)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if(trade.feePercent === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
summary.feePercent = ((summary.feePercent * amount) + (+trade.feePercent * trade.amount)) / (+trade.amount + amount);
|
||||
amount += +trade.amount;
|
||||
});
|
||||
}
|
||||
|
||||
this.emit('summary', summary);
|
||||
next(undefined, summary);
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
module.exports = StickyOrder;
|
||||
Reference in new issue
Block a user