Skip to content

Commit ae5f931

Browse files
authored
Merge pull request #418 from fasenderos/v6-oco
feat: add support for OCO orders
2 parents 41ef939 + 5b19318 commit ae5f931

14 files changed

+1380
-172
lines changed

README.md

Lines changed: 40 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ Ultra-fast matching engine written in TypeScript
1919

2020
- Standard price-time priority
2121
- Supports both market and limit orders
22-
- Supports conditional orders (Stop Market and Stop Limit)
22+
- Supports conditional orders (Stop Market, Stop Limit and OCO)
2323
- Supports time in force GTC, FOK and IOC
2424
- Supports order cancelling
2525
- Supports order price and/or size updating
@@ -57,14 +57,16 @@ const lob = new OrderBook()
5757
Then you'll be able to use next primary functions:
5858

5959
```js
60-
lob.createOrder({ type: 'limit' | 'market' | 'stop_limit' | 'stop_market', side: 'buy' | 'sell', size: number, price?: number, id?: string, stopPrice?: number, timeInForce?: 'GTC' | 'FOK' | 'IOC' })
60+
lob.createOrder({ type: 'limit' | 'market' | 'stop_limit' | 'stop_market' | 'oco', side: 'buy' | 'sell', size: number, price?: number, id?: string, stopPrice?: number, timeInForce?: 'GTC' | 'FOK' | 'IOC' })
6161

6262
lob.limit({ id: string, side: 'buy' | 'sell', size: number, price: number, timeInForce?: 'GTC' | 'FOK' | 'IOC' })
6363

6464
lob.market({ side: 'buy' | 'sell', size: number })
6565

6666
lob.stopLimit({ id: string, side: 'buy' | 'sell', size: number, price: number, stopPrice: number, timeInForce?: 'GTC' | 'FOK' | 'IOC' })
6767

68+
lob.oco({ id: string, side: 'buy' | 'sell', size: number, price: number, stopPrice: number, stopLimitPrice: number, timeInForce?: 'GTC' | 'FOK' | 'IOC', stopLimitTimeInForce?: 'GTC' | 'FOK' | 'IOC' })
69+
6870
lob.stopMarket({ side: 'buy' | 'sell', size: number, stopPrice: number })
6971

7072
lob.modify(orderID: string, { side: 'buy' | 'sell', size: number, price: number })
@@ -79,17 +81,20 @@ To add an order to the order book you can call the general `createOrder()` funct
7981
### Create Order
8082

8183
```js
82-
// Create a limit order
84+
// Create limit order
8385
createOrder({ type: 'limit', side: 'buy' | 'sell', size: number, price: number, id: string, timeInForce?: 'GTC' | 'FOK' | 'IOC' })
8486

85-
// Create a market order
87+
// Create market order
8688
createOrder({ type: 'market', side: 'buy' | 'sell', size: number })
8789

88-
// Create a stop limit order
90+
// Create stop limit order
8991
createOrder({ type: 'stop_limit', side: 'buy' | 'sell', size: number, price: number, id: string, stopPrice: number, timeInForce?: 'GTC' | 'FOK' | 'IOC' })
9092

91-
// Create a stop market order
93+
// Create stop market order
9294
createOrder({ type: 'stop_market', side: 'buy' | 'sell', size: number, stopPrice: number })
95+
96+
// Create OCO order
97+
createOrder({ type: 'oco', side: 'buy' | 'sell', size: number, stopPrice: number, stopLimitPrice: number, timeInForce?: 'GTC' | 'FOK' | 'IOC', stopLimitTimeInForce?: 'GTC' | 'FOK' | 'IOC' })
9398
```
9499

95100
### Create Limit Order
@@ -228,6 +233,35 @@ stopLimit({ side: 'buy' | 'sell', id: string, size: number, price: number, stopP
228233
stopMarket({ side: 'buy' | 'sell', size: number, stopPrice: number })
229234
```
230235

236+
### Create OCO (One-Cancels-the-Other) Order
237+
238+
```js
239+
/**
240+
* Create an OCO (One-Cancels-the-Other) order.
241+
* OCO order combines a `stop_limit` order and a `limit` order, where if stop price
242+
* is triggered or limit order is fully or partially fulfilled, the other is canceled.
243+
* Both orders have the same `side` and `size`. If you cancel one of the orders, the
244+
* entire OCO order pair will be canceled.
245+
*
246+
* For BUY orders the `stopPrice` must be above the current price and the `price` below the current price
247+
* For SELL orders the `stopPrice` must be below the current price and the `price` above the current price
248+
*
249+
* See {@link OCOOrderOptions} for details.
250+
*
251+
* @param options
252+
* @param options.side - `sell` or `buy`
253+
* @param options.id - Unique order ID
254+
* @param options.size - How much of currency you want to trade in units of base currency
255+
* @param options.price - The price of the `limit` order at which the order is to be fullfilled, in units of the quote currency
256+
* @param options.stopPrice - The price at which the `stop_limit` order will be triggered.
257+
* @param options.stopLimitPrice - The price of the `stop_limit` order at which the order is to be fullfilled, in units of the quote currency.
258+
* @param options.timeInForce - Time-in-force of the `limit` order. Type supported are: GTC, FOK, IOC. Default is GTC
259+
* @param options.stopLimitTimeInForce - Time-in-force of the `stop_limit` order. Type supported are: GTC, FOK, IOC. Default is GTC
260+
* @returns An object with the result of the processed order or an error. See {@link IProcessOrder} for the returned data structure
261+
*/
262+
oco({ side: 'buy' | 'sell', id: string, size: number, price: number, stopPrice: number, stopLimitPrice: number, timeInForce?: 'GTC' | 'FOK' | 'IOC', stopLimitTimeInForce?: 'GTC' | 'FOK' | 'IOC' })
263+
```
264+
231265
### Modify an existing order
232266

233267
```js

src/errors.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,13 @@
11
export enum ERROR {
22
Default = 'Something wrong',
33
ErrInsufficientQuantity = 'orderbook: insufficient quantity to calculate price',
4+
ErrInvalidConditionalOrder = 'orderbook: Stop-Limit Order (BUY: marketPrice < stopPrice <= price, SELL: marketPrice > stopPrice >= price). Stop-Market Order (BUY: marketPrice < stopPrice, SELL: marketPrice > stopPrice). OCO order (BUY: price < marketPrice < stopPrice, SELL: price > marketPrice > stopPrice)',
45
ErrInvalidOrderType = "orderbook: supported order type are 'limit' and 'market'",
56
ErrInvalidPrice = 'orderbook: invalid order price',
67
ErrInvalidPriceLevel = 'orderbook: invalid order price level',
78
ErrInvalidPriceOrQuantity = 'orderbook: invalid order price or quantity',
89
ErrInvalidQuantity = 'orderbook: invalid order quantity',
910
ErrInvalidSide = "orderbook: given neither 'bid' nor 'ask'",
10-
ErrInvalidStopPrice = 'orderbook: Invalid Stop Price. For Stop-Limit Order (BUY: marketPrice < stopPrice <= price, SELL: marketPrice > stopPrice >= price). For Stop-Market Order (BUY: marketPrice < stopPrice, SELL: marketPrice > stopPrice)',
1111
ErrInvalidTimeInForce = "orderbook: supported time in force are 'GTC', 'IOC' and 'FOK'",
1212
ErrLimitFOKNotFillable = 'orderbook: limit FOK order not fillable',
1313
ErrOrderExists = 'orderbook: order already exists',
@@ -33,8 +33,8 @@ export const CustomError = (error?: ERROR | string): Error => {
3333
return new Error(ERROR.ErrOrderNotFound)
3434
case ERROR.ErrInvalidSide:
3535
return new Error(ERROR.ErrInvalidSide)
36-
case ERROR.ErrInvalidStopPrice:
37-
return new Error(ERROR.ErrInvalidStopPrice)
36+
case ERROR.ErrInvalidConditionalOrder:
37+
return new Error(ERROR.ErrInvalidConditionalOrder)
3838
case ERROR.ErrInvalidOrderType:
3939
return new Error(ERROR.ErrInvalidOrderType)
4040
case ERROR.ErrInvalidTimeInForce:

src/order.ts

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -75,12 +75,15 @@ export class LimitOrder extends BaseOrder {
7575
private _price: number
7676
private readonly _timeInForce: TimeInForce
7777
private readonly _isMaker: boolean
78+
// Refers to the linked Stop Limit order stopPrice
79+
private readonly _ocoStopPrice?: number
7880
constructor (options: InternalLimitOrderOptions) {
7981
super(options)
8082
this._type = options.type
8183
this._price = options.price
8284
this._timeInForce = options.timeInForce
8385
this._isMaker = options.isMaker
86+
this._ocoStopPrice = options.ocoStopPrice
8487
}
8588

8689
// Getter for order type
@@ -108,6 +111,10 @@ export class LimitOrder extends BaseOrder {
108111
return this._isMaker
109112
}
110113

114+
get ocoStopPrice (): number | undefined {
115+
return this._ocoStopPrice
116+
}
117+
111118
toString = (): string =>
112119
`${this._id}:
113120
type: ${this.type}
@@ -170,7 +177,7 @@ export class StopMarketOrder extends BaseOrder {
170177
side: this._side,
171178
size: this._size,
172179
origSize: this._origSize,
173-
stopPrice: this.stopPrice,
180+
stopPrice: this._stopPrice,
174181
time: this._time
175182
})
176183
}
@@ -181,13 +188,16 @@ export class StopLimitOrder extends BaseOrder {
181188
private readonly _stopPrice: number
182189
private readonly _timeInForce: TimeInForce
183190
private readonly _isMaker: boolean
191+
// It's true when there is a linked Limit Order
192+
private readonly _isOCO: boolean
184193
constructor (options: InternalStopLimitOrderOptions) {
185194
super(options)
186195
this._type = options.type
187196
this._price = options.price
188197
this._stopPrice = options.stopPrice
189198
this._timeInForce = options.timeInForce
190199
this._isMaker = options.isMaker
200+
this._isOCO = options.isOCO ?? false
191201
}
192202

193203
// Getter for order type
@@ -220,6 +230,11 @@ export class StopLimitOrder extends BaseOrder {
220230
return this._isMaker
221231
}
222232

233+
// Getter for order isOCO
234+
get isOCO (): boolean {
235+
return this._isOCO
236+
}
237+
223238
toString = (): string =>
224239
`${this._id}:
225240
type: ${this.type}
@@ -241,8 +256,8 @@ export class StopLimitOrder extends BaseOrder {
241256
size: this._size,
242257
origSize: this._origSize,
243258
price: this._price,
244-
stopPrice: this.stopPrice,
245-
timeInForce: this.timeInForce,
259+
stopPrice: this._stopPrice,
260+
timeInForce: this._timeInForce,
246261
time: this._time,
247262
isMaker: this._isMaker
248263
})

0 commit comments

Comments
 (0)