Skip to content

Commit 1d0a6b1

Browse files
authored
Merge pull request #341 from fasenderos/limit-order-reject-if-match
fix!: modify order price that cross the market price
2 parents 883c13b + be1514b commit 1d0a6b1

File tree

7 files changed

+280
-140
lines changed

7 files changed

+280
-140
lines changed

README.md

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -188,10 +188,10 @@ quantityLeft - 4
188188
* Modify an existing order with given ID
189189
*
190190
* @param orderID - The ID of the order to be modified
191-
* @param orderUpdate - An object with the modified size and/or price of an order. To be note that the `side` can't be modified. The shape of the object is `{side, size, price}`.
191+
* @param orderUpdate - An object with the modified size and/or price of an order. The shape of the object is `{size?: number, price?: number}`.
192192
* @returns The modified order if exists or `undefined`
193193
*/
194-
modify(orderID: string, { side: 'buy' | 'sell', size: number, price: number });
194+
modify(orderID: string, { size: number, price: number });
195195
```
196196

197197
For example:
@@ -206,7 +206,7 @@ bids: 90 -> 5 90 -> 5
206206
80 -> 1 80 -> 1
207207
208208
// Modify the size from 55 to 65
209-
modify("uniqueID", { side: "sell", size: 65, price: 100 })
209+
modify("uniqueID", { size: 65 })
210210
211211
asks: 110 -> 5 110 -> 5
212212
100 -> 56 100 -> 66
@@ -216,7 +216,7 @@ bids: 90 -> 5 90 -> 5
216216
217217
218218
// Modify the price from 100 to 110
219-
modify("uniqueID", { side: "sell", size: 65, price: 110 })
219+
modify("uniqueID", { price: 110 })
220220
221221
asks: 110 -> 5 110 -> 70
222222
100 -> 66 100 -> 1

src/order.ts

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -10,11 +10,8 @@ interface IOrder {
1010
isMaker: boolean
1111
}
1212

13-
export interface OrderUpdate {
14-
side: Side
15-
size?: number
16-
price?: number
17-
}
13+
export interface OrderUpdatePrice { price: number, size?: number }
14+
export interface OrderUpdateSize { price?: number, size: number }
1815

1916
export enum OrderType {
2017
LIMIT = 'limit',
@@ -31,7 +28,7 @@ export class Order {
3128
private readonly _id: string
3229
private readonly _side: Side
3330
private _size: BigNumber
34-
private readonly _price: number
31+
private _price: number
3532
private _time: number
3633
private readonly _isMaker: boolean
3734
constructor (
@@ -65,6 +62,11 @@ export class Order {
6562
return this._price
6663
}
6764

65+
// Getter for order price
66+
set price (price: number) {
67+
this._price = price
68+
}
69+
6870
// Getter for order size
6971
get size (): BigNumber {
7072
return this._size

src/orderbook.ts

Lines changed: 84 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import BigNumber from 'bignumber.js'
22
import { ERROR, CustomError } from './errors'
3-
import { Order, OrderType, OrderUpdate, TimeInForce } from './order'
3+
import { Order, OrderType, OrderUpdatePrice, OrderUpdateSize, TimeInForce } from './order'
44
import { OrderQueue } from './orderqueue'
55
import { OrderSide } from './orderside'
66
import { Side } from './side'
@@ -9,7 +9,7 @@ import { Side } from './side'
99
* This interface represents the result of a processed order or an error
1010
*
1111
* @param done - An array of orders fully filled by the processed order
12-
* @param partial - A partially executed order. It can be null when the processed order
12+
* @param partial - A partially executed order. Is null when the processed order is completelly filled
1313
* @param partialQuantityProcessed - if `partial` is not null, this field represents the processed quantity of the partial order
1414
* @param quantityLeft - more than zero if there are not enought orders to process all quantity
1515
* @param err - Not null if size or price are less or equal zero, or the provided orderId already exists, or something else went wrong.
@@ -252,7 +252,13 @@ export class OrderBook {
252252
}
253253

254254
response.done.push(
255-
new Order(orderID, side, new BigNumber(size), totalPrice / totalQuantity, Date.now())
255+
new Order(
256+
orderID,
257+
side,
258+
new BigNumber(size),
259+
totalPrice / totalQuantity,
260+
Date.now()
261+
)
256262
)
257263
}
258264

@@ -268,23 +274,90 @@ export class OrderBook {
268274
* Modify an existing order with given ID
269275
*
270276
* @param orderID - The ID of the order to be modified
271-
* @param orderUpdate - An object with the modified size and/or price of an order. To be note that the `side` can't be modified. The shape of the object is `{side, size, price}`.
277+
* @param orderUpdate - An object with the modified size and/or price of an order. The shape of the object is `{size, price}`.
272278
* @returns The modified order if exists or `undefined`
273279
*/
274280
public modify = (
275281
orderID: string,
276-
orderUpdate: OrderUpdate
282+
orderUpdate: OrderUpdatePrice | OrderUpdateSize
277283
): Order | undefined => {
278284
const order = this.orders[orderID]
279285
if (order === undefined) return
280-
const side = orderUpdate.side
281-
if (side === Side.BUY) {
282-
return this.bids.update(order, orderUpdate)
283-
} else if (side === Side.SELL) {
284-
return this.asks.update(order, orderUpdate)
286+
287+
let updatedOrder: Order | undefined
288+
if (order.side === Side.BUY) {
289+
// Check if price is changed
290+
if (
291+
orderUpdate.price !== undefined &&
292+
orderUpdate.price !== order.price
293+
) {
294+
const newPrice = orderUpdate.price
295+
// Check if the limit new price is equal or greater than the current ask price.
296+
// If so we have to remove the previous order and create a new limit order
297+
const lowerAsk = this.asks.minPriceQueue()
298+
if (lowerAsk !== undefined && newPrice >= lowerAsk.price()) {
299+
this.cancel(order.id)
300+
const result = this.limit(
301+
order.side,
302+
order.id,
303+
orderUpdate.size ?? order.size.toNumber(),
304+
newPrice
305+
)
306+
updatedOrder = result.partial?.id === order.id ? result.partial : result.done[result.done.length - 1]
307+
} else {
308+
updatedOrder = this.bids.updateOrderPrice(order, {
309+
size: orderUpdate.size,
310+
price: newPrice
311+
})
312+
}
313+
} else if (
314+
orderUpdate.size !== undefined &&
315+
orderUpdate.size !== order.size.toNumber()
316+
) {
317+
// Quantity changed. Price is the same.
318+
const newSize = orderUpdate.size
319+
updatedOrder = this.bids.updateOrderSize(order, { ...orderUpdate, size: newSize })
320+
}
285321
} else {
286-
throw CustomError(ERROR.ErrInvalidSide)
322+
// Check if price is changed
323+
if (
324+
orderUpdate.price !== undefined &&
325+
orderUpdate.price !== order.price
326+
) {
327+
const newPrice = orderUpdate.price
328+
// Check if the new price is equal or lower than the current bid price.
329+
// If so we have to remove the previous order and create a new limit order
330+
const highestBid = this.bids.maxPriceQueue()
331+
if (
332+
highestBid !== undefined &&
333+
newPrice <= highestBid.price()
334+
) {
335+
this.cancel(order.id)
336+
const result = this.limit(
337+
order.side,
338+
order.id,
339+
orderUpdate.size ?? order.size.toNumber(),
340+
newPrice
341+
)
342+
updatedOrder = result.partial?.id === order.id ? result.partial : result.done[result.done.length - 1]
343+
} else {
344+
updatedOrder = this.asks.updateOrderPrice(order, {
345+
size: orderUpdate.size,
346+
price: newPrice
347+
})
348+
}
349+
} else if (
350+
orderUpdate.size !== undefined &&
351+
orderUpdate.size !== order.size.toNumber()
352+
) {
353+
// Quantity changed. Price is the same.
354+
const newSize = orderUpdate.size
355+
updatedOrder = this.asks.updateOrderSize(order, { ...orderUpdate, size: newSize })
356+
}
287357
}
358+
// is undefined when size and price are the same
359+
if (updatedOrder != null) this.orders[orderID] = updatedOrder
360+
return updatedOrder
288361
}
289362

290363
/**

src/orderside.ts

Lines changed: 38 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import BigNumber from 'bignumber.js'
22
import createRBTree from 'functional-red-black-tree'
33
import { CustomError, ERROR } from './errors'
4-
import { Order, OrderUpdate } from './order'
4+
import { Order, OrderUpdatePrice, OrderUpdateSize } from './order'
55
import { OrderQueue } from './orderqueue'
66
import { Side } from './side'
77

@@ -68,7 +68,9 @@ export class OrderSide {
6868
remove = (order: Order): Order => {
6969
const price = order.price
7070
const strPrice = price.toString()
71-
if (this._prices[strPrice] === undefined) throw CustomError(ERROR.ErrInvalidPriceLevel)
71+
if (this._prices[strPrice] === undefined) {
72+
throw CustomError(ERROR.ErrInvalidPriceLevel)
73+
}
7274
this._prices[strPrice].remove(order)
7375
if (this._prices[strPrice].len() === 0) {
7476
/* eslint-disable @typescript-eslint/no-dynamic-delete */
@@ -83,38 +85,40 @@ export class OrderSide {
8385
return order
8486
}
8587

86-
update = (oldOrder: Order, orderUpdate: OrderUpdate): Order | undefined => {
87-
if (
88-
orderUpdate.price !== undefined &&
89-
orderUpdate.price !== oldOrder.price
90-
) {
91-
// Price changed. Remove order and update tree.
92-
this.remove(oldOrder)
93-
const newOrder = new Order(
94-
oldOrder.id,
95-
oldOrder.side,
96-
orderUpdate.size !== undefined ? new BigNumber(orderUpdate.size) : oldOrder.size,
97-
orderUpdate.price,
98-
Date.now(),
99-
oldOrder.isMaker
100-
)
101-
this.append(newOrder)
102-
return newOrder
103-
} else if (
104-
orderUpdate.size !== undefined &&
105-
orderUpdate.size !== oldOrder.size.toNumber()
106-
) {
107-
// Quantity changed. Price is the same.
108-
const oldOrderSize: number = oldOrder.size.toNumber()
109-
const strPrice = oldOrder.price.toString()
110-
const newOrderPrize: number = orderUpdate.price ?? oldOrder.price
111-
this._volume = this._volume.plus(orderUpdate.size - oldOrderSize)
112-
this._total = this._total.plus(
113-
orderUpdate.size * newOrderPrize - oldOrderSize * oldOrder.price
114-
)
115-
this._prices[strPrice].updateOrderSize(oldOrder, orderUpdate.size)
116-
return oldOrder
117-
}
88+
// Update the price of an order and return the order with the updated price
89+
updateOrderPrice = (
90+
oldOrder: Order,
91+
orderUpdate: OrderUpdatePrice
92+
): Order => {
93+
this.remove(oldOrder)
94+
const newOrder = new Order(
95+
oldOrder.id,
96+
oldOrder.side,
97+
orderUpdate.size !== undefined
98+
? new BigNumber(orderUpdate.size)
99+
: oldOrder.size,
100+
orderUpdate.price,
101+
Date.now(),
102+
oldOrder.isMaker
103+
)
104+
this.append(newOrder)
105+
return newOrder
106+
}
107+
108+
// Update the price of an order and return the order with the updated price
109+
updateOrderSize = (
110+
oldOrder: Order,
111+
orderUpdate: OrderUpdateSize
112+
): Order => {
113+
const oldOrderSize: number = oldOrder.size.toNumber()
114+
const strPrice = oldOrder.price.toString()
115+
const newOrderPrize: number = orderUpdate.price ?? oldOrder.price
116+
this._volume = this._volume.plus(orderUpdate.size - oldOrderSize)
117+
this._total = this._total.plus(
118+
orderUpdate.size * newOrderPrize - oldOrderSize * oldOrder.price
119+
)
120+
this._prices[strPrice].updateOrderSize(oldOrder, orderUpdate.size)
121+
return oldOrder
118122
}
119123

120124
// returns max level of price

test/order.test.ts

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,6 @@ void test('it should create order without passing a date', ({
5050
}) => {
5151
const fakeTimestamp = 1487076708000
5252
const { now } = Date
53-
// @ts-expect-error
5453
teardown(() => (Date.now = now))
5554
Date.now = (...m) => fakeTimestamp
5655

@@ -97,3 +96,29 @@ void test('it should create order without passing a date', ({
9796
)
9897
end()
9998
})
99+
100+
void test('test orders setters', (t) => {
101+
const id = 'fakeId'
102+
const side = Side.BUY
103+
const size = 5
104+
const price = 100
105+
const time = Date.now()
106+
const order = new Order(id, side, new BigNumber(size), price, time)
107+
108+
// Price setter
109+
const newPrice = 300
110+
order.price = newPrice
111+
t.equal(order.price, newPrice)
112+
113+
// Size setter
114+
const newSize = new BigNumber(40)
115+
order.size = newSize
116+
t.equal(order.size.toNumber(), newSize.toNumber())
117+
118+
// Time setter
119+
const newTime = Date.now()
120+
order.time = newTime
121+
t.equal(order.time, newTime)
122+
123+
t.end()
124+
})

0 commit comments

Comments
 (0)