Skip to content

Commit

Permalink
feat(orderbook): immediate-or-cancel orders
Browse files Browse the repository at this point in the history
This adds an "immediate-or-cancel" order type that prevents an order
from entering the order book after matching is complete. It combines
the immediate nature of a market order with the price requirement of a
limit order.

Closes #622.
  • Loading branch information
sangaman committed Dec 12, 2019
1 parent ff36567 commit c499d8b
Show file tree
Hide file tree
Showing 15 changed files with 197 additions and 185 deletions.
3 changes: 2 additions & 1 deletion docs/api.md

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

14 changes: 11 additions & 3 deletions lib/cli/placeorder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,11 @@ export const placeOrderBuilder = (argv: Argv, side: OrderSide) => {
alias: 'r',
describe: 'the local order id of a previous order to be replaced',
})
.option('ioc', {
type: 'boolean',
alias: 'i',
describe: 'immediate-or-cancel',
})
.example(`$0 ${command} 5 LTC/BTC .01 1337`, `place a limit order to ${command} 5 LTC @ 0.01 BTC with local order id 1337`)
.example(`$0 ${command} 3 LTC/BTC mkt`, `place a market order to ${command} 3 LTC for BTC`)
.example(`$0 ${command} 10 ZRX/GNT market`, `place a market order to ${command} 10 ZRX for GNT`);
Expand All @@ -45,6 +50,7 @@ export const placeOrderHandler = (argv: Arguments<any>, side: OrderSide) => {
request.setQuantity(coinsToSats(argv.quantity));
request.setSide(side);
request.setPairId(argv.pair_id.toUpperCase());
request.setImmediateOrCancel(argv.ioc);

if (!isNaN(numericPrice)) {
request.setPrice(numericPrice);
Expand Down Expand Up @@ -78,15 +84,17 @@ export const placeOrderHandler = (argv: Arguments<any>, side: OrderSide) => {
noMatches = false;
formatSwapSuccess(swapSuccess.toObject());
} else if (remainingOrder) {
if (noMatches) {
console.log('no matches found');
}
formatRemainingOrder(remainingOrder.toObject());
} else if (swapFailure) {
formatSwapFailure(swapFailure.toObject());
}
}
});
subscription.on('end', () => {
if (noMatches) {
console.log('no matches found');
}
});
} else {
loadXudClient(argv).placeOrderSync(request, callback(argv, formatPlaceOrderOutput));
}
Expand Down
44 changes: 27 additions & 17 deletions lib/orderbook/OrderBook.ts
Original file line number Diff line number Diff line change
Expand Up @@ -316,7 +316,8 @@ class OrderBook extends EventEmitter {
return pair.destroy();
}

public placeLimitOrder = async (order: OwnLimitOrder, onUpdate?: (e: PlaceOrderEvent) => void): Promise<PlaceOrderResult> => {
public placeLimitOrder = async (order: OwnLimitOrder, immediateOrCancel = false,
onUpdate?: (e: PlaceOrderEvent) => void): Promise<PlaceOrderResult> => {
const stampedOrder = this.stampOwnOrder(order);
if (this.nomatching) {
this.addOwnOrder(stampedOrder);
Expand All @@ -330,7 +331,7 @@ class OrderBook extends EventEmitter {
};
}

return this.placeOrder(stampedOrder, false, onUpdate, Date.now() + OrderBook.MAX_PLACEORDER_ITERATIONS_TIME);
return this.placeOrder(stampedOrder, immediateOrCancel, onUpdate, Date.now() + OrderBook.MAX_PLACEORDER_ITERATIONS_TIME);
}

public placeMarketOrder = async (order: OwnMarketOrder, onUpdate?: (e: PlaceOrderEvent) => void): Promise<PlaceOrderResult> => {
Expand Down Expand Up @@ -519,9 +520,13 @@ class OrderBook extends EventEmitter {
// failed swaps will be added to the remaining order which may be added to the order book.
await Promise.all(matchPromises);

if (remainingOrder && !discardRemaining) {
this.addOwnOrder(remainingOrder);
onUpdate && onUpdate({ type: PlaceOrderEventType.RemainingOrder, payload: remainingOrder });
if (remainingOrder) {
if (discardRemaining) {
remainingOrder = undefined;
} else {
this.addOwnOrder(remainingOrder);
onUpdate && onUpdate({ type: PlaceOrderEventType.RemainingOrder, payload: remainingOrder });
}
}

return {
Expand Down Expand Up @@ -617,6 +622,16 @@ class OrderBook extends EventEmitter {
return true;
}

public getOwnOrderByLocalId = (localId: string) => {
const orderIdentifier = this.localIdMap.get(localId);
if (!orderIdentifier) {
throw errors.LOCAL_ID_DOES_NOT_EXIST(localId);
}

const order = this.getOwnOrder(orderIdentifier.id, orderIdentifier.pairId);
return order;
}

/**
* Removes all or part of an order from the order book by its local id. Throws an error if the
* specified pairId is not supported or if the order to cancel could not be found.
Expand All @@ -628,12 +643,7 @@ class OrderBook extends EventEmitter {
* @returns any quantity of the order that was on hold and could not be immediately removed (if allowed).
*/
public removeOwnOrderByLocalId = (localId: string, allowAsyncRemoval?: boolean, quantityToRemove?: number) => {
const orderIdentifier = this.localIdMap.get(localId);
if (!orderIdentifier) {
throw errors.LOCAL_ID_DOES_NOT_EXIST(localId);
}

const order = this.getOwnOrder(orderIdentifier.id, orderIdentifier.pairId);
const order = this.getOwnOrderByLocalId(localId);

let remainingQuantityToRemove = quantityToRemove || order.quantity;

Expand All @@ -644,28 +654,28 @@ class OrderBook extends EventEmitter {

const removableQuantity = order.quantity - order.hold;
if (remainingQuantityToRemove <= removableQuantity) {
this.removeOwnOrder(orderIdentifier.id, orderIdentifier.pairId, remainingQuantityToRemove);
this.removeOwnOrder(order.id, order.pairId, remainingQuantityToRemove);
remainingQuantityToRemove = 0;
} else {
// we can't immediately remove the entire quantity because of a hold on the order.
if (!allowAsyncRemoval) {
throw errors.QUANTITY_ON_HOLD(localId, order.hold);
}

this.removeOwnOrder(orderIdentifier.id, orderIdentifier.pairId, removableQuantity);
this.removeOwnOrder(order.id, order.pairId, removableQuantity);
remainingQuantityToRemove -= removableQuantity;

const failedHandler = (deal: SwapDeal) => {
if (deal.orderId === orderIdentifier.id) {
if (deal.orderId === order.id) {
// remove the portion that failed now that it's not on hold
const quantityToRemove = Math.min(deal.quantity!, remainingQuantityToRemove);
this.removeOwnOrder(orderIdentifier.id, orderIdentifier.pairId, quantityToRemove);
this.removeOwnOrder(order.id, order.pairId, quantityToRemove);
cleanup(quantityToRemove);
}
};

const paidHandler = (result: SwapSuccess) => {
if (result.orderId === orderIdentifier.id) {
if (result.orderId === order.id) {
const quantityToRemove = Math.min(result.quantity, remainingQuantityToRemove);
cleanup(quantityToRemove);
}
Expand Down Expand Up @@ -873,7 +883,7 @@ class OrderBook extends EventEmitter {
throw errors.DUPLICATE_ORDER(order.localId);
}

return { ...order, id, initialQuantity: order.quantity, createdAt: ms() };
return { ...order, id, initialQuantity: order.quantity, hold: 0, createdAt: ms() };
}

private handleOrderInvalidation = (oi: OrderPortion, peerPubKey: string) => {
Expand Down
13 changes: 8 additions & 5 deletions lib/orderbook/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,8 +60,6 @@ export type OrderIdentifier = Pick<MarketOrder, 'pairId'> & {
type Local = {
/** A local identifier for the order. */
localId: string;
/** The amount of an order that is on hold pending swap execution. */
hold: number;
};

/** Properties that apply only to orders placed by remote peers. */
Expand All @@ -82,16 +80,21 @@ export type OwnMarketOrder = MarketOrder & Local;

export type OwnLimitOrder = LimitOrder & Local;

export type OwnOrder = OwnLimitOrder & Stamp;
/** A local order that may enter the order book. */
export type OwnOrder = OwnLimitOrder & Stamp & {
/** The amount of an order that is on hold pending swap execution. */
hold: number;
};

/** A peer order that may enter the order book. */
export type PeerOrder = LimitOrder & Stamp & Remote;

export type Order = OwnOrder | PeerOrder;

/** An outgoing version of a local own order without fields that are not useful for peers. */
/** An outgoing local order which only includes fields that are relevant to peers. */
export type OutgoingOrder = Pick<OwnOrder, Exclude<keyof OwnOrder, 'localId' | 'createdAt' | 'hold' | 'initialQuantity'>>;

/** An outgoing version of a local own order without fields that are not useful for peers. */
/** An incoming peer order which only includes fields that are relevant to us. */
export type IncomingOrder = OutgoingOrder & Remote;

/** A reference to a portion of an existing order. */
Expand Down
2 changes: 1 addition & 1 deletion lib/proto/annotations_grpc_pb.js

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion lib/proto/xudp2p_grpc_pb.js

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading

0 comments on commit c499d8b

Please sign in to comment.