Эта статья является логическим продолжением предыдущей. Кто не читал - прошу ознакомиться тут.
Итак продолжим. Быстренько допишем еще пару вспомогательных сервисов и перейдем к базовой стратегии.
Чтобы запросы к бирже не превышали допустимых лимитов в секунду - добавим сервис асинхронной очереди.
const MAX_QUERY_PER_SECOND = 4;
class ApiQueueService {
constructor() {
this.queue = [];
this.executeHistory = [];
this.isRun = false;
}
executeInQueue(method, ...data) {
return new Promise((resolve, reject) => {
this.addToQueue(method, data, ({ err, data }) => {
if (err) {
return reject(err);
}
return resolve(data);
});
});
}
addToQueue(method, data, cb) {
this.queue.push({ method, data, cb });
this.run();
}
run(force = false) {
if (this.isRun && !force) {
return;
}
this.isRun = true;
if (this.isBusy()) {
const timer = setTimeout(() => {
clearTimeout(timer);
this.run(true);
}, 100);
return;
}
const q = this.getFirst();
if (!q) {
this.isRun = false;
return;
}
this.execute(q).finally(() => {
this.removeFirst();
if (this.queue.length === 0) {
this.isRun = false;
return;
}
this.run(true);
});
}
getFirst() {
return this.queue && this.queue[0];
}
removeFirst() {
this.queue.splice(0, 1);
}
isBusy() {
return this.getCountQueryInLastSecond() >= MAX_QUERY_PER_SECOND;
}
getCountQueryInLastSecond() {
const currentTime = +new Date();
this.executeHistory = this.executeHistory.filter((d) => d.time > (currentTime - 1000));
return this.executeHistory.length;
}
execute(q) {
this.executeHistory.push({ time: +new Date() });
return q.method(...(q.data || [])).then((data) => {
q.cb({ data });
}).catch((err) => {
q.cb({ err });
});
}
}
module.exports = new ApiQueueService();
Делаем из него синглтон при помощи module.exports = new ApiQueueService();
и запустим небольшой тест очереди:
const apiQueue = require('./services/copy/ApiQueueService');
const apiQueue2 = require('./services/copy/ApiQueueService');
const apiQueue3 = require('./services/copy/ApiQueueService');
const wait = (t) => new Promise((resolve) => setTimeout(() => resolve(), t));
const testAsync = async (d) => {
const { t, ...other } = d;
await wait(t);
return other;
};
(async () => {
for (let i = 0; i < 100; i++) {
apiQueue.executeInQueue(testAsync, { t: 100, index: i, fromFile: 1 }).then((d) => console.log(d));
apiQueue2.executeInQueue(testAsync, { t: 100, index: i, fromFile: 2 }).then((d) => console.log(d));
apiQueue3.executeInQueue(testAsync, { t: 100, index: i, fromFile: 3 }).then((d) => console.log(d));
}
})();
Отлично. Работает как надо. Также нам надо следить за статусом наших ордеров. Для этого установим сокет соединение с биржей по API ключам и будем ловить ивенты обновления ордеров. Бинанс дает нам слушать такие события через сокет:
this.api.websockets.userFutureData(
this.marginCallCallback,
this.accountUpdateCallback,
this.orderUpdateCallback,
this.subscribedCallback,
this.accountConfigUpdateCallback,
)
Испольуем только одно событие orderUpdateCallback
. Как только будет приходить событие - проверяем ордер. Если его статус в базе отличается от того, что пришел в событии - обновляем статус и сохраняем комиссию.
checkOrder = async (data)=>{
if (data && data.order) {
try {
const { order } = data
if (order && order.orderId && order.orderStatus && order.executionType === 'TRADE') {
const o = await orderProvider.getOrder(order)
if (o && o.status !== order.orderStatus) {
o.status = order.orderStatus
await orderProvider.updateOrderCommission(o, order)
}
}
} catch (e) {
this.error('executionTRADE error', e)
}
}
}
Теперь напишем базовый сервис, который будет управлять стратегией торговли. Стратегия должна иметь несколько состояний WAIT_ENTRY_POINT
,IN_PROGRESS
, COMPLETED
. И в зависимости от состояний должна выполнять разные действия:
WAIT_ENTRY_POINT
- стратегия анализирует свечи, патерны, индикаторы (зависит от торговой стратегии) и выставляет ордера для входа в лонг/шорт. Пока стратегия в этом режиме, он должен переодически повторять эту процедуру, корректирую точку входа (отменить старые ордера и добавлять новые)IN_PROGRESS
- в этом состоянии стратегия уже в "позиции" (ордера для входа в лонг/шорт сработали). Теперь ей нужно определится с уровнями фиксирования позиции, а также ордером стоп лосс. Также дополнительная логика при срабатывании ордера фиксирования позицииCOMPLETED
- в это состояние стратегия переходит только когда позиция полностью зафиксирована. Идет просчет прибыли/убытка и запускается новая стратегия со статусомWAIT_ENTRY_POINT
Получился вот такой базовый класс. По определенному событию, стратегия будет проверятся методом checkStrategy
и в зависимости от статуса выполняется нужная функция. Complete
у всех стратегий одинаковый, а вот wait
и progress
- это самая важная часть, это логика работы нашего бота. И поэтому методы wait
и progress
определяются в каждой конкретной стратегии.
class Strategy extends BaseApiService{
constructor(params) {
const { symbol, user, positionSide } = params
super(user.binanceApiKey, user.binanceApiSecret)
this.symbol = symbol
this.user = user
this.positionSide = positionSide
}
async init() {
await this.loadStrategy()
await this.checkStrategy()
}
async checkStrategy() {
await this.loadStrategy()
if (this.strategy.status === STRATEGY.STATUS.WAIT_ENTRY_POINT) {
await this.wait()
} else if (this.strategy.status === STRATEGY.STATUS.IN_PROGRESS) {
await this.progress()
} else if (this.strategy.status === STRATEGY.STATUS.COMPLETED) {
await this.complete()
}
}
async wait() {
// this should be implemented in parent strategy class
}
async progress() {
// this should be implemented in parent strategy class
}
async cancelAllOrders() {
if (this.strategy.orders && Array.isArray(this.strategy.orders)) {
const prs = []
for (const order of this.strategy.orders) {
if (order && order.orderId &&
![ORDER.STATUS.FILLED, ORDER.STATUS.CANCELED, ORDER.STATUS.EXPIRED].includes(order.status)) {
prs.push(this.cancelDBOrder(order))
}
}
if (prs.length > 0) {
await Promise.all(prs)
}
}
}
async addOrderToStrategy(order) {
return orderProvider.createOrder({
...order,
userId: this.user.id,
strategyId: this.strategy.id,
})
}
async complete() {
this.strategy.status = STRATEGY.STATUS.COMPLETED
await this.cancelAllOrders()
await this.strategy.save()
await this.loadStrategy()
}
async loadStrategy() {
this.strategy = await strategyProvider.getCurrentStrategy({
symbol: this.symbol,
userId: this.user.id,
positionSide: this.positionSide,
})
if (!(this.strategy && this.strategy.id)) {
this.strategy = await strategyProvider.create({
symbol: this.symbol,
userId: this.user.id,
positionSide: this.positionSide,
status: STRATEGY.STATUS.WAIT_ENTRY_POINT,
})
}
}
}
Итак мы написали основу трейд бота. В следующей статье реалзиуем первую стратегию. И посмотрим в кабинете binance как бот будет создавать и закрывать ордера в онлайн режиме
Текущий исходный код