diff --git a/package.json b/package.json index b5ceac7..160134a 100644 --- a/package.json +++ b/package.json @@ -52,6 +52,7 @@ "chalk": "3.0.0", "cli-progress": "3.10.0", "commander": "5.0.0", + "dayjs": "1.11.10", "debug": "4.3.4", "fastest-validator": "1.10.0", "fs-extra": "10.1.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 729e1bb..4038322 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -11,6 +11,7 @@ specifiers: chalk: 3.0.0 cli-progress: 3.10.0 commander: 5.0.0 + dayjs: 1.11.10 debug: 4.3.4 eslint: 8.13.0 eslint-config-prettier: 8.5.0 @@ -33,6 +34,7 @@ dependencies: chalk: 3.0.0 cli-progress: 3.10.0 commander: 5.0.0 + dayjs: 1.11.10 debug: 4.3.4 fastest-validator: 1.10.0 fs-extra: 10.1.0 @@ -1246,6 +1248,10 @@ packages: resolution: {integrity: sha512-jyCETtSl3VMZMWeRo7iY1FL19ges1t55hMo5yaam4Jrsm5EPL89UQkoQRyiI+Yf4k8r2ZpdngkV8hr1lIdjb3Q==} dev: true + /dayjs/1.11.10: + resolution: {integrity: sha512-vjAczensTgRcqDERK0SR2XMwsF/tSvnvlv6VcF2GIhg6Sx4yOIt/irsr1RDJsKiIyBzJDpCoXiWWq28MqH2cnQ==} + dev: false + /debug/4.3.4: resolution: {integrity: sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==} engines: {node: '>=6.0'} diff --git a/src/cli/cli.ts b/src/cli/cli.ts index 8a7b022..3e74fe9 100644 --- a/src/cli/cli.ts +++ b/src/cli/cli.ts @@ -14,12 +14,18 @@ import { processData } from '../processor'; import { formatBytes } from '../utils/formatBytes'; import chalk from 'chalk'; import debug from 'debug'; +import dayjs from 'dayjs'; +import utc from 'dayjs/plugin/utc'; +import tz from 'dayjs/plugin/timezone'; import { version } from '../../package.json'; import { BatchStreamWriter } from '../stream-writer'; import { BufferObject } from '../buffer-fetcher/types'; import { formatTimeDuration } from '../utils/formatTimeDuration'; +dayjs.extend(utc); +dayjs.extend(tz); + const DEBUG_NAMESPACE = 'dukascopy-node:cli'; export async function run(argv: NodeJS.Process['argv']) { @@ -47,7 +53,9 @@ export async function run(argv: NodeJS.Process['argv']) { failAfterRetryCount, retryOnEmpty, pauseBetweenRetriesMs, - fileName: customFileName + fileName: customFileName, + dateFormat, + timeZone } = input; if (isDebugActive) { @@ -187,7 +195,22 @@ export async function run(argv: NodeJS.Process['argv']) { ignoreFlats }); - await batchStreamWriter.writeBatch(processedBatch); + await batchStreamWriter.writeBatch( + processedBatch, + dateFormat + ? timeStamp => { + if (dateFormat === 'iso') { + return new Date(timeStamp).toISOString(); + } + + if (timeZone) { + return dayjs(timeStamp).tz(timeZone).format(dateFormat); + } + + return dayjs(timeStamp).utc().format(dateFormat); + } + : undefined + ); } if (isLastBatch) { diff --git a/src/cli/config.ts b/src/cli/config.ts index 4e5d214..049ff9e 100644 --- a/src/cli/config.ts +++ b/src/cli/config.ts @@ -14,6 +14,8 @@ export interface CliConfig extends ConfigBase { debug: boolean; inline: boolean; fileName: string; + dateFormat: string; + timeZone: string; } const now = 'now'; @@ -44,6 +46,8 @@ const commanderSchema = program .option('-bp, --batch-pause ', 'Pause between batches in ms', Number, 1000) .option('-ch, --cache', 'Use cache', false) .option('-chpath, --cache-path ', 'Folder path for cache data', './.dukascopy-cache') + .option('-df, --date-format ', 'Date format', '') + .option('-tz, --time-zone ', 'Timezone', '') .option('-r, --retries ', 'Number of retries for a failed artifact download', Number, 0) .option('-rp, --retry-pause ', 'Pause between retries in milliseconds', Number, 500) .option( @@ -95,7 +99,9 @@ export function getConfigFromCliArgs(argv: NodeJS.Process['argv']) { pauseBetweenRetriesMs: options.retryPause, debug: options.debug, inline: options.inline, - fileName: options.fileName + fileName: options.fileName, + dateFormat: options.dateFormat, + timeZone: options.timeZone }; const cliSchema: InputSchema = { @@ -105,7 +111,9 @@ export function getConfigFromCliArgs(argv: NodeJS.Process['argv']) { silent: { type: 'boolean', required: false } as RuleBoolean, debug: { type: 'boolean', required: false } as RuleBoolean, inline: { type: 'boolean', required: false } as RuleBoolean, - fileName: { type: 'string', required: false } as RuleString + fileName: { type: 'string', required: false } as RuleString, + dateFormat: { type: 'string', required: false } as RuleString, + timeZone: { type: 'string', required: false } as RuleString } }; diff --git a/src/stream-writer/index.ts b/src/stream-writer/index.ts index 7751827..5e101b8 100644 --- a/src/stream-writer/index.ts +++ b/src/stream-writer/index.ts @@ -101,7 +101,7 @@ export class BatchStreamWriter { private initHeaders() { const bodyHeaders = this.timeframe === Timeframe.tick - ? ['timestamp', 'askPrice', 'bidPrice', 'askVolume', 'bidVolume'] + ? ['timestamp', 'askPrice', 'bidPrice', 'askVolume', 'bidVolume'] // TODO: add custom header names as cli options : ['timestamp', 'open', 'high', 'low', 'close', 'volume']; if (!this.volumes) { @@ -114,8 +114,8 @@ export class BatchStreamWriter { return bodyHeaders; } - public async writeBatch(batch: number[][]) { - const batchWithinRange: number[][] = []; + public async writeBatch(batch: number[][], dateFormatter?: (timestamp: number) => string) { + const batchWithinRange: (number | string)[][] = []; for (let j = 0; j < batch.length; j++) { const item = batch[j]; @@ -123,12 +123,16 @@ export class BatchStreamWriter { item.length > 0 && item[0] >= this.startDateTs && item[0] < this.endDateTs; if (isItemInRange) { + if (dateFormatter) { + //@ts-expect-error TODO: fix this + item[0] = dateFormatter(item[0]); + } batchWithinRange.push(item); } } for (let i = 0; i < batchWithinRange.length; i++) { - const item = batchWithinRange[i]; + let item = batchWithinRange[i]; const isFirstItem = i === 0; const isLastItem = i === batchWithinRange.length - 1; @@ -152,6 +156,10 @@ export class BatchStreamWriter { body += ',' + (!this.isInline ? '\n' : ''); } + if (dateFormatter && (this.format === Format.json || this.format === Format.array)) { + item[0] = `"${item[0]}"`; + } + if (this.format === Format.json) { const jsonObjectBody = item.map((val, i) => `"${this.bodyHeaders[i]}":${val}`).join(','); body += `{${jsonObjectBody}}`;