Skip to content

Commit

Permalink
feat(bybit): use batch for cancel / place orders
Browse files Browse the repository at this point in the history
  • Loading branch information
iam4x committed Sep 27, 2024
1 parent 40f2254 commit 556eeb5
Show file tree
Hide file tree
Showing 2 changed files with 134 additions and 85 deletions.
217 changes: 132 additions & 85 deletions src/exchanges/bybit/bybit.exchange.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import type { Axios } from 'axios';
import rateLimit from 'axios-rate-limit';
import { chunk, flatten } from 'lodash';
import omit from 'lodash/omit';
import orderBy from 'lodash/orderBy';
import times from 'lodash/times';
Expand Down Expand Up @@ -526,87 +527,41 @@ export class BybitExchange extends BaseExchange {
return this.placeTrailingStopLoss(opts);
}

const market = this.store.markets.find(
({ symbol }) => symbol === opts.symbol
);

if (!market) {
throw new Error(`Market ${opts.symbol} not found`);
}

const positionIdx = await this.getOrderPositionIdx(opts);

const maxSize = market.limits.amount.max;
const pPrice = market.precision.price;
const pAmount = market.precision.amount;

const amount = adjust(opts.amount, pAmount);

const price = opts.price ? adjust(opts.price, pPrice) : null;
const stopLoss = opts.stopLoss ? adjust(opts.stopLoss, pPrice) : null;
const takeProfit = opts.takeProfit ? adjust(opts.takeProfit, pPrice) : null;
const timeInForce =
inverseObj(ORDER_TIME_IN_FORCE)[
opts.timeInForce || OrderTimeInForce.GoodTillCancel
];

const req = omitUndefined({
category: this.accountCategory,
symbol: opts.symbol,
side: inverseObj(ORDER_SIDE)[opts.side],
orderType: inverseObj(ORDER_TYPE)[opts.type],
qty: `${amount}`,
price: opts.type === OrderType.Limit ? `${price}` : undefined,
stopLoss: opts.stopLoss ? `${stopLoss}` : undefined,
takeProfit: opts.takeProfit ? `${takeProfit}` : undefined,
reduceOnly: opts.reduceOnly || false,
slTriggerBy: opts.stopLoss ? 'MarkPrice' : undefined,
tpTriggerBy: opts.takeProfit ? 'LastPrice' : undefined,
timeInForce: opts.type === OrderType.Limit ? timeInForce : undefined,
closeOnTrigger: false,
positionIdx,
});

const lots = amount > maxSize ? Math.ceil(amount / maxSize) : 1;
const rest = amount > maxSize ? adjust(amount % maxSize, pAmount) : 0;

const lotSize = adjust((amount - rest) / lots, pAmount);
const payloads = await this.formatCreateOrder(opts);
return this.placeOrderBatch(payloads);
};

const payloads = times(lots, (idx) => {
// We want to remove stopLoss and takeProfit from the rest of the orders
// because they are already set on the first one
const payload =
idx > 0
? omit(req, ['stopLoss', 'takeProfit', 'slTriggerBy', 'tpTriggerBy'])
: req;
placeOrders = async (opts: PlaceOrderOpts[]) => {
const tslOrders = opts.filter((o) => o.type === OrderType.TrailingStopLoss);
const slOrTpOrders = opts.filter(
(o) => o.type === OrderType.StopLoss || o.type === OrderType.TakeProfit
);

return { ...payload, qty: `${lotSize}` };
});
const normalOrders = opts.filter(
(o) =>
o.type !== OrderType.TrailingStopLoss &&
o.type !== OrderType.StopLoss &&
o.type !== OrderType.TakeProfit
);

if (rest) payloads.push({ ...req, qty: `${rest}` });
const tslOrderIds = flatten(
await mapSeries(tslOrders, this.placeTrailingStopLoss)
);

const responses = await mapSeries(payloads, async (p) => {
try {
const { data } = await this.unlimitedXHR.post(
ENDPOINTS.CREATE_ORDER,
p
);
return data;
} catch (err: any) {
this.emitter.emit('error', err?.response?.data?.retMsg || err.message);
return undefined;
}
});
const slOrTpOrderIds = flatten(
await mapSeries(slOrTpOrders, this.placeStopLossOrTakeProfit)
);

const fullfilled = responses.filter((r) => r !== undefined);
const normalOrdersPayloads = await mapSeries(
normalOrders,
this.formatCreateOrder
);

fullfilled.forEach((resp) => {
if (v(resp, 'retMsg') !== 'OK') {
this.emitter.emit('error', v(resp, 'retMsg'));
}
});
const normalOrderIds = await this.placeOrderBatch(
flatten(normalOrdersPayloads)
);

return fullfilled.map((resp) => resp.result.orderId);
return [...normalOrderIds, ...slOrTpOrderIds, ...tslOrderIds];
};

placeStopLossOrTakeProfit = async (opts: PlaceOrderOpts) => {
Expand All @@ -632,7 +587,7 @@ export class BybitExchange extends BaseExchange {
this.emitter.emit('error', data.retMsg);
}

return [data.result.orderId];
return [data.result.orderId] as string[];
};

placeTrailingStopLoss = async (opts: PlaceOrderOpts) => {
Expand Down Expand Up @@ -661,22 +616,31 @@ export class BybitExchange extends BaseExchange {
this.emitter.emit('error', data.retMsg);
}

return [data.result.orderId];
return [data.result.orderId] as string[];
};

cancelOrders = async (orders: Order[]) => {
await forEachSeries(orders, async (order) => {
const { data } = await this.unlimitedXHR.post(ENDPOINTS.CANCEL_ORDER, {
await forEachSeries(chunk(orders, 10), async (chunkOrders) => {
const { data } = await this.unlimitedXHR.post<{
result: { list: Array<{ orderId: string }> };
retExtInfo: { list: Array<{ code: number; msg: string }> };
}>(ENDPOINTS.CANCEL_ORDERS, {
category: this.accountCategory,
symbol: order.symbol,
orderId: order.id,
request: chunkOrders.map((o) => ({
symbol: o.symbol,
orderId: o.id,
})),
});

if (data.retMsg === 'OK' || data.retMsg.includes('order not exists or')) {
this.store.removeOrder(order);
} else {
this.emitter.emit('error', data.retMsg);
}
data.result.list.forEach((o, idx) => {
const info = data.retExtInfo.list[idx];

if (info.msg === 'OK' || info.msg.includes('Order does not exist')) {
this.store.removeOrder({ id: o.orderId });
} else {
this.emitter.emit('error', info.msg);
}
});
});
};

Expand Down Expand Up @@ -825,6 +789,89 @@ export class BybitExchange extends BaseExchange {
return orders;
}

private formatCreateOrder = async (opts: PlaceOrderOpts) => {
const market = this.store.markets.find(
({ symbol }) => symbol === opts.symbol
);

if (!market) {
throw new Error(`Market ${opts.symbol} not found`);
}

const positionIdx = await this.getOrderPositionIdx(opts);

const maxSize = market.limits.amount.max;
const pPrice = market.precision.price;
const pAmount = market.precision.amount;

const amount = adjust(opts.amount, pAmount);

const price = opts.price ? adjust(opts.price, pPrice) : null;
const stopLoss = opts.stopLoss ? adjust(opts.stopLoss, pPrice) : null;
const takeProfit = opts.takeProfit ? adjust(opts.takeProfit, pPrice) : null;
const timeInForce =
inverseObj(ORDER_TIME_IN_FORCE)[
opts.timeInForce || OrderTimeInForce.GoodTillCancel
];

const req = omitUndefined({
category: this.accountCategory,
symbol: opts.symbol,
side: inverseObj(ORDER_SIDE)[opts.side],
orderType: inverseObj(ORDER_TYPE)[opts.type],
qty: `${amount}`,
price: opts.type === OrderType.Limit ? `${price}` : undefined,
stopLoss: opts.stopLoss ? `${stopLoss}` : undefined,
takeProfit: opts.takeProfit ? `${takeProfit}` : undefined,
reduceOnly: opts.reduceOnly || false,
slTriggerBy: opts.stopLoss ? 'MarkPrice' : undefined,
tpTriggerBy: opts.takeProfit ? 'LastPrice' : undefined,
timeInForce: opts.type === OrderType.Limit ? timeInForce : undefined,
closeOnTrigger: false,
positionIdx,
});

const lots = amount > maxSize ? Math.ceil(amount / maxSize) : 1;
const rest = amount > maxSize ? adjust(amount % maxSize, pAmount) : 0;

const lotSize = adjust((amount - rest) / lots, pAmount);

const payloads = times(lots, (idx) => {
// We want to remove stopLoss and takeProfit from the rest of the orders
// because they are already set on the first one
const payload =
idx > 0
? omit(req, ['stopLoss', 'takeProfit', 'slTriggerBy', 'tpTriggerBy'])
: req;

return { ...payload, qty: `${lotSize}` };
});

if (rest) payloads.push({ ...req, qty: `${rest}` });

return payloads;
};

private placeOrderBatch = async (payloads: Array<Record<string, any>>) => {
const responses = await mapSeries(chunk(payloads, 10), async (batch) => {
try {
const { data } = await this.unlimitedXHR.post<{
result: { list: Array<{ orderId: string }> };
}>(ENDPOINTS.CREATE_ORDERS, {
category: this.accountCategory,
request: batch,
});

return data.result.list.map((o) => o.orderId);
} catch (err: any) {
this.emitter.emit('error', err?.response?.data?.retMsg || err.message);
return [];
}
});

return flatten(responses);
};

private fetchPositionMode = async (symbol: string) => {
if (this.store.options.isHedged) return true;

Expand Down
2 changes: 2 additions & 0 deletions src/exchanges/bybit/bybit.types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,12 +34,14 @@ export const ENDPOINTS = {
TICKERS: '/v5/market/tickers',
MARKETS: '/v5/market/instruments-info',
CANCEL_ORDER: '/v5/order/cancel',
CANCEL_ORDERS: '/v5/order/cancel-batch',
CANCEL_SYMBOL_ORDERS: '/v5/order/cancel-all',
POSITIONS: '/v5/position/list',
KLINE: '/v5/market/kline',
SET_LEVERAGE: '/v5/position/set-leverage',
SET_TRADING_STOP: '/v5/position/trading-stop',
CREATE_ORDER: '/v5/order/create',
CREATE_ORDERS: '/v5/order/create-batch',
SET_POSITION_MODE: '/v5/position/switch-mode',
};

Expand Down

0 comments on commit 556eeb5

Please sign in to comment.