diff --git a/article/apiqueue.webp b/article/apiqueue.webp deleted file mode 100644 index 7579ea5..0000000 Binary files a/article/apiqueue.webp and /dev/null differ diff --git a/config/config.json b/config/config.json index 5460561..2af4509 100644 --- a/config/config.json +++ b/config/config.json @@ -2,21 +2,21 @@ "development": { "username": "root", "password": "example", - "database": "bot", + "database": "tradebot", "host": "127.0.0.1", "dialect": "postgres" }, "test": { "username": "root", "password": "example", - "database": "bot", + "database": "tradebot", "host": "127.0.0.1", "dialect": "postgres" }, "production": { "username": "root", "password": "example", - "database": "bot", + "database": "tradebot", "host": "127.0.0.1", "dialect": "postgres" } diff --git a/constant.js b/constant.js index 0e59bcb..8fc2a9c 100644 --- a/constant.js +++ b/constant.js @@ -67,8 +67,19 @@ const SYMBOL = { ETHUSDT: 'ETHUSDT', BTCUSDT: 'BTCUSDT', } + + +const STRATEGY = { + STATUS:{ + WAIT_ENTRY_POINT:'WAIT_ENTRY_POINT', + IN_PROGRESS:'IN_PROGRESS', + COMPLETED:'COMPLETED' + } +} + module.exports = { ORDER, K_LINE, SYMBOL, + STRATEGY } diff --git a/core/index.js b/core/index.js deleted file mode 100644 index 09c65e0..0000000 --- a/core/index.js +++ /dev/null @@ -1,17 +0,0 @@ -const path = require('path') -const initConfig = require('./config') -const initDB = require('./db') - -const ROOT_PATH = process.cwd(); - -(async function () { - const config = await initConfig(path.resolve(ROOT_PATH, 'config')) - - const db = await initDB() - application.services = { db, config } - - - await application.start(true) - - logger.info('Application started') -})() diff --git a/db/index.js b/db/index.js index 5f01e51..66e1ed4 100644 --- a/db/index.js +++ b/db/index.js @@ -1,6 +1,7 @@ const defineModels = require('../models'); const defineProviders = require('../providers'); const { Sequelize } = require('sequelize'); +const config = require('../config'); async function initDB(config) { const sequelize = new Sequelize({ @@ -26,4 +27,4 @@ async function initDB(config) { }); } -module.exports = initDB; +module.exports = initDB(config); diff --git a/docker-compose.yml b/docker-compose.yml index 7243888..e6afcdd 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -9,7 +9,7 @@ services: environment: POSTGRES_USER: root POSTGRES_PASSWORD: example - POSTGRES_DB: bot + POSTGRES_DB: tradebot ports: - 5432:5432 networks: diff --git a/helpers/asynQueue.js b/helpers/asynQueue.js new file mode 100644 index 0000000..2266fbc --- /dev/null +++ b/helpers/asynQueue.js @@ -0,0 +1,42 @@ +class AsyncQueue { + constructor(options = {}) { + const { log, error } = options; + this.log = log || console.log; + this.error = error || console.error; + this.queue = []; + this.isRunning = false; + } + + addToQueue(func, data) { + this.queue.push({ func, data }); + } + + emitInQueue = (func, data) => { + this.addToQueue(func, data); + this.runQueue(); + }; + + runQueue() { + if (this.isRunning) { + return; + } + this.isRunning = true; + this.runNext().catch((e) => this.error('runQueue error', e)); + } + + async runNext() { + if (this.queue.length === 0) { + this.isRunning = false; + return; + } + const { func, data } = this.queue.splice(0, 1)[0]; + if (typeof func === 'function') { + await func(data); + } else { + this.log('function in queue not found', { func, data }); + } + await this.runNext(); + } +} + +module.exports = AsyncQueue; diff --git a/index.js b/index.js index e69de29..9c13067 100644 --- a/index.js +++ b/index.js @@ -0,0 +1,17 @@ +const path = require('path') +const initConfig = require('./config') +const initDB = require('./db') + +const ROOT_PATH = process.cwd(); + +(async function () { + const config = await initConfig(path.resolve(ROOT_PATH, 'config')) + + const db = await initDB() + application.services = { db, config } + + + await application.start(true) + + logger.info('Application started') +})() diff --git a/migrations/20210602202235-order.js b/migrations/20210602202235-order.js index b08760d..c4850f6 100644 --- a/migrations/20210602202235-order.js +++ b/migrations/20210602202235-order.js @@ -39,23 +39,23 @@ module.exports = { allowNull: true }, price: { - type: Sequelize.REAL, // ??? + type: Sequelize.REAL, allowNull: true }, avg_price: { - type: Sequelize.REAL, // ??? + type: Sequelize.REAL, allowNull: true }, activate_price: { - type: Sequelize.REAL, // ??? + type: Sequelize.REAL, allowNull: true }, price_rate: { - type: Sequelize.REAL, // ??? + type: Sequelize.REAL, allowNull: true }, stop_price: { - type: Sequelize.REAL, // ??? + type: Sequelize.REAL, allowNull: true }, side: { @@ -83,7 +83,7 @@ module.exports = { allowNull: true }, update_time: { - type: Sequelize.STRING, // ??? DATE + type: Sequelize.STRING, allowNull: true }, working_type: { @@ -94,19 +94,6 @@ module.exports = { type: Sequelize.JSONB, allowNull: true }, - created_at: { - type: Sequelize.DATE, - allowNull: true, - defaultValue: Sequelize.literal('CURRENT_TIMESTAMP') - }, - updated_at: { - type: Sequelize.DATE, - allowNull: true - }, - deleted_at: { - type: Sequelize.DATE, - allowNull: true - }, commission: { type: Sequelize.REAL, allowNull: true diff --git a/migrations/20210703122401-strategy.js b/migrations/20210703122401-strategy.js new file mode 100644 index 0000000..de54e63 --- /dev/null +++ b/migrations/20210703122401-strategy.js @@ -0,0 +1,68 @@ +'use strict'; + +module.exports = { + up: async (queryInterface, Sequelize) => { + await queryInterface.createTable('strategy', { + id: { + autoIncrement: true, + type: Sequelize.BIGINT, + allowNull: false, + primaryKey: true + }, + status: { + type: Sequelize.STRING, + allowNull: false + }, + profit: { + type: Sequelize.REAL, + allowNull: true + }, + position_side: { + type: Sequelize.STRING, + allowNull: true + }, + symbol: { + type: Sequelize.STRING, + allowNull: true + }, + type: { + type: Sequelize.STRING, + allowNull: true + }, + options:{ + type: Sequelize.JSONB, + allowNull: true, + }, + user_id: { + type: Sequelize.BIGINT, + allowNull: false, + references: { + model: { + tableName: 'user' + }, + key: 'id' + }, + onUpdate: 'cascade', + onDelete: 'cascade' + }, + createdAt: { + type: Sequelize.DATE, + allowNull: false, + defaultValue: Sequelize.literal('CURRENT_TIMESTAMP'), + }, + updatedAt: { + type: Sequelize.DATE, + allowNull: false, + defaultValue: Sequelize.literal('CURRENT_TIMESTAMP'), + }, + deletedAt: { + type: Sequelize.DATE, + allowNull: true, + }, + }) + }, + + down: (queryInterface) => { + return queryInterface.dropTable('strategy'); + } +}; diff --git a/models/index.js b/models/index.js index 6cad100..9eb4c63 100644 --- a/models/index.js +++ b/models/index.js @@ -1,6 +1,7 @@ 'use strict' const user = require('./user') const order = require('./order') +const strategy = require('./strategy') /** * @@ -11,6 +12,7 @@ function defineModels(sequelize) { [ order(sequelize), user(sequelize), + strategy(sequelize), ].map((model) => { if (model.associate) { model.associate(sequelize.models) diff --git a/models/strategy.js b/models/strategy.js new file mode 100644 index 0000000..f5469f2 --- /dev/null +++ b/models/strategy.js @@ -0,0 +1,66 @@ +const { Model, DataTypes } = require('sequelize') + +class Strategy extends Model { + static associate(models) { + Strategy.hasMany(models.Order, { + foreignKey: 'strategyId', + sourceKey: 'id', + as: 'orders' + }); + } +} +module.exports = function (sequelize) { + return Strategy.init( + { + id: { + autoIncrement: true, + type: DataTypes.BIGINT, + allowNull: false, + primaryKey: true + }, + status: { + type: DataTypes.STRING, + allowNull: false + }, + profit: { + type: DataTypes.REAL, + allowNull: true + }, + positionSide: { + type: DataTypes.STRING, + allowNull: true + }, + symbol: { + type: DataTypes.STRING, + allowNull: true + }, + type: { + type: DataTypes.STRING, + allowNull: true + }, + options:{ + type: DataTypes.JSONB, + allowNull: true, + }, + userId: { + type: DataTypes.BIGINT, + allowNull: false, + references: { + model: { + tableName: 'user' + }, + key: 'id' + }, + onUpdate: 'cascade', + onDelete: 'cascade' + }, + }, + { + sequelize, + tableName: 'strategy', + timestamps: true, + }, + ) +} + +module.exports.Strategy = Strategy diff --git a/models/user.js b/models/user.js index 2f97fcd..9b1c067 100644 --- a/models/user.js +++ b/models/user.js @@ -19,11 +19,11 @@ module.exports = function (sequelize) { type: DataTypes.STRING, allowNull: false }, - binance_api_key: { + binanceApiKey: { type: DataTypes.STRING, allowNull: true }, - binance_api_secret: { + binanceApiSecret: { type: DataTypes.STRING, allowNull: true }, diff --git a/package.json b/package.json index d92f61f..35b82bb 100644 --- a/package.json +++ b/package.json @@ -8,6 +8,7 @@ "migrate": "node_modules/sequelize-cli/lib/sequelize db:migrate", "migrate:undo": "sequelize-cli db:migrate:undo", "migrate:undo:all": "sequelize-cli db:migrate:undo:all", + "migrate:add": "sequelize-cli migration:generate --name x", "seed": "sequelize-cli db:seed:all", "seed:undo": "sequelize-cli db:seed:undo", "seed:undo:all": "sequelize-cli db:seed:undo:all" diff --git a/providers/BaseProvider.js b/providers/BaseProvider.js index edcb0ca..7596ca0 100644 --- a/providers/BaseProvider.js +++ b/providers/BaseProvider.js @@ -1,7 +1,4 @@ -const { InstanceError, Model, Op } = require('sequelize'); -const { ApiServerError, UNIQUE_NAME_ERR_CODE } = require('../../../core/errors'); -const { prepareOrderDirection } = require('../../../utils/helpers'); -const { prepareSqlKey, operatorsMap } = require('../../../utils/query'); +const { InstanceError, Model, Op } = require('sequelize') /** * @class BaseProvider @@ -10,316 +7,88 @@ const { prepareSqlKey, operatorsMap } = require('../../../utils/query'); */ class BaseProvider { constructor(sequelize, logger) { - this.sequelize = sequelize; - this.logger = logger; - this.Op = Op; + this.sequelize = sequelize + this.logger = logger + this.Op = Op } get Model() { // [required] mast be overwritten in child model - throw Error('Model mast be overwritten provider'); + throw Error('Model mast be overwritten provider') } get includes() { - return {}; + return {} } get defaultOptions() { - return {}; + return {} } async transaction(options) { - return this.sequelize.transaction(options); - } - - withScope(model, scope) { - if (scope) { - return model.scope(scope.name, scope.data); - } - return model; - } - - applyFilter(builder, filter, op) { - if (filter && Array.isArray(filter) && filter.length > 0) { - builder.where = { - ...builder.where, - [op]: [] - }; - for (const [key, v] of filter) { - if (typeof key === 'string' && (typeof v === 'string' || typeof v === 'number' || typeof v === 'boolean')) { - builder.where[op].push({ [key]: v }); - } - if (typeof key === 'string' && typeof v === 'object') { - if (v['$eq'] === null) { - builder.where[op].push({ [key]: { [this.Op.eq]: null } }); - } - if ( - v['$ne'] === null || - typeof v['$ne'] === 'string' || - typeof v['$ne'] === 'number' || - typeof v['$ne'] === 'boolean' - ) { - builder.where[op].push({ [key]: { [this.Op.ne]: v['$ne'] } }); - } - if (v['$gte']) { - builder.where[op].push({ [key]: { [this.Op.gte]: v['$gte'] } }); - } - if (v['$lte']) { - builder.where[op].push({ [key]: { [this.Op.lte]: v['$lte'] } }); - } - if (v['$gt']) { - builder.where[op].push({ [key]: { [this.Op.gt]: v['$gt'] } }); - } - if (v['$lt']) { - builder.where[op].push({ [key]: { [this.Op.lt]: v['$lt'] } }); - } - if (v['$in'] && Array.isArray(v['$in']) && v['$in'].length > 0) { - builder.where[op].push({ [key]: { [this.Op.in]: v['$in'] } }); - } - if (v['$notIn'] && Array.isArray(v['$notIn']) && v['$notIn'].length > 0) { - builder.where[op].push({ [key]: { [this.Op.notIn]: v['$notIn'] } }); - } - if (v['$ilike'] && typeof v['$ilike'] === 'string') { - builder.where[op].push({ [key]: { [this.Op.iLike]: v['$ilike'] } }); - } - - if (v['$like'] && typeof v['$like'] === 'string') { - builder.where[op].push({ - [this.Op.col]: this.sequelize.where(this.sequelize.fn('lower', this.sequelize.col(key)), { - [this.Op.like]: v['$like'].toLowerCase() - }) - }); - } - if (v['$ilikeCast'] && typeof v['$ilikeCast'] === 'string' && v['cast'] && typeof v['cast'] === 'string') { - builder.where[op].push( - this.sequelize.where(this.sequelize.cast(this.sequelize.col(key), v['cast']), { - [Op.iLike]: v['$ilikeCast'] - }) - ); - } - if (v['$ilikeNum'] && typeof v['$ilikeNum'] === 'string') { - const col = this.sequelize.cast(this.sequelize.col(key), 'text'); - builder.where[op].push(this.sequelize.where(col, { [this.Op.iLike]: v['$ilikeNum'] })); - } - } - } - } - return builder; - } - - applyFilterNaked(builder, filter, op) { - if (!Array.isArray(filter)) return builder; - const where = []; - - filter.forEach(([key, val]) => { - if (typeof key !== 'string') return; - const sqlKey = prepareSqlKey({ - key, - tableAlias: this.Model.getTableName(), - attributes: this.Model.rawAttributes - }); - - if (typeof val === 'string' || typeof val === 'number') { - where.push(`${sqlKey}=${this.sequelize.escape(val)}`); - } else if (typeof val === 'object') { - const opKey = Object.keys(val)[0]; - if (!operatorsMap[opKey]) return; - - where.push( - `${sqlKey} ${operatorsMap[opKey]} ${ - Array.isArray(val[opKey]) - ? `(${val[opKey].map((v) => this.sequelize.escape(v)).join(',')})` - : this.sequelize.escape(val[opKey]) - }` - ); - } - }); - - if (where.length > 0) { - builder.where = `${builder.where ? `${builder.where} AND ` : ''}(${where.join( - op === this.Op.and ? ' AND ' : ' OR ' - )})`; - } - - return builder; - } - - /** - * @param {*} data - * @param {boolean} withCount - * @param {*} [options] - * @return {Promise<{items: Model[], total: number}>|Promise} - */ - getFilteredList(data, withCount = false, options = undefined) { - const query = this.getFilteredQuery(data); - return this.getList(Object.assign({}, options, query), withCount); - } - - getOrderParams(order = []) { - if (!Array.isArray(order)) return undefined; - - return order.map((orderPair) => { - // example 'name' - if (!Array.isArray(orderPair)) return orderPair; - - const [orderBy, orderDir] = orderPair; - const orderDirection = prepareOrderDirection(orderDir); - - // order by associations, example [['user', 'name'], 'asc'] - if (Array.isArray(orderBy)) { - const [model, modelField] = orderBy; - return [this.Model.associations[model] || model, modelField, orderDirection]; - } - - // example ['name', 'desc'] || ['type.name', 'desc'] - if (typeof orderBy === 'string') { - return this.sequelize.literal(`"${orderBy}" ${orderDirection}`); - } - - // all others - return [orderBy, orderDirection]; - }); - } - - getOrderParamsNaked(order = []) { - if (!Array.isArray(order)) return undefined; - - return order - .map((orderPair) => { - // example 'name' - if (!Array.isArray(orderPair)) return `"${orderPair}"`; - - const [orderBy, orderDir] = orderPair; - const orderDirection = prepareOrderDirection(orderDir); - - // order by associations, example [['user', 'name'], 'asc'] - if (Array.isArray(orderBy)) { - const [model, modelField] = orderBy; - return `"${model}"."${modelField}" ${orderDirection}`; - } - - return `"${orderBy}" ${orderDirection}`; - }) - .join(', '); - } - - getFilteredQuery(data = {}) { - let builder = {}; - builder = this.applyFilter(builder, data.filter, this.Op.and); - builder = this.applyFilter(builder, data.orFilter, this.Op.or); - - if (data.limit && typeof data.limit === 'number') { - builder.limit = data.limit; - } - if (data.offset && typeof data.offset === 'number') { - builder.offset = data.offset; - } - if (data.order && Array.isArray(data.order)) { - builder.order = this.getOrderParams(data.order); - } - - return builder; - } - - getFilteredQueryNaked(data = {}) { - let builder = {}; - builder = this.applyFilterNaked(builder, data.filter, this.Op.and); - builder = this.applyFilterNaked(builder, data.orFilter, this.Op.or); - - if (data.limit && typeof data.limit === 'number') { - builder.limit = data.limit; - } - if (data.offset && typeof data.offset === 'number') builder.offset = data.offset; - - if (data.order && Array.isArray(data.order)) { - builder.order = this.getOrderParamsNaked(data.order); - } - - return builder; - } - getList(query, withCount = false) { - const findOptions = Object.assign({}, this.defaultOptions, query); - if (withCount) { - return this.Model.findAndCountAll(findOptions).then((res) => { - return { - total: res.count, - items: res.rows - }; - }); - } - return this.Model.findAll(findOptions); + return this.sequelize.transaction(options) } getCount(query) { - return this.Model.count(query).catch(() => 0); + return this.Model.count(query).catch(() => 0) } get models() { - return this.sequelize.models; + return this.sequelize.models } async create(data, options = undefined) { - return this.Model.create(data, { ...options }); + return this.Model.create(data, { ...options }) } async update(where, data, options = undefined) { - const res = await this.Model.unscoped().update(data, { where, ...options }); + const res = await this.Model.unscoped().update(data, { where, ...options }) - return Boolean(res && res[0]); + return Boolean(res && res[0]) } async updateById(id, data, options = undefined) { - return this.update({ id }, data, options); + return this.update({ id }, data, options) } async updateModel(model, values, options = undefined) { return model instanceof Model ? model.update(values, options) - : Promise.reject(new InstanceError('It mast be instance of Model')); + : Promise.reject(new InstanceError('It mast be instance of Model')) } async delete(where, options = undefined) { - return await this.Model.unscoped().destroy({ where, ...options }); + return await this.Model.unscoped().destroy({ where, ...options }) } async deleteById(id, options = undefined) { - return this.delete({ id }, options); + return this.delete({ id }, options) } async deleteModel(model, options = undefined) { return model instanceof Model ? model.destroy(options) - : Promise.reject(new InstanceError('It mast be instance of Model')); + : Promise.reject(new InstanceError('It mast be instance of Model')) } async isExist(where, options = undefined) { return this.Model.count({ where, ...options }).then( (value) => Boolean(value), - () => false - ); - } - - async uniqueNameCheck({ name, id }, entity = 'Item', key = 'name') { - const item = await this.Model.findOne({ where: { [key]: name, deletedAt: null } }); - if (item && Number(item.id) !== Number(id)) - throw new ApiServerError(`${entity} with the current name already exist.`, UNIQUE_NAME_ERR_CODE, item); + () => false, + ) } async getById(id, options = undefined) { - return this.Model.unscoped().findByPk(id, { ...this.defaultOptions, ...options }); + return this.Model.unscoped().findByPk(id, { ...this.defaultOptions, ...options }) } async findOne(where = {}, options = undefined) { - return this.Model.unscoped().findOne({ ...this.defaultOptions, ...options, where }); + return this.Model.unscoped().findOne({ ...this.defaultOptions, ...options, where }) } async getModel(idOrModel, ...args) { - return idOrModel instanceof Model ? idOrModel : this.getById(idOrModel, ...args); - } - - async total(data) { - return this.getCount(this.getFilteredQuery(data)).then((total) => ({ total })); + return idOrModel instanceof Model ? idOrModel : this.getById(idOrModel, ...args) } } -module.exports = BaseProvider; +module.exports = BaseProvider diff --git a/providers/OrderProvider.js b/providers/OrderProvider.js index 1e316c1..f3eb453 100644 --- a/providers/OrderProvider.js +++ b/providers/OrderProvider.js @@ -1,13 +1,52 @@ -const EventEmitter = require('events').EventEmitter const LoggerService = require('../services/LoggerService') const BaseProvider = require('./BaseProvider') -class OrderProvider extends BaseProvider{ - constructor() { - const { log, error } = new LoggerService('DBOrderService') +class OrderProvider extends BaseProvider { + constructor(...p) { + super(...p) + const { log, error } = new LoggerService('DBOrderProvider') this.log = log this.error = error } + + get Model() { + return this.models.Order + } + + getOrderUnique(type, orderId) { + return `${type}_${orderId}` + } + + async getOrder(order, filter) { + let params = { + unique: this.getOrderUnique(order.originalOrderType || order.type, order.orderId), + } + if (filter) { + params = { + ...params, + ...filter, + } + } + return this.findOne(params) + } + + createOrder(order) { + return this.create({ + ...order, + orderId: String(order.orderId), + origQty: order.origQty || order.originalQuantity, + price: order.price, + avgPrice: order.averagePrice, + unique: this.getOrderUnique(order.originalOrderType || order.type, order.orderId), + }) + } + async updateOrderCommission(o, trade) { + if (trade.commission && Number(trade.commission) > 0) { + o.commission = trade.commission + await o.save() + } + } + } module.exports = OrderProvider diff --git a/providers/StrategyProvider.js b/providers/StrategyProvider.js new file mode 100644 index 0000000..7950421 --- /dev/null +++ b/providers/StrategyProvider.js @@ -0,0 +1,35 @@ +const LoggerService = require('../services/LoggerService') +const BaseProvider = require('./BaseProvider') +const { STRATEGY } = require('../config') + +class StrategyProvider extends BaseProvider { + constructor(...p) { + super(...p) + const { log, error } = new LoggerService('DBStrategyProvider') + this.log = log + this.error = error + } + get includes() { + return { + orders: { model: this.models.Order, as: 'orders'} + }; + } + + get Model() { + return this.models.Strategy + } + + getCurrentStrategy({ symbol, userId, positionSide }) { + return this.findOne({ + positionSide, + symbol, + userId, + status: { [this.Op.in]: [STRATEGY.STATUS.WAIT_ENTRY_POINT, STRATEGY.STATUS.WAIT_ENTRY_POINT] }, + },{ + includes:[this.includes.orders] + }) + } + +} + +module.exports = StrategyProvider diff --git a/providers/UserProvider.js b/providers/UserProvider.js new file mode 100644 index 0000000..eb17b6c --- /dev/null +++ b/providers/UserProvider.js @@ -0,0 +1,18 @@ +const LoggerService = require('../services/LoggerService') +const BaseProvider = require('./BaseProvider') + +class UserProvider extends BaseProvider { + constructor(...p) { + super(...p) + const { log, error } = new LoggerService('DBUserProvider') + this.log = log + this.error = error + } + + get Model() { + return this.models.User + } + +} + +module.exports = UserProvider diff --git a/providers/index.js b/providers/index.js index 917d945..2bbc24b 100644 --- a/providers/index.js +++ b/providers/index.js @@ -1,6 +1,7 @@ const BaseProvider = require('./base') -const UserProvider = require('./user') -const OrderProvider = require('./order') +const UserProvider = require('./UserProvider') +const OrderProvider = require('./OrderProvider') +const StrategyProvider = require('./StrategyProvider') /** * @param {Sequelize} sequelize @@ -12,6 +13,7 @@ async function defineProviders(sequelize) { baseProvider: new BaseProvider(sequelize), userProvider: new UserProvider(sequelize), orderProvider: new OrderProvider(sequelize), + strategyProvider: new StrategyProvider(sequelize), } } diff --git a/services/StrategyService.js b/services/StrategyService.js new file mode 100644 index 0000000..8d8c4d8 --- /dev/null +++ b/services/StrategyService.js @@ -0,0 +1,89 @@ +const { STRATEGY, ORDER } = require('../constant') +const { strategyProvider, orderProvider } = require('../db') +const BaseApiService = require('./BaseApiService') + +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, + }) + } + } + +} + +module.exports = Strategy + + + diff --git a/services/TradeService.js b/services/TradeService.js index 04520b2..ec70d0c 100644 --- a/services/TradeService.js +++ b/services/TradeService.js @@ -1,28 +1,44 @@ const NodeBinanceApi = require('node-binance-api') -const EventEmitter = require('events').EventEmitter const LoggerService = require('./LoggerService') +const { orderProvider } = require('../db') +const AsyncQueue = require('../helpers/asynQueue') -class TradeService { - constructor({client, secret}) { - const { log, error } = new LoggerService('WS') +class CheckOrderService { + constructor({ binanceApiKey, binanceApiSecret }) { + const { log, error } = new LoggerService('CheckOrderService') this.log = log this.error = error this.api = new NodeBinanceApi().options({ - APIKEY: client, - APISECRET: secret, + APIKEY: binanceApiKey, + APISECRET: binanceApiSecret, hedgeMode: true, }) - this.events = new EventEmitter() + this.asyncQueue = new AsyncQueue() } - marginCallCallback = (data) => this.log('marginCallCallback', data) - - accountUpdateCallback = (data) => this.emitPositions(data.updateData.positions) - - orderUpdateCallback = (data) => this.emit(data) + 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.log('executionTRADE error', e) + } + } + } + orderUpdateCallback = (data) => { + this.asyncQueue.emitInQueue(this.checkOrder, data) + } + marginCallCallback = (data) => this.log('marginCallCallback', data) + accountUpdateCallback = (data) => this.log('accountUpdateCallback', data) subscribedCallback = (data) => this.log('subscribedCallback', data) - accountConfigUpdateCallback = (data) => this.log('accountConfigUpdateCallback', data) startListening() { @@ -34,14 +50,6 @@ class TradeService { this.accountConfigUpdateCallback, ) } - - subscribe(cb) { - this.events.on('trade', cb) - } - - emit = (data) => { - this.events.emit('trade', data) - } } -module.exports = TradeService +module.exports = CheckOrderService diff --git a/services/UserFuturesService.js b/services/UserFuturesService.js deleted file mode 100644 index 236c7a3..0000000 --- a/services/UserFuturesService.js +++ /dev/null @@ -1,105 +0,0 @@ -const { ORDER } = require('../helpers/binance/constants') -const FutureOrder = require('../models/futureOrder') -const WS = require('./WS') -const BaseApiService = require('./BaseApiService') - -class UserFuturesService extends BaseApiService { - - constructor(user) { - super(user.bClient, user.bSecret) - this.logPrefix = user.name - this.user = user - this.queue = [] - this.isRunning = false - this.positions = [] - this.orderModel = FutureOrder - this.ws = new WS(this.user.bClient, this.user.bSecret) - this.ws.subscribe(this.onTrade) - this.ws.subscribePositions(this.onPositions) - this.ws.startListening() - this.getActivePositions().then(positions => this.positions = positions) - } - - async executionNEW(data) { - try { - await this.createDbOrder(data.order) - } catch (e) { - this.log('order error', e) - } - } - - async executionTRADE(data) { - try { - const { order } = data - console.log('trade', data) - console.log('this.orderModel',this.orderModel) - if (order && order.orderId && order.orderStatus && order.executionType === 'TRADE') { - const o = await this.getOrder(order) - if (o && o.status !== order.orderStatus) { - o.status = order.orderStatus - console.log(`order ${order.orderId} status updated ${order.orderStatus}`) - await this.updateOrderTrade(o, order) - } - } - } catch (e) { - this.log('executionTRADE error', e) - } - } - - async executionEXPIRED(data) { - return this.executionTRADE(data) - } - - async executionCANCELED(data) { - return this.executionTRADE(data) - } - - async execute(data) { - // this.log('execute', { trader, data }) - if (data && data.order) { - const { executionType } = data.order - if (typeof this[`execution${executionType}`] === 'function') { - try { - this.log(`execution${executionType}`) - await this[`execution${executionType}`](data) - } catch (e) { - this.log(`error execution${executionType}`, e) - } - } - } - } - - addToQueue(data) { - this.queue.push(data) - } - - runQueue() { - if (this.isRunning) { - return - } - this.isRunning = true - this.runNext().catch(e => this.log('runQueue error', e)) - } - - async runNext() { - if (this.queue.length === 0) { - this.isRunning = false - return - } - const data = this.queue.splice(0, 1)[0] - await this.execute(data) - await this.runNext() - - } - - onTrade = async (data) => { - // this.log('add to queue', data.data.order.orderType) - this.addToQueue(data) - this.runQueue() - } - onPositions = (data) => { - this.positions = data - } -} - -module.exports = UserFuturesService diff --git a/services/UserService.js b/services/UserService.js new file mode 100644 index 0000000..55ca7ac --- /dev/null +++ b/services/UserService.js @@ -0,0 +1,26 @@ +const FutureOrder = require('../models/futureOrder') +const NodeBinanceApi = require('node-binance-api') +const TradeService = require('./TradeService') +const { orderProvider, userProvider } = require('../db') + +class UserService { + + constructor(user) { + this.user = user + this.positions = [] + this.orderModel = FutureOrder + this.tradeService = new TradeService(user) + + this.api = new NodeBinanceApi().options({ + APIKEY: user.binanceApiKey, + APISECRET: user.binanceApiSecret, + hedgeMode: true, + }) + } + + init = async () => { + this.exchangeInfo = await this.api.futuresExchangeInfo() + } +} + +module.exports = UserService