akshare-cli 0.2.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
akshare_cli/cli.py ADDED
@@ -0,0 +1,1285 @@
1
+ #!/usr/bin/env python
2
+ # -*- coding: utf-8 -*-
3
+ """
4
+ AKShare CLI Harness - Main CLI Entry Point
5
+
6
+ A complete CLI for the akshare financial data library.
7
+ Supports one-shot commands, REPL mode, and JSON output.
8
+
9
+ Usage:
10
+ akshare-cli call stock_zh_a_hist --symbol 000001
11
+ akshare-cli stock hist 000001 --json
12
+ akshare-cli search fund --json
13
+ akshare-cli repl
14
+ """
15
+
16
+ import functools
17
+ import json
18
+ import os
19
+ import shlex
20
+ import sys
21
+ import traceback
22
+ from typing import Optional
23
+
24
+ import click
25
+ import pandas as pd
26
+
27
+ from akshare_cli.core.cache import _result_cache
28
+ from akshare_cli.core.registry import (
29
+ call_function,
30
+ get_all_functions,
31
+ get_domains,
32
+ get_function,
33
+ get_function_info,
34
+ list_functions_by_domain,
35
+ search_functions,
36
+ )
37
+ from akshare_cli.core.session import Session
38
+ from akshare_cli.core.export import auto_export
39
+ from akshare_cli.utils.formatting import (
40
+ display_result,
41
+ print_error,
42
+ print_info,
43
+ )
44
+
45
+ # Global session
46
+ _session = Session()
47
+
48
+
49
+ class OutputConfig:
50
+ """Holds output configuration for the current invocation."""
51
+
52
+ def __init__(self):
53
+ self.format = "table"
54
+ self.output_file = None
55
+ self.max_rows = None
56
+ self.show_index = False
57
+ self.no_header = False
58
+
59
+ def apply(self, use_json=False, use_csv=False, output_file=None, limit=None, no_header=False):
60
+ """Apply output options, only overriding if explicitly set."""
61
+ if use_json:
62
+ self.format = "json"
63
+ if use_csv:
64
+ self.format = "csv"
65
+ if output_file is not None:
66
+ self.output_file = output_file
67
+ if limit is not None:
68
+ self.max_rows = limit
69
+ if no_header:
70
+ self.no_header = no_header
71
+
72
+
73
+ _output_config = OutputConfig()
74
+
75
+
76
+ def output_options(func):
77
+ """Decorator that adds --json/--csv/--output/--limit/--no-header to any command."""
78
+ @click.option("--json", "use_json", is_flag=True, help="Output as JSON.")
79
+ @click.option("--csv", "use_csv", is_flag=True, help="Output as CSV.")
80
+ @click.option("--output", "-o", "output_file", default=None, help="Save output to file.")
81
+ @click.option("--limit", "-n", "limit", type=int, default=None, help="Limit number of rows.")
82
+ @click.option("--no-header", "no_header", is_flag=True, help="Suppress table headers.")
83
+ @functools.wraps(func)
84
+ def wrapper(*args, use_json=False, use_csv=False, output_file=None, limit=None, no_header=False, **kwargs):
85
+ _output_config.apply(use_json, use_csv, output_file, limit, no_header)
86
+ return func(*args, **kwargs)
87
+ return wrapper
88
+
89
+
90
+ def _handle_result(result, func_name: str = "", kwargs: dict = None):
91
+ """Process and display a function result."""
92
+ kwargs = kwargs or {}
93
+
94
+ if isinstance(result, pd.DataFrame):
95
+ _session.record_call(func_name, kwargs, result)
96
+
97
+ if result.empty:
98
+ if _output_config.format == "json":
99
+ click.echo(json.dumps({"total_rows": 0, "columns": [], "data": []}, indent=2))
100
+ else:
101
+ click.echo("No data returned (empty DataFrame).")
102
+ return
103
+
104
+ output = display_result(
105
+ result,
106
+ output_format=_output_config.format,
107
+ max_rows=_output_config.max_rows,
108
+ show_index=_output_config.show_index,
109
+ output_file=_output_config.output_file,
110
+ )
111
+ click.echo(output)
112
+
113
+ if _output_config.format == "table" and _output_config.output_file is None:
114
+ click.echo(
115
+ f"\n[{result.shape[0]} rows x {result.shape[1]} columns]",
116
+ err=True,
117
+ )
118
+ else:
119
+ if _output_config.format == "json":
120
+ click.echo(json.dumps({"result": str(result)}, indent=2))
121
+ else:
122
+ click.echo(str(result))
123
+
124
+
125
+ @click.group(invoke_without_command=True)
126
+ @click.option("--json", "use_json", is_flag=True, help="Output as JSON.")
127
+ @click.option("--csv", "use_csv", is_flag=True, help="Output as CSV.")
128
+ @click.option("--output", "-o", "output_file", help="Save output to file.")
129
+ @click.option("--limit", "-n", type=int, help="Limit number of rows.")
130
+ @click.option("--no-header", is_flag=True, help="Suppress table headers.")
131
+ @click.option("--no-cache", is_flag=True, help="禁用结果缓存,强制从 API 获取最新数据。")
132
+ @click.version_option(version="0.1.0", prog_name="akshare-cli")
133
+ @click.pass_context
134
+ def cli(ctx, use_json, use_csv, output_file, limit, no_header, no_cache):
135
+ """AKShare CLI - 命令行金融数据工具
136
+
137
+ 支持调用 akshare 库的 1090+ 个函数,获取股票、基金、期货、债券、外汇、宏观等金融数据。
138
+ 输出选项 (--json, --csv, --output, --limit) 可以放在子命令的前面或后面。
139
+
140
+ \b
141
+ 基本用法:
142
+ akshare-cli <命令> [参数] 一次性查询
143
+ akshare-cli repl 进入交互模式
144
+
145
+ 示例:
146
+ akshare-cli call stock_zh_a_hist --symbol 000001 --json
147
+ akshare-cli stock hist 000001 --json
148
+ akshare-cli --json search fund_etf
149
+ akshare-cli macro gdp --csv --output gdp.csv
150
+ akshare-cli repl
151
+
152
+ 更多信息:
153
+ 每个命令都支持 --help 查看详细帮助
154
+ 日期参数未指定时会自动填充为当天日期
155
+ """
156
+ ctx.ensure_object(dict)
157
+
158
+ _output_config.apply(use_json, use_csv, output_file, limit, no_header)
159
+
160
+ if no_cache:
161
+ _result_cache.enabled = False
162
+
163
+ if ctx.invoked_subcommand is None:
164
+ click.echo(ctx.get_help())
165
+
166
+
167
+ # ─── CALL: Dynamic function dispatch ─────────────────────────────────────────
168
+
169
+
170
+ @cli.command("call", context_settings=dict(
171
+ ignore_unknown_options=True,
172
+ allow_extra_args=True,
173
+ ))
174
+ @click.argument("params", nargs=-1, type=click.UNPROCESSED)
175
+ @click.pass_context
176
+ def cmd_call(ctx, params):
177
+ """调用任意 akshare 函数
178
+
179
+ 通过函数名直接调用 akshare 库的任意函数。参数以 --key value 形式传入。
180
+ 输出选项 (--json/--csv/--output) 可以放在任意位置。
181
+ 日期参数未指定时会自动填充为当天日期。
182
+
183
+ \b
184
+ 用法:
185
+ call <函数名> [--参数 值 ...]
186
+
187
+ 示例:
188
+ # 获取股票历史数据 (JSON 输出)
189
+ akshare-cli call stock_zh_a_hist --symbol 000001 --json
190
+
191
+ # --json 放在前面也可以
192
+ akshare-cli call --json stock_zh_a_spot_em
193
+
194
+ # 导出到文件
195
+ akshare-cli call futures_hist_em --symbol 螺纹主连 --output data.csv
196
+
197
+ # 获取经济新闻 (自动使用今天日期)
198
+ akshare-cli call news_economic_baidu --json
199
+
200
+ # 获取可转债数据,限制 10 行
201
+ akshare-cli call bond_zh_cov --json --limit 10
202
+
203
+ 查看函数用法:
204
+ akshare-cli call --full-help <函数名> 查看指定函数的完整用法
205
+ akshare-cli search <关键词> 搜索函数
206
+ akshare-cli info <函数名> 查看参数详情
207
+ """
208
+ # First pass: strip output flags from the token list
209
+ remaining = list(params)
210
+ call_output_json = False
211
+ call_output_csv = False
212
+ call_output_file = None
213
+ call_limit = None
214
+ full_help = False
215
+
216
+ filtered = []
217
+ i = 0
218
+ while i < len(remaining):
219
+ p = remaining[i]
220
+ if p == "--json":
221
+ call_output_json = True
222
+ i += 1
223
+ elif p == "--csv":
224
+ call_output_csv = True
225
+ i += 1
226
+ elif p == "--full-help":
227
+ full_help = True
228
+ i += 1
229
+ elif p in ("--output", "-o") and i + 1 < len(remaining):
230
+ call_output_file = remaining[i + 1]
231
+ i += 2
232
+ elif p in ("--limit", "-n") and i + 1 < len(remaining):
233
+ try:
234
+ call_limit = int(remaining[i + 1])
235
+ except ValueError:
236
+ pass
237
+ i += 2
238
+ elif p == "--no-header":
239
+ _output_config.no_header = True
240
+ i += 1
241
+ else:
242
+ filtered.append(p)
243
+ i += 1
244
+
245
+ _output_config.apply(call_output_json, call_output_csv, call_output_file, call_limit)
246
+
247
+ # Second pass: the first non-flag token is the function name
248
+ func_name = None
249
+ func_params = []
250
+ for j, tok in enumerate(filtered):
251
+ if not tok.startswith("--"):
252
+ func_name = tok
253
+ func_params = filtered[:j] + filtered[j + 1:]
254
+ break
255
+
256
+ # Handle --full-help
257
+ if full_help:
258
+ _print_full_help(func_name)
259
+ return
260
+
261
+ if not func_name:
262
+ print_error("Missing function name. Usage: call <func_name> [--param value ...]")
263
+ sys.exit(1)
264
+
265
+ kwargs = _parse_extra_params(func_params)
266
+ try:
267
+ result = call_function(func_name, kwargs)
268
+ _handle_result(result, func_name, kwargs)
269
+ except ValueError as e:
270
+ err_msg = str(e)
271
+ if "not found" in err_msg:
272
+ print_error(err_msg)
273
+ suggestions = search_functions(func_name)
274
+ if suggestions:
275
+ click.echo("\n你是不是想找:")
276
+ for s in suggestions[:5]:
277
+ click.echo(f" {s}")
278
+ else:
279
+ print_error(f"调用 {func_name} 失败: {e}")
280
+ if os.environ.get("AKSHARE_CLI_DEBUG"):
281
+ traceback.print_exc()
282
+ sys.exit(1)
283
+ except Exception as e:
284
+ print_error(f"调用 {func_name} 失败: {e}")
285
+ if os.environ.get("AKSHARE_CLI_DEBUG"):
286
+ traceback.print_exc()
287
+ sys.exit(1)
288
+
289
+
290
+ def _print_full_help(func_name: Optional[str] = None) -> None:
291
+ """Print detailed usage help for one function or list popular examples."""
292
+ if func_name:
293
+ info = get_function_info(func_name)
294
+ if info is None:
295
+ print_error(f"函数 '{func_name}' 未找到。")
296
+ suggestions = search_functions(func_name)
297
+ if suggestions:
298
+ click.echo("\n你是不是想找:")
299
+ for s in suggestions[:5]:
300
+ click.echo(f" {s}")
301
+ sys.exit(1)
302
+
303
+ if _output_config.format == "json":
304
+ # Build structured help object
305
+ help_obj = {
306
+ "function": info["name"],
307
+ "module": info["module"],
308
+ "description": info["docstring"] or "",
309
+ "parameters": [],
310
+ "usage": f"akshare-cli call {info['name']}",
311
+ }
312
+ parts = [f"akshare-cli call {info['name']}"]
313
+ for p in info["params"]:
314
+ pdata = {"name": p["name"]}
315
+ if p.get("type"):
316
+ pdata["type"] = p["type"]
317
+ if p.get("has_default"):
318
+ pdata["default"] = p.get("default")
319
+ pdata["required"] = False
320
+ else:
321
+ pdata["required"] = True
322
+ help_obj["parameters"].append(pdata)
323
+ if pdata.get("required"):
324
+ parts.append(f"--{p['name']} <值>")
325
+ else:
326
+ parts.append(f"[--{p['name']} {p.get('default', '')}]")
327
+ help_obj["usage"] = " ".join(parts)
328
+ click.echo(json.dumps(help_obj, indent=2, ensure_ascii=False, default=str))
329
+ else:
330
+ click.echo(f"函数: {info['name']}")
331
+ click.echo(f"模块: {info['module']}")
332
+ if info["docstring"]:
333
+ click.echo(f"说明: {info['docstring']}")
334
+ click.echo()
335
+
336
+ # Parameter table
337
+ if info["params"]:
338
+ click.echo("参数:")
339
+ for p in info["params"]:
340
+ name = p["name"]
341
+ type_str = p.get("type", "Any")
342
+ if p.get("has_default"):
343
+ default = p.get("default")
344
+ click.echo(f" --{name:<20s} 类型: {type_str:<8s} 默认值: {default!r}")
345
+ else:
346
+ click.echo(f" --{name:<20s} 类型: {type_str:<8s} (必填)")
347
+ else:
348
+ click.echo("参数: 无")
349
+ click.echo()
350
+
351
+ # Usage example
352
+ parts = [f"akshare-cli call {info['name']}"]
353
+ for p in info["params"]:
354
+ if not p.get("has_default"):
355
+ parts.append(f"--{p['name']} <值>")
356
+ parts.append("[--json]")
357
+ click.echo("用法:")
358
+ click.echo(f" {' '.join(parts)}")
359
+ else:
360
+ # No function name: show general usage
361
+ click.echo("用法: akshare-cli call --full-help <函数名>")
362
+ click.echo()
363
+ click.echo("查看指定函数的完整用法说明,包括参数、类型、默认值。")
364
+ click.echo()
365
+ click.echo("示例:")
366
+ click.echo(" akshare-cli call --full-help stock_zh_a_hist")
367
+ click.echo(" akshare-cli call --full-help news_economic_baidu")
368
+ click.echo(" akshare-cli call --full-help futures_hist_em")
369
+ click.echo(" akshare-cli call --full-help bond_zh_cov --json")
370
+ click.echo()
371
+ click.echo("搜索函数:")
372
+ click.echo(" akshare-cli search <关键词>")
373
+
374
+
375
+ def _parse_extra_params(params):
376
+ """Parse CLI params like --symbol 000001 --period daily into a dict."""
377
+ kwargs = {}
378
+ i = 0
379
+ while i < len(params):
380
+ p = params[i]
381
+ if p.startswith("--"):
382
+ key = p[2:]
383
+ if i + 1 < len(params) and not params[i + 1].startswith("--"):
384
+ kwargs[key] = params[i + 1]
385
+ i += 2
386
+ else:
387
+ kwargs[key] = "true"
388
+ i += 1
389
+ else:
390
+ i += 1
391
+ return kwargs
392
+
393
+
394
+ # ─── SEARCH: Find functions ──────────────────────────────────────────────────
395
+
396
+
397
+ @cli.command("search")
398
+ @click.argument("keyword")
399
+ @output_options
400
+ def cmd_search(keyword):
401
+ """按关键词搜索 akshare 函数
402
+
403
+ 在 1090+ 个函数中模糊搜索,返回包含关键词的所有函数名及简介。
404
+
405
+ \b
406
+ 示例:
407
+ # 搜索股票相关函数
408
+ akshare-cli search stock_zh
409
+
410
+ # 搜索 ETF 基金函数 (JSON 输出)
411
+ akshare-cli search fund_etf --json
412
+
413
+ # 搜索新闻接口
414
+ akshare-cli search news
415
+ """
416
+ results = search_functions(keyword)
417
+ if not results:
418
+ if _output_config.format == "json":
419
+ click.echo(json.dumps({"keyword": keyword, "count": 0, "functions": []}, indent=2))
420
+ else:
421
+ click.echo(f"No functions found matching '{keyword}'.")
422
+ return
423
+
424
+ if _output_config.format == "json":
425
+ click.echo(json.dumps({"keyword": keyword, "count": len(results), "functions": results}, indent=2))
426
+ else:
427
+ click.echo(f"Found {len(results)} functions matching '{keyword}':\n")
428
+ for name in results:
429
+ info = get_function_info(name)
430
+ doc_line = ""
431
+ if info and info["docstring"]:
432
+ doc_line = info["docstring"].split("\n")[0][:60]
433
+ click.echo(f" {name:<50s} {doc_line}")
434
+
435
+
436
+ # ─── LIST: List functions ────────────────────────────────────────────────────
437
+
438
+
439
+ @cli.command("list")
440
+ @click.argument("domain", required=False)
441
+ @output_options
442
+ def cmd_list(domain):
443
+ """列出可用的 akshare 函数
444
+
445
+ 不指定域时显示所有域的函数数量概览;指定域时列出该域下所有函数。
446
+
447
+ \b
448
+ 常见域:
449
+ stock 股票 fund 基金
450
+ futures 期货 bond 债券
451
+ forex 外汇 macro 宏观经济
452
+ index 指数 option 期权
453
+ news 新闻
454
+
455
+ 示例:
456
+ # 查看所有域的概览
457
+ akshare-cli list
458
+
459
+ # 列出股票域的所有函数
460
+ akshare-cli list stock
461
+
462
+ # JSON 格式输出
463
+ akshare-cli list fund --json
464
+ """
465
+ if domain:
466
+ funcs = list_functions_by_domain(domain)
467
+ if not funcs:
468
+ if _output_config.format == "json":
469
+ click.echo(json.dumps({"domain": domain, "count": 0, "functions": [], "available_domains": get_domains()}, indent=2))
470
+ else:
471
+ click.echo(f"No functions found for domain '{domain}'.")
472
+ click.echo(f"\nAvailable domains: {', '.join(get_domains())}")
473
+ return
474
+
475
+ if _output_config.format == "json":
476
+ click.echo(json.dumps({"domain": domain, "count": len(funcs), "functions": funcs}, indent=2))
477
+ else:
478
+ click.echo(f"{domain} ({len(funcs)} functions):\n")
479
+ for name in funcs:
480
+ click.echo(f" {name}")
481
+ else:
482
+ domains = get_domains()
483
+ all_funcs = get_all_functions()
484
+ if _output_config.format == "json":
485
+ domain_counts = {}
486
+ for d in domains:
487
+ domain_counts[d] = len(list_functions_by_domain(d))
488
+ click.echo(json.dumps({
489
+ "total_functions": len(all_funcs),
490
+ "domains": domain_counts,
491
+ }, indent=2))
492
+ else:
493
+ click.echo(f"AKShare has {len(all_funcs)} functions across {len(domains)} domains:\n")
494
+ for d in domains:
495
+ count = len(list_functions_by_domain(d))
496
+ click.echo(f" {d:<25s} {count:>4d} functions")
497
+ click.echo(f"\nUse 'list <domain>' to see functions in a domain.")
498
+
499
+
500
+ # ─── INFO: Function details ──────────────────────────────────────────────────
501
+
502
+
503
+ @cli.command("info")
504
+ @click.argument("func_name")
505
+ @output_options
506
+ def cmd_info(func_name):
507
+ """查看函数的详细信息
508
+
509
+ 显示函数的参数列表、类型、默认值和文档说明。
510
+ 在调用陌生函数前先用 info 查看参数要求。
511
+
512
+ \b
513
+ 示例:
514
+ # 查看股票历史数据函数的参数
515
+ akshare-cli info stock_zh_a_hist
516
+
517
+ # JSON 格式输出
518
+ akshare-cli info news_economic_baidu --json
519
+
520
+ 输出内容:
521
+ - 函数名和模块
522
+ - 参数列表(名称、类型、默认值、是否必填)
523
+ - 函数文档说明
524
+ """
525
+ info = get_function_info(func_name)
526
+ if info is None:
527
+ if _output_config.format == "json":
528
+ click.echo(json.dumps({"error": f"Function '{func_name}' not found"}, indent=2))
529
+ else:
530
+ print_error(f"Function '{func_name}' not found.")
531
+ suggestions = search_functions(func_name)
532
+ if suggestions:
533
+ click.echo("\nDid you mean one of these?")
534
+ for s in suggestions[:5]:
535
+ click.echo(f" {s}")
536
+ sys.exit(1)
537
+
538
+ if _output_config.format == "json":
539
+ click.echo(json.dumps(info, indent=2, ensure_ascii=False, default=str))
540
+ else:
541
+ click.echo(f"Function: {info['name']}")
542
+ click.echo(f"Module: {info['module']}")
543
+ click.echo()
544
+ if info["params"]:
545
+ click.echo("Parameters:")
546
+ for p in info["params"]:
547
+ default_str = f" = {p['default']!r}" if p.get("has_default") else " (required)"
548
+ type_str = f" : {p.get('type', 'Any')}" if p.get("type") else ""
549
+ click.echo(f" --{p['name']}{type_str}{default_str}")
550
+ else:
551
+ click.echo("Parameters: none")
552
+ click.echo()
553
+ if info["docstring"]:
554
+ click.echo("Description:")
555
+ click.echo(f" {info['docstring']}")
556
+
557
+
558
+ # ─── DOMAIN SHORTCUTS ────────────────────────────────────────────────────────
559
+
560
+
561
+ @cli.group("stock")
562
+ def cmd_stock():
563
+ """股票数据快捷命令
564
+
565
+ \b
566
+ 子命令:
567
+ hist 获取股票历史 K 线数据 (OHLCV)
568
+ spot 获取股票实时行情
569
+
570
+ 示例:
571
+ akshare-cli stock hist 000001 --json
572
+ akshare-cli stock spot --market sh --csv
573
+ """
574
+ pass
575
+
576
+
577
+ @cmd_stock.command("hist")
578
+ @click.argument("symbol")
579
+ @click.option("--period", default="daily", help="daily/weekly/monthly")
580
+ @click.option("--start", "start_date", default="19700101", help="Start date YYYYMMDD")
581
+ @click.option("--end", "end_date", default="20500101", help="End date YYYYMMDD")
582
+ @click.option("--adjust", default="", help="qfq/hfq/'' (forward/backward/none)")
583
+ @output_options
584
+ def cmd_stock_hist(symbol, period, start_date, end_date, adjust):
585
+ """获取股票历史 K 线数据
586
+
587
+ 获取 A 股历史 OHLCV 数据(开盘价、最高价、最低价、收盘价、成交量)。
588
+ 数据来源:东方财富 (stock_zh_a_hist)
589
+
590
+ \b
591
+ 参数:
592
+ SYMBOL 股票代码 (如 000001, 600519)
593
+ --period 周期: daily/weekly/monthly (默认 daily)
594
+ --start 开始日期 YYYYMMDD (默认 19700101)
595
+ --end 结束日期 YYYYMMDD (默认 20500101)
596
+ --adjust 复权: qfq(前复权)/hfq(后复权)/空串(不复权, 默认)
597
+
598
+ 示例:
599
+ # 平安银行日线数据
600
+ akshare-cli stock hist 000001 --json
601
+
602
+ # 贵州茅台周线 + 前复权
603
+ akshare-cli stock hist 600519 --period weekly --adjust qfq
604
+
605
+ # 指定日期范围
606
+ akshare-cli stock hist 000001 --start 20260101 --end 20260311 --csv
607
+ """
608
+ kwargs = {
609
+ "symbol": symbol,
610
+ "period": period,
611
+ "start_date": start_date,
612
+ "end_date": end_date,
613
+ "adjust": adjust,
614
+ }
615
+ try:
616
+ result = call_function("stock_zh_a_hist", kwargs)
617
+ _handle_result(result, "stock_zh_a_hist", kwargs)
618
+ except Exception as e:
619
+ print_error(f"Failed: {e}")
620
+ sys.exit(1)
621
+
622
+
623
+ @cmd_stock.command("spot")
624
+ @click.option("--market", default="all", help="all/sh/sz/bj/hk/us")
625
+ @output_options
626
+ def cmd_stock_spot(market):
627
+ """获取股票实时行情
628
+
629
+ 获取各市场股票实时报价数据。数据来源:东方财富。
630
+
631
+ \b
632
+ 参数:
633
+ --market 市场选择 (默认 all)
634
+ all 全部 A 股
635
+ sh 上海 A 股
636
+ sz 深圳 A 股
637
+ bj 北交所
638
+ hk 港股
639
+ us 美股
640
+
641
+ 示例:
642
+ akshare-cli stock spot --json
643
+ akshare-cli stock spot --market sh --csv
644
+ akshare-cli stock spot --market hk --json --limit 20
645
+
646
+ 注意: 全市场数据量较大 (5000+ 行),建议配合 --limit 使用
647
+ """
648
+ func_map = {
649
+ "all": "stock_zh_a_spot_em",
650
+ "sh": "stock_sh_a_spot_em",
651
+ "sz": "stock_sz_a_spot_em",
652
+ "bj": "stock_bj_a_spot_em",
653
+ "hk": "stock_hk_spot_em",
654
+ "us": "stock_us_spot_em",
655
+ }
656
+ func_name = func_map.get(market, "stock_zh_a_spot_em")
657
+ try:
658
+ result = call_function(func_name, {})
659
+ _handle_result(result, func_name, {})
660
+ except Exception as e:
661
+ print_error(f"Failed: {e}")
662
+ sys.exit(1)
663
+
664
+
665
+ @cli.group("fund")
666
+ def cmd_fund():
667
+ """基金数据快捷命令
668
+
669
+ \b
670
+ 子命令:
671
+ etf 获取 ETF 基金数据
672
+
673
+ 示例:
674
+ akshare-cli fund etf --json 列出所有 ETF
675
+ akshare-cli fund etf 159707 --json 获取单只 ETF 历史数据
676
+ """
677
+ pass
678
+
679
+
680
+ @cmd_fund.command("etf")
681
+ @click.argument("symbol", required=False)
682
+ @click.option("--period", default="daily", help="daily/weekly/monthly")
683
+ @click.option("--start", "start_date", default="19700101", help="Start date")
684
+ @click.option("--end", "end_date", default="20500101", help="End date")
685
+ @click.option("--adjust", default="", help="qfq/hfq/''")
686
+ @output_options
687
+ def cmd_fund_etf(symbol, period, start_date, end_date, adjust):
688
+ """获取 ETF 基金数据
689
+
690
+ 不传代码时列出所有 ETF 实时行情;传代码时获取单只 ETF 历史数据。
691
+ 数据来源:东方财富
692
+
693
+ \b
694
+ 参数:
695
+ [SYMBOL] ETF 代码 (可选,如 159707, 510300)
696
+ --period 周期: daily/weekly/monthly (默认 daily)
697
+ --start 开始日期 YYYYMMDD
698
+ --end 结束日期 YYYYMMDD
699
+ --adjust 复权: qfq/hfq/空串
700
+
701
+ 示例:
702
+ # 列出所有 ETF 实时行情
703
+ akshare-cli fund etf --json
704
+
705
+ # 获取单只 ETF 历史数据
706
+ akshare-cli fund etf 159707 --json
707
+
708
+ # 前复权周线数据
709
+ akshare-cli fund etf 510300 --period weekly --adjust qfq --csv
710
+ """
711
+ if symbol:
712
+ kwargs = {
713
+ "symbol": symbol,
714
+ "period": period,
715
+ "start_date": start_date,
716
+ "end_date": end_date,
717
+ "adjust": adjust,
718
+ }
719
+ try:
720
+ result = call_function("fund_etf_hist_em", kwargs)
721
+ _handle_result(result, "fund_etf_hist_em", kwargs)
722
+ except Exception as e:
723
+ print_error(f"Failed: {e}")
724
+ sys.exit(1)
725
+ else:
726
+ try:
727
+ result = call_function("fund_etf_spot_em", {})
728
+ _handle_result(result, "fund_etf_spot_em", {})
729
+ except Exception as e:
730
+ print_error(f"Failed: {e}")
731
+ sys.exit(1)
732
+
733
+
734
+ @cli.group("futures")
735
+ def cmd_futures():
736
+ """期货数据快捷命令
737
+
738
+ \b
739
+ 子命令:
740
+ hist 获取期货历史 K 线数据
741
+ list 列出可用期货合约
742
+
743
+ 示例:
744
+ akshare-cli futures hist 螺纹主连 --json
745
+ akshare-cli futures list --json
746
+ """
747
+ pass
748
+
749
+
750
+ @cmd_futures.command("hist")
751
+ @click.argument("symbol")
752
+ @click.option("--period", default="daily", help="daily/weekly/monthly")
753
+ @click.option("--start", "start_date", default="19900101", help="Start date")
754
+ @click.option("--end", "end_date", default="20500101", help="End date")
755
+ @output_options
756
+ def cmd_futures_hist(symbol, period, start_date, end_date):
757
+ """获取期货历史 K 线数据
758
+
759
+ 数据来源:东方财富 (futures_hist_em)
760
+
761
+ \b
762
+ 参数:
763
+ SYMBOL 期货合约名称 (如 螺纹主连, 热卷主连)
764
+ --period 周期: daily/weekly/monthly (默认 daily)
765
+ --start 开始日期 YYYYMMDD
766
+ --end 结束日期 YYYYMMDD
767
+
768
+ 示例:
769
+ akshare-cli futures hist 螺纹主连 --json
770
+ akshare-cli futures hist 热卷主连 --period weekly --csv
771
+ akshare-cli futures hist 沥青主连 --start 20260101 --json
772
+
773
+ 提示: 先用 'futures list' 查看可用合约名称
774
+ """
775
+ kwargs = {
776
+ "symbol": symbol,
777
+ "period": period,
778
+ "start_date": start_date,
779
+ "end_date": end_date,
780
+ }
781
+ try:
782
+ result = call_function("futures_hist_em", kwargs)
783
+ _handle_result(result, "futures_hist_em", kwargs)
784
+ except Exception as e:
785
+ print_error(f"Failed: {e}")
786
+ sys.exit(1)
787
+
788
+
789
+ @cmd_futures.command("list")
790
+ @output_options
791
+ def cmd_futures_list():
792
+ """列出可用期货合约
793
+
794
+ 显示所有可查询的期货合约名称。数据来源:东方财富 (futures_hist_table_em)
795
+
796
+ \b
797
+ 示例:
798
+ akshare-cli futures list --json
799
+ akshare-cli futures list --csv --output contracts.csv
800
+ """
801
+ try:
802
+ result = call_function("futures_hist_table_em", {})
803
+ _handle_result(result, "futures_hist_table_em", {})
804
+ except Exception as e:
805
+ print_error(f"Failed: {e}")
806
+ sys.exit(1)
807
+
808
+
809
+ @cli.group("bond")
810
+ def cmd_bond():
811
+ """债券数据快捷命令
812
+
813
+ \b
814
+ 子命令:
815
+ convertible 获取可转债数据
816
+
817
+ 示例:
818
+ akshare-cli bond convertible --json
819
+
820
+ 更多债券函数可通过 call 命令调用:
821
+ akshare-cli call bond_china_yield --json
822
+ akshare-cli call bond_zh_us_rate --json
823
+ """
824
+ pass
825
+
826
+
827
+ @cmd_bond.command("convertible")
828
+ @output_options
829
+ def cmd_bond_convertible():
830
+ """获取可转债数据
831
+
832
+ 获取全市场可转债实时数据。数据来源:集思录 (bond_cb_jsl)
833
+
834
+ \b
835
+ 示例:
836
+ akshare-cli bond convertible --json
837
+ akshare-cli bond convertible --csv --output bonds.csv
838
+ akshare-cli bond convertible --json --limit 20
839
+ """
840
+ try:
841
+ result = call_function("bond_cb_jsl", {})
842
+ _handle_result(result, "bond_cb_jsl", {})
843
+ except Exception as e:
844
+ print_error(f"Failed: {e}")
845
+ sys.exit(1)
846
+
847
+
848
+ @cli.group("index")
849
+ def cmd_index():
850
+ """指数数据快捷命令
851
+
852
+ \b
853
+ 子命令:
854
+ spot 获取指数实时行情
855
+
856
+ 示例:
857
+ akshare-cli index spot --json 全球指数
858
+ akshare-cli index spot --market cn --json 中国指数
859
+ """
860
+ pass
861
+
862
+
863
+ @cmd_index.command("spot")
864
+ @click.option("--market", default="global", help="global/cn")
865
+ @output_options
866
+ def cmd_index_spot(market):
867
+ """获取指数实时行情
868
+
869
+ \b
870
+ 参数:
871
+ --market 市场选择 (默认 global)
872
+ global 全球主要指数 (index_global_spot_em)
873
+ cn 中国指数 (stock_zh_index_spot_em)
874
+
875
+ 示例:
876
+ akshare-cli index spot --json
877
+ akshare-cli index spot --market cn --json --limit 20
878
+ """
879
+ func_name = "index_global_spot_em" if market == "global" else "stock_zh_index_spot_em"
880
+ try:
881
+ result = call_function(func_name, {})
882
+ _handle_result(result, func_name, {})
883
+ except Exception as e:
884
+ print_error(f"Failed: {e}")
885
+ sys.exit(1)
886
+
887
+
888
+ @cli.group("macro")
889
+ def cmd_macro():
890
+ """宏观经济数据快捷命令
891
+
892
+ \b
893
+ 子命令:
894
+ gdp 中国 GDP 数据
895
+ cpi 中国 CPI 数据 (支持月度/年度)
896
+
897
+ 示例:
898
+ akshare-cli macro gdp --json
899
+ akshare-cli macro cpi --freq yearly --csv
900
+
901
+ 更多宏观数据可通过 call 命令调用:
902
+ akshare-cli call macro_china_ppi_yearly --json
903
+ akshare-cli call macro_china_lpr --json
904
+ akshare-cli call macro_china_m2_yearly --json
905
+ """
906
+ pass
907
+
908
+
909
+ @cmd_macro.command("gdp")
910
+ @output_options
911
+ def cmd_macro_gdp():
912
+ """获取中国 GDP 数据
913
+
914
+ 获取中国年度 GDP 数据。数据来源:macro_china_gdp_yearly
915
+
916
+ \b
917
+ 示例:
918
+ akshare-cli macro gdp --json
919
+ akshare-cli macro gdp --csv --output gdp.csv
920
+ """
921
+ try:
922
+ result = call_function("macro_china_gdp_yearly", {})
923
+ _handle_result(result, "macro_china_gdp_yearly", {})
924
+ except Exception as e:
925
+ print_error(f"Failed: {e}")
926
+ sys.exit(1)
927
+
928
+
929
+ @cmd_macro.command("cpi")
930
+ @click.option("--freq", default="monthly", help="monthly/yearly")
931
+ @output_options
932
+ def cmd_macro_cpi(freq):
933
+ """获取中国 CPI 数据
934
+
935
+ 获取中国消费者物价指数 (CPI) 数据。
936
+
937
+ \b
938
+ 参数:
939
+ --freq 频率: monthly(月度)/yearly(年度) (默认 monthly)
940
+
941
+ 示例:
942
+ # 月度 CPI
943
+ akshare-cli macro cpi --json
944
+
945
+ # 年度 CPI
946
+ akshare-cli macro cpi --freq yearly --csv
947
+ """
948
+ func_name = f"macro_china_cpi_{freq}"
949
+ try:
950
+ result = call_function(func_name, {})
951
+ _handle_result(result, func_name, {})
952
+ except Exception as e:
953
+ print_error(f"Failed: {e}")
954
+ sys.exit(1)
955
+
956
+
957
+ @cli.group("forex")
958
+ def cmd_forex():
959
+ """外汇数据快捷命令
960
+
961
+ \b
962
+ 子命令:
963
+ spot 获取外汇实时汇率
964
+ hist 获取外汇历史汇率
965
+
966
+ 示例:
967
+ akshare-cli forex spot --json
968
+ akshare-cli forex hist USDCNH --json
969
+ """
970
+ pass
971
+
972
+
973
+ @cmd_forex.command("spot")
974
+ @output_options
975
+ def cmd_forex_spot():
976
+ """获取外汇实时汇率
977
+
978
+ 获取主要货币对实时汇率。数据来源:东方财富 (forex_spot_em)
979
+
980
+ \b
981
+ 示例:
982
+ akshare-cli forex spot --json
983
+ akshare-cli forex spot --csv --limit 10
984
+ """
985
+ try:
986
+ result = call_function("forex_spot_em", {})
987
+ _handle_result(result, "forex_spot_em", {})
988
+ except Exception as e:
989
+ print_error(f"Failed: {e}")
990
+ sys.exit(1)
991
+
992
+
993
+ @cmd_forex.command("hist")
994
+ @click.argument("symbol", default="USDCNH")
995
+ @output_options
996
+ def cmd_forex_hist(symbol):
997
+ """获取外汇历史汇率
998
+
999
+ 数据来源:东方财富 (forex_hist_em)
1000
+
1001
+ \b
1002
+ 参数:
1003
+ [SYMBOL] 货币对 (默认 USDCNH)
1004
+ 常用: USDCNH, EURUSD, GBPUSD, USDJPY
1005
+
1006
+ 示例:
1007
+ akshare-cli forex hist USDCNH --json
1008
+ akshare-cli forex hist EURUSD --csv
1009
+ """
1010
+ kwargs = {"symbol": symbol}
1011
+ try:
1012
+ result = call_function("forex_hist_em", kwargs)
1013
+ _handle_result(result, "forex_hist_em", kwargs)
1014
+ except Exception as e:
1015
+ print_error(f"Failed: {e}")
1016
+ sys.exit(1)
1017
+
1018
+
1019
+ @cli.group("option")
1020
+ def cmd_option():
1021
+ """期权数据快捷命令
1022
+
1023
+ \b
1024
+ 子命令:
1025
+ info 获取期权合约信息
1026
+
1027
+ 示例:
1028
+ akshare-cli option info --json
1029
+ """
1030
+ pass
1031
+
1032
+
1033
+ @cmd_option.command("info")
1034
+ @output_options
1035
+ def cmd_option_info():
1036
+ """获取期权合约信息
1037
+
1038
+ 获取期权合约基本信息。数据来源:option_contract_info_ctp
1039
+
1040
+ \b
1041
+ 示例:
1042
+ akshare-cli option info --json
1043
+ akshare-cli option info --csv --output options.csv
1044
+ """
1045
+ try:
1046
+ result = call_function("option_contract_info_ctp", {})
1047
+ _handle_result(result, "option_contract_info_ctp", {})
1048
+ except Exception as e:
1049
+ print_error(f"Failed: {e}")
1050
+ sys.exit(1)
1051
+
1052
+
1053
+ # ─── EXPORT ──────────────────────────────────────────────────────────────────
1054
+
1055
+
1056
+ @cli.command("export")
1057
+ @click.argument("filepath")
1058
+ def cmd_export(filepath):
1059
+ """导出上一次查询结果到文件
1060
+
1061
+ 将最近一次查询返回的 DataFrame 导出到指定文件。
1062
+
1063
+ \b
1064
+ 支持格式:
1065
+ .csv CSV 文件
1066
+ .json JSON 文件
1067
+ .xlsx Excel 文件
1068
+ .md Markdown 表格
1069
+
1070
+ 示例:
1071
+ akshare-cli export result.csv
1072
+ akshare-cli export data.xlsx
1073
+ akshare-cli export report.md
1074
+
1075
+ 注意: 需要先运行查询命令,否则无数据可导出
1076
+ """
1077
+ if _session.last_result is None:
1078
+ print_error("No data to export. Run a query first.")
1079
+ sys.exit(1)
1080
+
1081
+ try:
1082
+ saved = auto_export(_session.last_result, filepath)
1083
+ click.echo(f"Exported to {saved}")
1084
+ except Exception as e:
1085
+ print_error(f"Export failed: {e}")
1086
+ sys.exit(1)
1087
+
1088
+
1089
+ # ─── REPL ────────────────────────────────────────────────────────────────────
1090
+
1091
+
1092
+ @cli.command("repl")
1093
+ def cmd_repl():
1094
+ """启动交互式 REPL 会话
1095
+
1096
+ 进入交互模式,可以连续执行多个查询,保留会话状态。
1097
+
1098
+ \b
1099
+ REPL 命令:
1100
+ call <函数> [--参数 值 ...] 调用函数
1101
+ search <关键词> 搜索函数
1102
+ list [域] 列出函数
1103
+ info <函数> 查看函数详情
1104
+ export <文件路径> 导出上次结果
1105
+ history 查看历史记录
1106
+ set <键> <值> 设置偏好
1107
+ json on|off 切换 JSON 输出
1108
+ help 显示帮助
1109
+ quit / exit / q 退出
1110
+
1111
+ 所有命令都支持 --json, --csv, --output 输出选项。
1112
+
1113
+ 示例:
1114
+ akshare-cli repl
1115
+ """
1116
+ click.echo("AKShare CLI REPL - Type 'help' for commands, 'quit' to exit.")
1117
+ click.echo(f"Loaded {len(get_all_functions())} functions.\n")
1118
+
1119
+ while True:
1120
+ try:
1121
+ line = input("akshare> ").strip()
1122
+ except (EOFError, KeyboardInterrupt):
1123
+ click.echo("\nBye!")
1124
+ break
1125
+
1126
+ if not line:
1127
+ continue
1128
+
1129
+ if line in ("quit", "exit", "q"):
1130
+ click.echo("Bye!")
1131
+ break
1132
+
1133
+ if line == "help":
1134
+ click.echo(
1135
+ "Commands:\n"
1136
+ " call <func> [--param value ...] Call function\n"
1137
+ " search <keyword> Search functions\n"
1138
+ " list [domain] List functions\n"
1139
+ " info <func> Function details\n"
1140
+ " export <filepath> Export last result\n"
1141
+ " history Show history\n"
1142
+ " set <key> <value> Set preference\n"
1143
+ " json on|off Toggle JSON output\n"
1144
+ " quit Exit\n"
1145
+ "\nAll commands support --json, --csv, --output flags.\n"
1146
+ )
1147
+ continue
1148
+
1149
+ if line == "history":
1150
+ for i, entry in enumerate(_session.history):
1151
+ click.echo(
1152
+ f" {i+1}. [{entry['timestamp'][:19]}] {entry['function']} "
1153
+ f"-> {entry.get('rows', '?')} rows"
1154
+ )
1155
+ continue
1156
+
1157
+ if line.startswith("json "):
1158
+ mode = line.split()[1]
1159
+ if mode == "on":
1160
+ _output_config.format = "json"
1161
+ click.echo("JSON output enabled.")
1162
+ else:
1163
+ _output_config.format = "table"
1164
+ click.echo("Table output enabled.")
1165
+ continue
1166
+
1167
+ if line.startswith("set "):
1168
+ parts = line.split(maxsplit=2)
1169
+ if len(parts) == 3:
1170
+ _session.set_preference(parts[1], parts[2])
1171
+ click.echo(f"Set {parts[1]} = {parts[2]}")
1172
+ else:
1173
+ click.echo("Usage: set <key> <value>")
1174
+ continue
1175
+
1176
+ # Parse and dispatch REPL commands through Click
1177
+ try:
1178
+ args = shlex.split(line)
1179
+ except ValueError:
1180
+ args = line.split()
1181
+
1182
+ if not args:
1183
+ continue
1184
+
1185
+ try:
1186
+ cli.main(args=args, standalone_mode=False)
1187
+ except SystemExit:
1188
+ pass
1189
+ except Exception as e:
1190
+ print_error(str(e))
1191
+ if os.environ.get("AKSHARE_CLI_DEBUG"):
1192
+ traceback.print_exc()
1193
+
1194
+
1195
+ # ─── VERSION ─────────────────────────────────────────────────────────────────
1196
+
1197
+
1198
+ @cli.command("version")
1199
+ @output_options
1200
+ def cmd_version():
1201
+ """显示版本信息
1202
+
1203
+ 显示 CLI 工具和 akshare 库的版本号。
1204
+
1205
+ \b
1206
+ 示例:
1207
+ akshare-cli version
1208
+ akshare-cli version --json
1209
+ """
1210
+ from akshare_cli import __version__ as harness_version
1211
+
1212
+ info = {"cli_harness_version": harness_version}
1213
+ try:
1214
+ ak_mod = __import__("akshare")
1215
+ info["akshare_version"] = ak_mod.__version__
1216
+ except ImportError:
1217
+ info["akshare_version"] = "not installed"
1218
+
1219
+ if _output_config.format == "json":
1220
+ click.echo(json.dumps(info, indent=2))
1221
+ else:
1222
+ for k, v in info.items():
1223
+ click.echo(f"{k}: {v}")
1224
+
1225
+
1226
+ # ─── CACHE MANAGEMENT ────────────────────────────────────────────────────────
1227
+
1228
+
1229
+ @cli.group("cache")
1230
+ def cmd_cache():
1231
+ """管理结果缓存
1232
+
1233
+ 查看缓存统计信息或清空缓存。API 调用结果会自动缓存以加速重复查询。
1234
+ 实时行情类接口不会被缓存。
1235
+
1236
+ \b
1237
+ 子命令:
1238
+ stats 显示缓存命中率等统计信息
1239
+ clear 清空所有缓存
1240
+
1241
+ 示例:
1242
+ akshare-cli cache stats
1243
+ akshare-cli cache clear
1244
+ """
1245
+ pass
1246
+
1247
+
1248
+ @cmd_cache.command("stats")
1249
+ def cache_stats():
1250
+ """显示缓存统计信息
1251
+
1252
+ \b
1253
+ 示例:
1254
+ akshare-cli cache stats
1255
+ """
1256
+ stats = _result_cache.stats()
1257
+ if _output_config.format == "json":
1258
+ click.echo(json.dumps(stats, indent=2, ensure_ascii=False))
1259
+ else:
1260
+ click.echo(f"缓存状态: {'启用' if stats['enabled'] else '禁用'}")
1261
+ click.echo(f"缓存条目: {stats['entries']}")
1262
+ click.echo(f"命中次数: {stats['hits']}")
1263
+ click.echo(f"未命中: {stats['misses']}")
1264
+ click.echo(f"命中率: {stats['hit_rate']}")
1265
+
1266
+
1267
+ @cmd_cache.command("clear")
1268
+ def cache_clear():
1269
+ """清空所有缓存
1270
+
1271
+ \b
1272
+ 示例:
1273
+ akshare-cli cache clear
1274
+ """
1275
+ _result_cache.clear()
1276
+ click.echo("缓存已清空。")
1277
+
1278
+
1279
+ def main():
1280
+ """Entry point for console_scripts."""
1281
+ cli()
1282
+
1283
+
1284
+ if __name__ == "__main__":
1285
+ main()