Skip to content

Commit a907068

Browse files
maxgfrclaude
andcommitted
feat: add CSV export format support
- Add --format flag to choose between json and csv output - Implement CSV converter in utils.ts - Add format option to interactive mode - Update README with CSV export examples - Default format is json for backward compatibility Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
1 parent ce15575 commit a907068

File tree

4 files changed

+107
-17
lines changed

4 files changed

+107
-17
lines changed

README.md

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -43,18 +43,32 @@ binance-historical download \
4343
--interval 4h \
4444
--start 2024-01-01 \
4545
--end 2024-12-31 \
46-
--output ./data/
46+
--output ./data/ \
47+
--format json
48+
```
49+
50+
Export as CSV:
51+
52+
```sh
53+
binance-historical download \
54+
--pair ETHUSDT \
55+
--interval 1h \
56+
--start 2024-01-01 \
57+
--end 2024-01-31 \
58+
--output ./data/ \
59+
--format csv
4760
```
4861

4962
#### CLI Options
5063

5164
| Option | Alias | Description |
52-
|--------|-------|-------------|
65+
| ------ | ----- | ----------- |
5366
| `--pair <symbol>` | `-p` | Trading pair (e.g., BTCUSDT, ETHUSDT) |
5467
| `--interval <interval>` | `-i` | Kline interval (1m, 3m, 5m, 15m, 30m, 1h, 2h, 4h, 6h, 8h, 12h, 1d, 3d, 1w) |
5568
| `--start <date>` | `-s` | Start date (YYYY-MM-DD or ISO 8601) |
5669
| `--end <date>` | `-e` | End date (YYYY-MM-DD or ISO 8601) |
5770
| `--output <path>` | `-o` | Output directory path (filename is auto-generated) |
71+
| `--format <format>` | `-f` | Output format (json or csv, default: json) |
5872
| `--help` | `-h` | Display help |
5973
| `--version` | `-V` | Display version |
6074

src/cli.ts

Lines changed: 43 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
import prompts from 'prompts';
2-
import type { PromptResult, BinanceInterval } from './types';
2+
import { Command } from 'commander';
3+
import type { BinanceInterval, OutputFormat, PromptResult } from './types';
34
import { getKline } from './klines';
45
import { formatDate, saveKline } from './utils';
5-
import { Command } from 'commander';
66

77
const VALID_INTERVALS: BinanceInterval[] = [
88
'1m',
@@ -68,6 +68,16 @@ const questions: Array<prompts.PromptObject> = [
6868
message: 'The path of the file that will be saved:',
6969
initial: `${process.cwd()}/`,
7070
},
71+
{
72+
type: 'select',
73+
name: 'format',
74+
message: 'Output format:',
75+
choices: [
76+
{ title: 'JSON', value: 'json' },
77+
{ title: 'CSV', value: 'csv' },
78+
],
79+
initial: 0,
80+
},
7181
];
7282

7383
interface CliOptions {
@@ -76,12 +86,17 @@ interface CliOptions {
7686
start?: string;
7787
end?: string;
7888
output?: string;
89+
format?: string;
7990
}
8091

8192
function isValidInterval(interval: string): interval is BinanceInterval {
8293
return VALID_INTERVALS.includes(interval as BinanceInterval);
8394
}
8495

96+
function isValidFormat(format: string): format is OutputFormat {
97+
return format === 'json' || format === 'csv';
98+
}
99+
85100
function parseDate(dateStr: string): Date | null {
86101
const date = new Date(dateStr);
87102
if (Number.isNaN(date.getTime())) {
@@ -122,6 +137,10 @@ function validateOptions(options: CliOptions): {
122137
}
123138
}
124139

140+
if (options.format && !isValidFormat(options.format)) {
141+
errors.push(`Invalid format "${options.format}". Valid formats: json, csv`);
142+
}
143+
125144
return { valid: errors.length === 0, errors };
126145
}
127146

@@ -131,14 +150,15 @@ function hasAllRequiredOptions(options: CliOptions): boolean {
131150
options.interval &&
132151
options.start &&
133152
options.end &&
134-
options.output
153+
options.output &&
154+
options.format
135155
);
136156
}
137157

138158
async function promptUser(): Promise<Partial<PromptResult>> {
139-
const { pair, interval, startDate, endDate, fileName } =
159+
const { pair, interval, startDate, endDate, fileName, format } =
140160
await prompts(questions);
141-
return { pair, interval, startDate, endDate, fileName };
161+
return { pair, interval, startDate, endDate, fileName, format };
142162
}
143163

144164
async function promptMissingOptions(
@@ -187,6 +207,12 @@ async function promptMissingOptions(
187207
missingQuestions.push(questions[4]);
188208
}
189209

210+
if (options.format && isValidFormat(options.format)) {
211+
providedValues.format = options.format;
212+
} else {
213+
missingQuestions.push(questions[5]);
214+
}
215+
190216
if (missingQuestions.length > 0) {
191217
const answers = await prompts(missingQuestions);
192218
return { ...providedValues, ...answers };
@@ -196,7 +222,7 @@ async function promptMissingOptions(
196222
}
197223

198224
async function downloadKlines(config: PromptResult): Promise<void> {
199-
const { pair, interval, startDate, endDate, fileName } = config;
225+
const { pair, interval, startDate, endDate, fileName, format } = config;
200226

201227
const kLines = await getKline(pair, interval, startDate, endDate).catch(
202228
(error) => {
@@ -206,10 +232,11 @@ async function downloadKlines(config: PromptResult): Promise<void> {
206232
);
207233

208234
if (kLines) {
235+
const extension = format === 'csv' ? 'csv' : 'json';
209236
const outputPath =
210237
fileName +
211-
`${pair}_${interval}_${formatDate(startDate)}_${formatDate(endDate)}.json`;
212-
saveKline(outputPath, kLines);
238+
`${pair}_${interval}_${formatDate(startDate)}_${formatDate(endDate)}.${extension}`;
239+
saveKline(outputPath, kLines, format);
213240
console.log(`Downloaded ${kLines.length} klines to ${outputPath}`);
214241
}
215242
}
@@ -229,16 +256,18 @@ async function processWithOptions(options: CliOptions): Promise<void> {
229256
const startDate = parseDate(options.start as string) as Date;
230257
const endDate = parseDate(options.end as string) as Date;
231258
const fileName = options.output as string;
259+
const format = options.format as OutputFormat;
232260

233-
await downloadKlines({ pair, interval, startDate, endDate, fileName });
261+
await downloadKlines({ pair, interval, startDate, endDate, fileName, format });
234262
} else {
235263
const result = await promptMissingOptions(options);
236264
if (
237265
!result.pair ||
238266
!result.interval ||
239267
!result.startDate ||
240268
!result.endDate ||
241-
!result.fileName
269+
!result.fileName ||
270+
!result.format
242271
) {
243272
console.error('Missing required information');
244273
process.exit(1);
@@ -254,7 +283,8 @@ async function processInteractive(): Promise<void> {
254283
!result.interval ||
255284
!result.startDate ||
256285
!result.endDate ||
257-
!result.fileName
286+
!result.fileName ||
287+
!result.format
258288
) {
259289
console.error('Missing required information');
260290
process.exit(1);
@@ -273,7 +303,7 @@ export async function runCommand(): Promise<void> {
273303
program
274304
.command('download')
275305
.description(
276-
'Download a JSON file containing historical klines from Binance API',
306+
'Download historical klines from Binance API',
277307
)
278308
.option('-p, --pair <symbol>', 'Trading pair (e.g., BTCUSDT, ETHUSDT)')
279309
.option(
@@ -286,6 +316,7 @@ export async function runCommand(): Promise<void> {
286316
'-o, --output <path>',
287317
'Output directory path (filename is auto-generated)',
288318
)
319+
.option('-f, --format <format>', 'Output format (json, csv)', 'json')
289320
.action(async (options: CliOptions) => {
290321
const hasAnyOption =
291322
options.pair ||

src/types.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,10 +46,13 @@ export type BinanceResponseData = [
4646
string,
4747
];
4848

49+
export type OutputFormat = 'json' | 'csv';
50+
4951
export type PromptResult = {
5052
pair: string;
5153
interval: BinanceInterval;
5254
startDate: Date;
5355
endDate: Date;
5456
fileName: string;
57+
format: OutputFormat;
5558
};

src/utils.ts

Lines changed: 45 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,51 @@
1-
import { BinanceInterval, Kline } from './types';
1+
import type { BinanceInterval, Kline, OutputFormat } from './types';
22

33
import fs = require('fs');
44

5-
export const saveKline = (fileName: string, jsonArray: Array<Kline>) => {
6-
fs.writeFileSync(fileName, JSON.stringify(jsonArray, null, 2));
5+
const convertToCSV = (klines: Array<Kline>): string => {
6+
const headers = [
7+
'openTime',
8+
'open',
9+
'high',
10+
'low',
11+
'close',
12+
'volume',
13+
'closeTime',
14+
'quoteAssetVolume',
15+
'trades',
16+
'takerBaseAssetVolume',
17+
'takerQuoteAssetVolume',
18+
'ignored',
19+
].join(',');
20+
21+
const rows = klines.map((kline) =>
22+
[
23+
kline.openTime,
24+
kline.open,
25+
kline.high,
26+
kline.low,
27+
kline.close,
28+
kline.volume,
29+
kline.closeTime,
30+
kline.quoteAssetVolume,
31+
kline.trades,
32+
kline.takerBaseAssetVolume,
33+
kline.takerQuoteAssetVolume,
34+
kline.ignored,
35+
].join(','),
36+
);
37+
38+
return [headers, ...rows].join('\n');
39+
};
40+
41+
export const saveKline = (
42+
fileName: string,
43+
jsonArray: Array<Kline>,
44+
format: OutputFormat = 'json',
45+
): void => {
46+
const content =
47+
format === 'csv' ? convertToCSV(jsonArray) : JSON.stringify(jsonArray, null, 2);
48+
fs.writeFileSync(fileName, content);
749
};
850

951
export const formatDate = (date: Date, withHour = false): string => {

0 commit comments

Comments
 (0)