<commit_before>/*
  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) {
    super(api);

    // global async lock
    this.sticking = false;
  }

  create(side, rawAmount, params = {}) {
    if(this.completed || this.completing) {
      return false;
    }

    this.side = side;

    this.amount = this.api.roundAmount(rawAmount);

    if(side === 'buy') {
      if(params.limit)
        this.limit = this.api.roundPrice(params.limit);
      else
        this.limit = Infinity;
    } else {
      if(params.limit)
        this.limit = this.api.roundPrice(params.limit);
      else
        this.limit = -Infinity;
    }

    this.status = states.SUBMITTED;
    this.emitStatus();

    this.orders = {};

    this.outbid = params.outbid && _.isFunction(this.api.outbidPrice);

    this.price = this.calculatePrice(this.data.ticker);

    this.createOrder();

    return this;
  }

  calculatePrice(ticker) {

    const r = this.api.roundPrice;

    if(this.side === 'buy') {
      if(ticker.bid >= this.limit) {
        return r(this.limit);
      }

      if(!this.outbid) {
        return r(ticker.bid);
      }

      const outbidPrice = this.api.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(ticker.ask <= this.limit) {
        return r(this.limit);
      }

      if(!this.outbid) {
        return r(ticker.ask);
      }

      const outbidPrice = this.api.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.api.roundAmount(this.amount - alreadyFilled),
      price: this.price,
      alreadyFilled
    });
  }

  handleCreate(err, id) {
    if(err) {
      console.log('handleCreate', err.message);
      throw err;
    }

    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.id = id;
    this.orders[id] = {
      price: this.price,
      filled: 0
    }

    this.emit('new order', this.id);

    this.status = states.OPEN;
    this.emitStatus();

    // remove lock
    this.sticking = false;

    // check whether we had an action pending
    if(this.cancelling)
      return this.cancel();

    if(this.movingLimit)
      return this.moveLimit();

    if(this.movingAmount)
      return this.moveAmount();

    // register check
    this.timeout = setTimeout(this.checkOrder, this.checkInterval);
  }

  checkOrder() {
    if(this.completed || this.completing) {
      return console.log(new Date, 'checkOrder called on completed/completing order..', this.completed, this.completing);
    }

    this.sticking = true;

    this.api.checkOrder(this.id, (err, result) => {
      if(err) {
        console.log(new Date, 'error creating:', err.message);
        throw err;
      }

      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.timeout = setTimeout(this.checkOrder, this.checkInterval);
          this.sticking = false;
          return;
        }

        this.api.getTicker((err, ticker) => {
          if(err)
            throw err;

          this.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));
          }

          this.timeout = setTimeout(this.checkOrder, this.checkInterval);
          this.sticking = false;
        });

        return;
      }

      if(!result.executed) {
        // not open and not executed means it never hit the book
        this.sticking = false;
        this.status = states.REJECTED;
        this.emitStatus();
        this.finish();
        return;
      }

      // order got filled!
      this.orders[this.id].filled = this.amount;
      this.sticking = false;
      this.emit('fill', this.amount);
      this.filled(this.price);

    });
  }

  // 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 true;
    }

    // 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.api.roundAmount(this.amount - alreadyFilled);
        amountFilled = this.api.roundAmount(orderAmount - data.remaining);
      }

      if(amountFilled > this.orders[this.id].filled) {
        this.orders[this.id].filled = amountFilled;
        this.emit('fill', this.calculateFilled());
      }
    }

    return false;
  }

  move(price) {
    if(this.completed || this.completing) {
      return false;
    }

    this.status = states.MOVING;
    this.emitStatus();

    this.api.cancelOrder(this.id, (err, filled, data) => {
      // it got filled before we could cancel
      if(this.handleCancel(filled, data)) {
        return;
      }

      // update to new price
      this.price = this.api.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;
    }

    if(this.limit === this.api.roundPrice(limit)) {
      // effectively nothing changed
      return false;
    }

    if(
      this.status === states.INITIALIZING ||
      this.status === states.SUBMITTED ||
      this.status === states.MOVING ||
      this.sticking
    ) {
      this.moveLimitTo = limit;
      this.movingLimit = true;
      return;
    }

    this.limit = this.api.roundPrice(limit);

    clearTimeout(this.timeout);

    this.movingLimit = false;

    if(this.side === 'buy' && this.limit < this.price) {
      this.sticking = true;
      this.move(this.limit);
    } else if(this.side === 'sell' && this.limit > this.price) {
      this.sticking = true;
      this.move(this.limit);
    } else {
      this.timeout = setTimeout(this.checkOrder, this.checkInterval);
    }

    return true;
  }

  moveAmount(amount) {
    if(this.completed || this.completing)
      return false;

    if(!amount)
      amount = this.moveAmountTo;

    if(this.amount === this.api.roundAmount(amount))
      // effectively nothing changed
      return true;

    if(this.calculateFilled() > this.api.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.api.roundAmount(amount - this.calculateFilled());

    if(this.amount < this.data.market.minimalOrder.amount) {
      if(this.calculateFilled()) {
        // we already filled enough of the order!
        this.filled();
        return false;
      } else {
        throw 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(err) {
        throw err;
      }

      // it got filled before we could cancel
      if(this.handleCancel(filled, data)) {
        return;
      }

      this.createOrder();
    });

    return true;
  }<commit_msg>upstream error as event & force bind wrapper rounding fns<commit_after>/*
  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) {
    super(api);

    // 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);
    }
  }

  create(side, rawAmount, params = {}) {
    if(this.completed || this.completing) {
      return false;
    }

    this.side = side;

    this.amount = this.roundAmount(rawAmount);

    if(side === 'buy') {
      if(params.limit)
        this.limit = this.roundPrice(params.limit);
      else
        this.limit = Infinity;
    } else {
      if(params.limit)
        this.limit = this.roundPrice(params.limit);
      else
        this.limit = -Infinity;
    }

    this.status = states.SUBMITTED;
    this.emitStatus();

    this.orders = {};

    this.outbid = params.outbid && _.isFunction(this.outbidPrice);

    this.price = this.calculatePrice(this.data.ticker);

    this.createOrder();

    return this;
  }

  calculatePrice(ticker) {

    const r = this.roundPrice;

    if(this.side === 'buy') {
      if(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(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
    });
  }

  handleCreate(err, id) {
    if(err) {
      console.log('handleCreate', err.message);
      throw err;
    }

    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.id = id;
    this.orders[id] = {
      price: this.price,
      filled: 0
    }

    this.emit('new order', this.id);

    this.status = states.OPEN;
    this.emitStatus();

    // remove lock
    this.sticking = false;

    // check whether we had an action pending
    if(this.cancelling)
      return this.cancel();

    if(this.movingLimit)
      return this.moveLimit();

    if(this.movingAmount)
      return this.moveAmount();

    // register check
    this.timeout = setTimeout(this.checkOrder, this.checkInterval);
  }

  checkOrder() {
    if(this.completed || this.completing) {
      return console.log(new Date, 'checkOrder called on completed/completing order..', this.completed, this.completing);
    }

    this.sticking = true;

    this.api.checkOrder(this.id, (err, result) => {
      if(err) {
        console.log(new Date, 'error creating:', err.message);
        throw err;
      }

      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.timeout = setTimeout(this.checkOrder, this.checkInterval);
          this.sticking = false;
          return;
        }

        this.api.getTicker((err, ticker) => {
          if(err)
            throw err;

          this.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));
          }

          this.timeout = setTimeout(this.checkOrder, this.checkInterval);
          this.sticking = false;
        });

        return;
      }

      if(!result.executed) {
        // not open and not executed means it never hit the book
        this.sticking = false;
        this.status = states.REJECTED;
        this.emitStatus();
        this.finish();
        return;
      }

      // order got filled!
      this.orders[this.id].filled = this.amount;
      this.sticking = false;
      this.emit('fill', this.amount);
      this.filled(this.price);

    });
  }

  // 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.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 true;
    }

    // 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 false;
  }

  move(price) {
    if(this.completed || this.completing) {
      return false;
    }

    this.status = states.MOVING;
    this.emitStatus();

    this.api.cancelOrder(this.id, (err, filled, data) => {
      // it got filled before we could cancel
      if(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;
    }

    if(this.limit === this.roundPrice(limit)) {
      // effectively nothing changed
      return false;
    }

    if(
      this.status === states.INITIALIZING ||
      this.status === states.SUBMITTED ||
      this.status === states.MOVING ||
      this.sticking
    ) {
      this.moveLimitTo = limit;
      this.movingLimit = true;
      return;
    }

    this.limit = this.roundPrice(limit);

    clearTimeout(this.timeout);

    this.movingLimit = false;

    if(this.side === 'buy' && this.limit < this.price) {
      this.sticking = true;
      this.move(this.limit);
    } else if(this.side === 'sell' && this.limit > this.price) {
      this.sticking = true;
      this.move(this.limit);
    } else {
      this.timeout = setTimeout(this.checkOrder, this.checkInterval);
    }

    return true;
  }

  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.data.market.minimalOrder.amount) {
      if(this.calculateFilled()) {
        // we already filled enough of the order!
        this.filled();
        return false;
      } else {
        throw 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(err) {
        throw err;
      }

      // it got filled before we could cancel
      if(this.handleCancel(filled, data)) {
        return;
      }

      this.createOrder();
    });

    return true;
  }
{'fraction_non_alphanumeric': 0.12955153260645957, 'fraction_numerical': 0.00030857848179386956, 'mean_word_length': 2.154095701540957, 'pattern_counts': {'":': 0, '<': 13, '<?xml version=': 0, '>': 25, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '4088556', 'n_tokens_mistral': 6172, 'n_tokens_neox': 5910, 'n_words': 1378}