diff --git a/test/test-cases/additional-api-sync-mode-sqlite-test-cases.js b/test/test-cases/additional-api-sync-mode-sqlite-test-cases.js index d692479aa..8f8153d9d 100644 --- a/test/test-cases/additional-api-sync-mode-sqlite-test-cases.js +++ b/test/test-cases/additional-api-sync-mode-sqlite-test-cases.js @@ -554,6 +554,50 @@ module.exports = ( } }) + it('it should be successfully performed by the getWeightedAveragesReport method', async function () { + this.timeout(120000) + + const paramsArr = [ + { end, start }, + { + end, + start: end - (10 * 60 * 60 * 1000), + symbol: ['tBTCUSD'] + } + ] + + for (const params of paramsArr) { + const res = await agent + .post(`${basePath}/json-rpc`) + .type('json') + .send({ + auth, + method: 'getWeightedAveragesReport', + params, + id: 5 + }) + .expect('Content-Type', /json/) + .expect(200) + + assert.isObject(res.body) + assert.propertyVal(res.body, 'id', 5) + assert.isArray(res.body.result) + + const resItem = res.body.result[0] + + assert.isObject(resItem) + assert.containsAllKeys(resItem, [ + 'symbol', + 'buyingWeightedPrice', + 'buyingAmount', + 'sellingWeightedPrice', + 'sellingAmount', + 'cumulativeWeightedPrice', + 'cumulativeAmount' + ]) + } + }) + it('it should be successfully performed by the getMultipleCsv method', async function () { this.timeout(60000) @@ -939,4 +983,29 @@ module.exports = ( await testMethodOfGettingCsv(procPromise, aggrPromise, res) }) + + it('it should be successfully performed by the getWeightedAveragesReportCsv method', async function () { + this.timeout(60000) + + const procPromise = queueToPromise(params.processorQueue) + const aggrPromise = queueToPromise(params.aggregatorQueue) + + const res = await agent + .post(`${basePath}/json-rpc`) + .type('json') + .send({ + auth, + method: 'getWeightedAveragesReportCsv', + params: { + end, + start, + email + }, + id: 5 + }) + .expect('Content-Type', /json/) + .expect(200) + + await testMethodOfGettingCsv(procPromise, aggrPromise, res) + }) } diff --git a/workers/loc.api/di/app.deps.js b/workers/loc.api/di/app.deps.js index 0517e2d87..eebec41dd 100644 --- a/workers/loc.api/di/app.deps.js +++ b/workers/loc.api/di/app.deps.js @@ -90,9 +90,11 @@ const CurrencyConverter = require('../sync/currency.converter') const CsvJobData = require('../generate-csv/csv.job.data') const { fullSnapshotReportCsvWriter, - fullTaxReportCsvWriter + fullTaxReportCsvWriter, + weightedAveragesReportCsvWriter } = require('../generate-csv/csv-writer') const FullTaxReport = require('../sync/full.tax.report') +const WeightedAveragesReport = require('../sync/weighted.averages.report') const SqliteDbMigrator = require( '../sync/dao/db-migrations/sqlite.db.migrator' ) @@ -152,6 +154,7 @@ module.exports = ({ ['_syncCollsManager', TYPES.SyncCollsManager], ['_dataConsistencyChecker', TYPES.DataConsistencyChecker], ['_winLossVSAccountBalance', TYPES.WinLossVSAccountBalance], + ['_weightedAveragesReport', TYPES.WeightedAveragesReport], ['_getDataFromApi', TYPES.GetDataFromApi] ] }) @@ -358,8 +361,20 @@ module.exports = ({ ] ) ) + bind(TYPES.WeightedAveragesReportCsvWriter) + .toConstantValue( + bindDepsToFn( + weightedAveragesReportCsvWriter, + [ + TYPES.RService, + TYPES.GetDataFromApi + ] + ) + ) bind(TYPES.FullTaxReport) .to(FullTaxReport) + bind(TYPES.WeightedAveragesReport) + .to(WeightedAveragesReport) rebind(TYPES.CsvJobData) .to(CsvJobData) .inSingletonScope() diff --git a/workers/loc.api/di/types.js b/workers/loc.api/di/types.js index 06eefc171..b970ec5ba 100644 --- a/workers/loc.api/di/types.js +++ b/workers/loc.api/di/types.js @@ -66,5 +66,7 @@ module.exports = { SyncTempTablesManager: Symbol.for('SyncTempTablesManager'), SyncUserStepManager: Symbol.for('SyncUserStepManager'), SyncUserStepData: Symbol.for('SyncUserStepData'), - SyncUserStepDataFactory: Symbol.for('SyncUserStepDataFactory') + SyncUserStepDataFactory: Symbol.for('SyncUserStepDataFactory'), + WeightedAveragesReport: Symbol.for('WeightedAveragesReport'), + WeightedAveragesReportCsvWriter: Symbol.for('WeightedAveragesReportCsvWriter') } diff --git a/workers/loc.api/generate-csv/csv-writer/index.js b/workers/loc.api/generate-csv/csv-writer/index.js index 1ef83bf4f..f9a8660f5 100644 --- a/workers/loc.api/generate-csv/csv-writer/index.js +++ b/workers/loc.api/generate-csv/csv-writer/index.js @@ -6,8 +6,12 @@ const fullSnapshotReportCsvWriter = require( const fullTaxReportCsvWriter = require( './full-tax-report-csv-writer' ) +const weightedAveragesReportCsvWriter = require( + './weighted-averages-report-csv-writer' +) module.exports = { fullSnapshotReportCsvWriter, - fullTaxReportCsvWriter + fullTaxReportCsvWriter, + weightedAveragesReportCsvWriter } diff --git a/workers/loc.api/generate-csv/csv-writer/weighted-averages-report-csv-writer.js b/workers/loc.api/generate-csv/csv-writer/weighted-averages-report-csv-writer.js new file mode 100644 index 000000000..e620ee95c --- /dev/null +++ b/workers/loc.api/generate-csv/csv-writer/weighted-averages-report-csv-writer.js @@ -0,0 +1,77 @@ +'use strict' + +const { pipeline } = require('stream') +const { stringify } = require('csv') + +const { + write +} = require('bfx-report/workers/loc.api/queue/write-data-to-stream/helpers') + +const nope = () => {} + +module.exports = ( + rService, + getDataFromApi +) => async ( + wStream, + jobData +) => { + const queue = rService.ctx.lokue_aggregator.q + const { + args, + columnsCsv, + formatSettings, + name + } = jobData ?? {} + const { params } = args ?? {} + + queue.emit('progress', 0) + + if (typeof jobData === 'string') { + const stringifier = stringify( + { columns: ['mess'] } + ) + + pipeline(stringifier, wStream, nope) + write([{ mess: jobData }], stringifier) + queue.emit('progress', 100) + stringifier.end() + + return + } + + wStream.setMaxListeners(50) + + const headerStringifier = stringify( + { columns: ['empty', 'buy', 'empty', 'sell', 'empty', 'cumulative', 'empty'] } + ) + const resStringifier = stringify({ + header: true, + columns: columnsCsv + }) + + pipeline(headerStringifier, wStream, nope) + pipeline(resStringifier, wStream, nope) + + const res = await getDataFromApi({ + getData: rService[name].bind(rService), + args, + callerName: 'CSV_WRITER' + }) + + write( + [{ empty: '', buy: 'Buy', sell: 'Sell', cumulative: 'Cumulative' }], + headerStringifier + ) + write( + res, + resStringifier, + formatSettings, + params + ) + + queue.emit('progress', 100) + + headerStringifier.end() + resStringifier.end() +} diff --git a/workers/loc.api/generate-csv/csv.job.data.js b/workers/loc.api/generate-csv/csv.job.data.js index c769bb234..a4128fd1f 100644 --- a/workers/loc.api/generate-csv/csv.job.data.js +++ b/workers/loc.api/generate-csv/csv.job.data.js @@ -20,18 +20,21 @@ const { decorateInjectable } = require('../di/utils') const depsTypes = (TYPES) => [ TYPES.RService, TYPES.FullSnapshotReportCsvWriter, - TYPES.FullTaxReportCsvWriter + TYPES.FullTaxReportCsvWriter, + TYPES.WeightedAveragesReportCsvWriter ] class CsvJobData extends BaseCsvJobData { constructor ( rService, fullSnapshotReportCsvWriter, - fullTaxReportCsvWriter + fullTaxReportCsvWriter, + weightedAveragesReportCsvWriter ) { super(rService) this.fullSnapshotReportCsvWriter = fullSnapshotReportCsvWriter this.fullTaxReportCsvWriter = fullTaxReportCsvWriter + this.weightedAveragesReportCsvWriter = weightedAveragesReportCsvWriter } _addColumnsBySchema (columnsCsv = {}, schema = {}) { @@ -667,6 +670,48 @@ class CsvJobData extends BaseCsvJobData { return jobData } + + async getWeightedAveragesReportCsvJobData ( + args, + uId, + uInfo + ) { + checkParams(args, 'paramsSchemaForWeightedAveragesReportApiCsv') + + const { + userId, + userInfo + } = await checkJobAndGetUserData( + this.rService, + uId, + uInfo + ) + + const csvArgs = getCsvArgs(args) + + const jobData = { + userInfo, + userId, + name: 'getWeightedAveragesReport', + fileNamesMap: [['getWeightedAveragesReport', 'weighted-averages-report']], + args: csvArgs, + columnsCsv: { + symbol: 'PAIR', + buyingWeightedPrice: 'WEIGHTED PRICE', + buyingAmount: 'AMOUNT', + sellingWeightedPrice: 'WEIGHTED PRICE', + sellingAmount: 'AMOUNT', + cumulativeWeightedPrice: 'WEIGHTED PRICE', + cumulativeAmount: 'AMOUNT' + }, + formatSettings: { + symbol: 'symbol' + }, + csvCustomWriter: this.weightedAveragesReportCsvWriter + } + + return jobData + } } decorateInjectable(CsvJobData, depsTypes) diff --git a/workers/loc.api/helpers/schema.js b/workers/loc.api/helpers/schema.js index 6fbb2d89e..03142f22e 100644 --- a/workers/loc.api/helpers/schema.js +++ b/workers/loc.api/helpers/schema.js @@ -259,6 +259,21 @@ const paramsSchemaForWinLossVSAccountBalanceApi = { } } +const paramsSchemaForWeightedAveragesReportApi = { + type: 'object', + properties: { + start: { + type: 'integer' + }, + end: { + type: 'integer' + }, + symbol: { + type: ['string', 'array'] + } + } +} + const paramsSchemaForTradedVolumeApi = { type: 'object', properties: { @@ -369,6 +384,15 @@ const paramsSchemaForWinLossVSAccountBalanceCsv = { } } +const paramsSchemaForWeightedAveragesReportApiCsv = { + type: 'object', + properties: { + ...cloneDeep(paramsSchemaForWeightedAveragesReportApi.properties), + timezone, + dateFormat + } +} + const paramsSchemaForPositionsSnapshotCsv = { type: 'object', properties: { @@ -445,6 +469,7 @@ module.exports = { paramsSchemaForBalanceHistoryApi, paramsSchemaForWinLossApi, paramsSchemaForWinLossVSAccountBalanceApi, + paramsSchemaForWeightedAveragesReportApi, paramsSchemaForPositionsSnapshotApi, paramsSchemaForFullSnapshotReportApi, paramsSchemaForFullTaxReportApi, @@ -455,6 +480,7 @@ module.exports = { paramsSchemaForBalanceHistoryCsv, paramsSchemaForWinLossCsv, paramsSchemaForWinLossVSAccountBalanceCsv, + paramsSchemaForWeightedAveragesReportApiCsv, paramsSchemaForPositionsSnapshotCsv, paramsSchemaForFullSnapshotReportCsv, paramsSchemaForFullTaxReportCsv, diff --git a/workers/loc.api/service.report.framework.js b/workers/loc.api/service.report.framework.js index b8900f965..fa863ee28 100644 --- a/workers/loc.api/service.report.framework.js +++ b/workers/loc.api/service.report.framework.js @@ -1322,6 +1322,15 @@ class FrameworkReportService extends ReportService { }, 'getWinLossVSAccountBalance', args, cb) } + getWeightedAveragesReport (space, args, cb) { + return this._privResponder(async () => { + checkParams(args, 'paramsSchemaForWeightedAveragesReportApi') + + return this._weightedAveragesReport + .getWeightedAveragesReport(args) + }, 'getWeightedAveragesReport', args, cb) + } + /** * @override */ @@ -1426,6 +1435,15 @@ class FrameworkReportService extends ReportService { ) }, 'getWinLossVSAccountBalanceCsv', args, cb) } + + getWeightedAveragesReportCsv (space, args, cb) { + return this._responder(() => { + return this._generateCsv( + 'getWeightedAveragesReportCsvJobData', + args + ) + }, 'getWeightedAveragesReportCsv', args, cb) + } } module.exports = FrameworkReportService diff --git a/workers/loc.api/sync/weighted.averages.report/index.js b/workers/loc.api/sync/weighted.averages.report/index.js new file mode 100644 index 000000000..fa99fb806 --- /dev/null +++ b/workers/loc.api/sync/weighted.averages.report/index.js @@ -0,0 +1,196 @@ +'use strict' + +const { decorateInjectable } = require('../../di/utils') + +const depsTypes = (TYPES) => [ + TYPES.DAO, + TYPES.Authenticator, + TYPES.SyncSchema, + TYPES.ALLOWED_COLLS +] +class WeightedAveragesReport { + constructor ( + dao, + authenticator, + syncSchema, + ALLOWED_COLLS + ) { + this.dao = dao + this.authenticator = authenticator + this.syncSchema = syncSchema + this.ALLOWED_COLLS = ALLOWED_COLLS + + this.tradesModel = this.syncSchema.getModelsMap() + .get(this.ALLOWED_COLLS.TRADES) + } + + async getWeightedAveragesReport (args = {}) { + const { + auth = {}, + params = {} + } = args ?? {} + + const user = await this.authenticator + .verifyRequestUser({ auth }) + + const { + start = 0, + end = Date.now(), + symbol: _symbol = [] + } = params ?? {} + const symbolArr = Array.isArray(_symbol) + ? _symbol + : [_symbol] + const symbol = symbolArr.filter((s) => ( + s && typeof s === 'string' + )) + + const trades = await this._getTrades({ + user, + start, + end, + symbol + }) + const calcedTrades = this._calcTrades(trades) + + return calcedTrades + } + + async _getTrades (args) { + const { + user = {}, + start = 0, + end = Date.now(), + symbol = [] + } = args ?? {} + + const symbFilter = ( + Array.isArray(symbol) && + symbol.length !== 0 + ) + ? { $in: { symbol } } + : {} + + return this.dao.getElemsInCollBy( + this.ALLOWED_COLLS.TRADES, + { + filter: { + user_id: user._id, + $lte: { mtsCreate: end }, + $gte: { mtsCreate: start }, + ...symbFilter + }, + sort: [['mtsCreate', -1]], + projection: this.tradesModel, + exclude: ['user_id'], + isExcludePrivate: true + } + ) + } + + _calcTrades (trades = []) { + const symbResMap = new Map() + + for (const trade of trades) { + const { + symbol, + execAmount, + execPrice + } = trade ?? {} + + if ( + !symbol || + typeof symbol !== 'string' || + !Number.isFinite(execAmount) || + execAmount === 0 || + !Number.isFinite(execPrice) || + execPrice === 0 + ) { + continue + } + + const isBuying = execAmount > 0 + const spent = execAmount * execPrice + + const existedSymbRes = symbResMap.get(symbol) + const { + sumSpent: _sumSpent = 0, + sumAmount: _sumAmount = 0, + sumBuyingSpent: _sumBuyingSpent = 0, + sumBuyingAmount: _sumBuyingAmount = 0, + sumSellingSpent: _sumSellingSpent = 0, + sumSellingAmount: _sumSellingAmount = 0 + } = existedSymbRes ?? {} + + const sumSpent = Number.isFinite(spent) + ? _sumSpent + spent + : _sumSpent + const sumAmount = _sumAmount + execAmount + const sumBuyingSpent = ( + isBuying && + Number.isFinite(spent) + ) + ? _sumBuyingSpent + spent + : _sumBuyingSpent + const sumBuyingAmount = isBuying + ? _sumBuyingAmount + execAmount + : _sumBuyingAmount + const sumSellingSpent = ( + !isBuying && + Number.isFinite(spent) + ) + ? _sumSellingSpent + spent + : _sumSellingSpent + const sumSellingAmount = !isBuying + ? _sumSellingAmount + execAmount + : _sumSellingAmount + + symbResMap.set(symbol, { + sumSpent, + sumAmount, + sumBuyingSpent, + sumBuyingAmount, + sumSellingSpent, + sumSellingAmount, + + buyingWeightedPrice: sumBuyingAmount === 0 + ? 0 + : sumBuyingSpent / sumBuyingAmount, + buyingAmount: sumBuyingAmount, + sellingWeightedPrice: sumSellingAmount === 0 + ? 0 + : sumSellingSpent / sumSellingAmount, + sellingAmount: sumSellingAmount, + cumulativeWeightedPrice: sumAmount === 0 + ? 0 + : sumSpent / sumAmount, + cumulativeAmount: sumAmount + }) + } + + return [...symbResMap].map(([symbol, val]) => { + const { + buyingWeightedPrice = 0, + buyingAmount = 0, + sellingWeightedPrice = 0, + sellingAmount = 0, + cumulativeWeightedPrice = 0, + cumulativeAmount = 0 + } = val ?? {} + + return { + symbol, + buyingWeightedPrice, + buyingAmount, + sellingWeightedPrice, + sellingAmount, + cumulativeWeightedPrice, + cumulativeAmount + } + }) + } +} + +decorateInjectable(WeightedAveragesReport, depsTypes) + +module.exports = WeightedAveragesReport