feat: 接入同花顺官方SDK,新增v2数据接口,股票详情K线改用v2(前复权日K)
- vendor 同花顺官方 SDK 到 backend/sdk(含K线>10年自动切片、重试、拼音首字母检索兜底) - 新增 /api/v2 路由:行情/估值/财务/日历/指数/K线/标的检索 - 股票详情页K线改用 v2 同花顺接口(前复权日K+总手+按昨收涨跌幅) - 密钥仅后端持有,响应/日志无泄露 - 新增 run_local.sh 本地直接拉起(不再依赖 docker)
This commit is contained in:
+10
-2
@@ -7,7 +7,7 @@ from contextlib import asynccontextmanager
|
||||
from dotenv import load_dotenv
|
||||
|
||||
from database import init_db
|
||||
from routes import stock, collections, shares, themes, core_stocks
|
||||
from routes import stock, collections, shares, themes, core_stocks, fuyao
|
||||
from services.daily_collector import collector_loop, cache_cleanup_loop
|
||||
|
||||
load_dotenv()
|
||||
@@ -45,18 +45,26 @@ app.include_router(collections.router, prefix="/api/collections")
|
||||
app.include_router(shares.router, prefix="/api/share")
|
||||
app.include_router(themes.router, prefix="/api/themes")
|
||||
app.include_router(core_stocks.router, prefix="/api/core-stocks")
|
||||
app.include_router(fuyao.router, prefix="/api/v2")
|
||||
|
||||
# 生产模式:后端同时托管前端静态文件
|
||||
# catch-all 路由在 API 路由之后注册,所以 API 优先级更高
|
||||
dist_path = os.path.join(os.path.dirname(__file__), "dist")
|
||||
|
||||
# HTML 不缓存:保证 index.html 永远最新(引用的资源文件名带 hash,可长缓存)
|
||||
_NO_CACHE_HTML = {"Cache-Control": "no-cache, no-store, must-revalidate"}
|
||||
|
||||
if os.path.isdir(dist_path):
|
||||
@app.get("/{full_path:path}")
|
||||
async def serve_spa(full_path: str):
|
||||
file_path = os.path.join(dist_path, full_path) if full_path else os.path.join(dist_path, "index.html")
|
||||
if os.path.isfile(file_path):
|
||||
# 静态资源(带 hash 的文件名)可缓存;HTML 不缓存
|
||||
if file_path.endswith(".html"):
|
||||
return FileResponse(file_path, headers=_NO_CACHE_HTML)
|
||||
return FileResponse(file_path)
|
||||
# SPA fallback: 非文件路径统一返回 index.html
|
||||
index_path = os.path.join(dist_path, "index.html")
|
||||
if os.path.isfile(index_path):
|
||||
return FileResponse(index_path, media_type="text/html")
|
||||
return FileResponse(index_path, media_type="text/html", headers=_NO_CACHE_HTML)
|
||||
return JSONResponse({"detail": "Not Found"}, status_code=404)
|
||||
|
||||
@@ -3,4 +3,6 @@ uvicorn==0.30.0
|
||||
httpx==0.27.0
|
||||
python-dotenv==1.0.1
|
||||
akshare==1.18.64
|
||||
mootdx
|
||||
mootdx
|
||||
requests>=2.31,<3
|
||||
pypinyin
|
||||
@@ -0,0 +1,165 @@
|
||||
"""v2 数据接口路由:同花顺官方金融数据 API(/api/v2)
|
||||
|
||||
保留现有 /api/* 为 v1(腾讯/东财/新浪等抓取源),本模块提供独立 v2。
|
||||
数据源:https://fuyao.aicubes.cn(同花顺官方),密钥仅后端持有。
|
||||
|
||||
接口返回统一使用现有 v1 的 data 信封风格:{"data": ..., "count": ...},
|
||||
上游错误转 HTTPException,密钥永不出现在响应中。
|
||||
"""
|
||||
|
||||
from fastapi import APIRouter, Query, HTTPException
|
||||
from fastapi.responses import JSONResponse
|
||||
from services import fuyao_client
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
# 上游反代/CDN 可能按 path 缓存,显式禁止缓存
|
||||
_NO_CACHE_HEADERS = {"Cache-Control": "no-store, no-cache, must-revalidate, max-age=0"}
|
||||
|
||||
|
||||
async def _guard(awaitable):
|
||||
"""调用 fuyao_client,把上游/校验错误统一转 HTTPException"""
|
||||
try:
|
||||
return await awaitable
|
||||
except fuyao_client.FuyaoError as e:
|
||||
# 上游业务错误,message 来自上游,不含密钥
|
||||
raise HTTPException(status_code=502, detail=f"同花顺API: {e}")
|
||||
except ValueError as e:
|
||||
# SDK 参数校验错误(如 thscode 格式)
|
||||
raise HTTPException(status_code=400, detail=f"参数错误: {e}")
|
||||
|
||||
|
||||
# ---------------------------------------------------------------
|
||||
# 基础数据
|
||||
# ---------------------------------------------------------------
|
||||
|
||||
@router.get("/meta/tickers/search", summary="v2 标的检索")
|
||||
async def v2_ticker_search(
|
||||
q: str = Query(..., description="代码/名称/拼音关键词"),
|
||||
exchange: str = Query(None, description="交易所 SH/SZ/BJ"),
|
||||
asset_type: str = Query(None, description="资产类型 a-share 等"),
|
||||
limit: int = Query(10, ge=1, le=50),
|
||||
):
|
||||
items = await _guard(fuyao_client.ticker_search(q, exchange, asset_type, limit))
|
||||
return JSONResponse({"data": items, "count": len(items)}, headers=_NO_CACHE_HEADERS)
|
||||
|
||||
|
||||
@router.get("/meta/tickers/list", summary="v2 标的列表(分页)")
|
||||
async def v2_ticker_list(
|
||||
asset_type: str = Query(None, description="资产类型"),
|
||||
limit: int = Query(100, ge=1, le=10000),
|
||||
offset: int = Query(0, ge=0),
|
||||
):
|
||||
items = await _guard(fuyao_client.ticker_list(asset_type, limit, offset))
|
||||
return JSONResponse({"data": items, "count": len(items)}, headers=_NO_CACHE_HEADERS)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------
|
||||
# A股行情 / 日历 / 竞价
|
||||
# ---------------------------------------------------------------
|
||||
|
||||
@router.get("/prices/snapshot", summary="v2 行情快照(单只/多只)")
|
||||
async def v2_prices_snapshot(thscodes: str = Query(..., description="逗号分隔的 thscode,如 600519.SH,000001.SZ")):
|
||||
items = await _guard(fuyao_client.prices_snapshot(thscodes))
|
||||
return JSONResponse({"data": items, "count": len(items)}, headers=_NO_CACHE_HEADERS)
|
||||
|
||||
|
||||
@router.get("/prices/historical", summary="v2 历史日K(窗口≤10年)")
|
||||
async def v2_prices_historical(
|
||||
thscode: str = Query(..., description="标的 thscode,单只"),
|
||||
start: int = Query(..., description="起始时间,毫秒 Unix 时间戳"),
|
||||
end: int = Query(..., description="结束时间,毫秒 Unix 时间戳"),
|
||||
adjust: str = Query("forward", description="复权 none/forward/backward"),
|
||||
):
|
||||
try:
|
||||
items = await fuyao_client.prices_historical(thscode, start, end, adjust)
|
||||
except fuyao_client.FuyaoError as e:
|
||||
raise HTTPException(status_code=502, detail=f"同花顺API: {e}")
|
||||
except ValueError as e:
|
||||
raise HTTPException(status_code=400, detail=f"参数错误: {e}")
|
||||
return JSONResponse({"data": items, "count": len(items)}, headers=_NO_CACHE_HEADERS)
|
||||
|
||||
|
||||
@router.get("/calendar/trading-days", summary="v2 近一年交易日序列")
|
||||
async def v2_calendar():
|
||||
items = await _guard(fuyao_client.calendar_trading_days())
|
||||
return JSONResponse({"data": items, "count": len(items)}, headers=_NO_CACHE_HEADERS)
|
||||
|
||||
|
||||
@router.get("/auction/snapshot", summary="v2 集合竞价快照")
|
||||
async def v2_auction_snapshot(
|
||||
thscodes: str = Query(..., description="逗号分隔 thscode"),
|
||||
stage: str = Query("final", description="live 实时 / final 终态"),
|
||||
):
|
||||
data = await _guard(fuyao_client.auction_snapshot(thscodes, stage))
|
||||
return JSONResponse({"data": data}, headers=_NO_CACHE_HEADERS)
|
||||
|
||||
|
||||
@router.get("/auction/short-term-benchmark", summary="v2 短线风向标竞价基准")
|
||||
async def v2_auction_benchmark(date: str = Query(None, description="日期 yyyy-MM-dd")):
|
||||
data = await _guard(fuyao_client.auction_short_term_benchmark(date))
|
||||
return JSONResponse({"data": data}, headers=_NO_CACHE_HEADERS)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------
|
||||
# A股财务 / 估值
|
||||
# ---------------------------------------------------------------
|
||||
|
||||
@router.get("/financials/{statement}", summary="v2 三大财务报表")
|
||||
async def v2_financials(
|
||||
statement: str,
|
||||
thscode: str = Query(..., description="标的 thscode"),
|
||||
period: str = Query("annual", description="annual 年报 / quarterly 季报"),
|
||||
limit: int = Query(6, ge=1, le=20),
|
||||
):
|
||||
if statement not in ("income-statements", "balance-sheets", "cash-flow-statements"):
|
||||
raise HTTPException(status_code=400, detail="statement 仅支持 income-statements/balance-sheets/cash-flow-statements")
|
||||
items = await _guard(fuyao_client.financials(statement, thscode, period, limit))
|
||||
return JSONResponse({"data": items, "count": len(items)}, headers=_NO_CACHE_HEADERS)
|
||||
|
||||
|
||||
@router.get("/financials/indicators", summary="v2 五类财务指标")
|
||||
async def v2_financial_indicators(
|
||||
thscode: str = Query(..., description="标的 thscode"),
|
||||
report: str = Query(..., description="报告期 yyyy-1 ~ yyyy-4"),
|
||||
):
|
||||
items = await _guard(fuyao_client.financial_indicators(thscode, report))
|
||||
return JSONResponse({"data": items, "count": len(items)}, headers=_NO_CACHE_HEADERS)
|
||||
|
||||
|
||||
@router.get("/valuations/snapshot", summary="v2 估值快照")
|
||||
async def v2_valuations_snapshot(thscodes: str = Query(..., description="逗号分隔 thscode")):
|
||||
items = await _guard(fuyao_client.valuations_snapshot(thscodes))
|
||||
return JSONResponse({"data": items, "count": len(items)}, headers=_NO_CACHE_HEADERS)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------
|
||||
# 指数 / 板块
|
||||
# ---------------------------------------------------------------
|
||||
|
||||
@router.get("/index/catalog", summary="v2 同花顺指数清单")
|
||||
async def v2_index_catalog(tag: str = Query("industry", description="cn_concept/region/tszs/industry")):
|
||||
items = await _guard(fuyao_client.index_catalog(tag))
|
||||
return JSONResponse({"data": items, "count": len(items)}, headers=_NO_CACHE_HEADERS)
|
||||
|
||||
|
||||
@router.get("/index/constituents", summary="v2 指数成分股")
|
||||
async def v2_index_constituents(thscode: str = Query(..., description="指数 thscode")):
|
||||
items = await _guard(fuyao_client.index_constituents(thscode))
|
||||
return JSONResponse({"data": items, "count": len(items)}, headers=_NO_CACHE_HEADERS)
|
||||
|
||||
|
||||
@router.get("/index/prices/snapshot", summary="v2 指数行情快照")
|
||||
async def v2_index_prices_snapshot(thscodes: str = Query(..., description="逗号分隔指数 thscode")):
|
||||
items = await _guard(fuyao_client.index_prices_snapshot(thscodes))
|
||||
return JSONResponse({"data": items, "count": len(items)}, headers=_NO_CACHE_HEADERS)
|
||||
|
||||
|
||||
@router.get("/index/prices/historical", summary="v2 指数历史K线")
|
||||
async def v2_index_prices_historical(
|
||||
thscode: str = Query(..., description="指数 thscode"),
|
||||
start: int = Query(..., description="起始时间,毫秒 Unix 时间戳"),
|
||||
end: int = Query(..., description="结束时间,毫秒 Unix 时间戳"),
|
||||
):
|
||||
items = await _guard(fuyao_client.index_prices_historical(thscode, start, end))
|
||||
return JSONResponse({"data": items, "count": len(items)}, headers=_NO_CACHE_HEADERS)
|
||||
@@ -0,0 +1,101 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
import sys
|
||||
from collections.abc import Mapping
|
||||
from pathlib import Path
|
||||
|
||||
CANONICAL_API_KEY_ENV = "HITHINK_FINANCE_API_KEY"
|
||||
# AUV 项目自有约定:同花顺 key 存在 .env 的 fuyao_apikey(由 load_dotenv 加载)
|
||||
PROJECT_API_KEY_ENV = "fuyao_apikey"
|
||||
LEGACY_API_KEY_ENVS = ("FUYAO_TOKEN", "API_KEY")
|
||||
|
||||
|
||||
class CredentialFileError(RuntimeError):
|
||||
"""Raised when the user credential file exists but cannot be read safely."""
|
||||
|
||||
|
||||
def credential_file_path(
|
||||
*,
|
||||
platform: str | None = None,
|
||||
env: Mapping[str, str] | None = None,
|
||||
home: Path | None = None,
|
||||
) -> Path:
|
||||
resolved_platform = platform or sys.platform
|
||||
resolved_env = env if env is not None else os.environ
|
||||
resolved_home = home or Path.home()
|
||||
|
||||
if resolved_platform == "win32":
|
||||
config_root = resolved_env.get("APPDATA", "").strip()
|
||||
base = Path(config_root) if config_root else resolved_home / "AppData" / "Roaming"
|
||||
elif resolved_platform == "darwin":
|
||||
base = resolved_home / "Library" / "Application Support"
|
||||
else:
|
||||
config_root = resolved_env.get("XDG_CONFIG_HOME", "").strip()
|
||||
base = Path(config_root).expanduser() if config_root else resolved_home / ".config"
|
||||
return base / "hithink-finance" / "credentials.env"
|
||||
|
||||
|
||||
def _non_blank(value: str | None) -> str | None:
|
||||
if value is None:
|
||||
return None
|
||||
stripped = value.strip()
|
||||
return stripped or None
|
||||
|
||||
|
||||
def _read_credential_file(path: Path) -> str | None:
|
||||
if not path.exists():
|
||||
return None
|
||||
try:
|
||||
content = path.read_text(encoding="utf-8")
|
||||
except OSError as exc:
|
||||
raise CredentialFileError(
|
||||
f"Unable to read hithink finance credential file: {path}"
|
||||
) from exc
|
||||
|
||||
for line_number, raw_line in enumerate(content.splitlines(), start=1):
|
||||
line = raw_line.strip()
|
||||
if not line or line.startswith("#") or "=" not in line:
|
||||
continue
|
||||
name, value = line.split("=", 1)
|
||||
if name.strip() != CANONICAL_API_KEY_ENV:
|
||||
continue
|
||||
normalized = value.strip()
|
||||
if "'" in normalized or '"' in normalized:
|
||||
raise CredentialFileError(
|
||||
f"Invalid hithink finance credential file at {path}:{line_number}: "
|
||||
f"{CANONICAL_API_KEY_ENV} must not be quoted."
|
||||
)
|
||||
return _non_blank(normalized)
|
||||
return None
|
||||
|
||||
|
||||
def resolve_api_key(
|
||||
*,
|
||||
env: Mapping[str, str] | None = None,
|
||||
credential_path: Path | None = None,
|
||||
) -> str | None:
|
||||
resolved_env = env if env is not None else os.environ
|
||||
|
||||
canonical = _non_blank(resolved_env.get(CANONICAL_API_KEY_ENV))
|
||||
if canonical is not None:
|
||||
return canonical
|
||||
|
||||
# AUV 项目约定:优先读 .env 的 fuyao_apikey
|
||||
project_key = _non_blank(resolved_env.get(PROJECT_API_KEY_ENV))
|
||||
if project_key is not None:
|
||||
return project_key
|
||||
|
||||
stored = _read_credential_file(
|
||||
credential_path
|
||||
if credential_path is not None
|
||||
else credential_file_path(env=resolved_env)
|
||||
)
|
||||
if stored is not None:
|
||||
return stored
|
||||
|
||||
for name in LEGACY_API_KEY_ENVS:
|
||||
legacy = _non_blank(resolved_env.get(name))
|
||||
if legacy is not None:
|
||||
return legacy
|
||||
return None
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,202 @@
|
||||
"""同花顺金融数据 API 客户端(v2 数据源,接口前缀 /api/v2)
|
||||
|
||||
本模块是基于官方 Python SDK(vendor 在 backend/sdk/)的薄封装,
|
||||
对外暴露与 /api/v2 路由匹配的 async 函数,并统一返回 list/dict。
|
||||
|
||||
- SDK 提供:历史K线 >10 年自动切片、重试、参数校验、标的缓存。
|
||||
- SDK 内部用同步 requests,这里用 asyncio.to_thread 桥接,避免阻塞事件循环。
|
||||
- API Key 由 SDK 的 credentials resolver 读取(兼容 .env 的 fuyao_apikey),
|
||||
**绝不写入代码、日志、错误信息或 git。**
|
||||
|
||||
官方 SDK 来源:https://github.com/HiThink-Tech/Financial-API (MIT)
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import time
|
||||
from typing import Any, Iterable, Optional
|
||||
|
||||
from sdk import fuyao_client as _sdk
|
||||
from sdk.fuyao_client import FuyaoApiError # 复用官方错误类型
|
||||
|
||||
# 兼容旧路由引用名(routes/fuyao.py 用 FuyaoError)
|
||||
FuyaoError = FuyaoApiError
|
||||
|
||||
|
||||
# ---------------------------------------------------------------
|
||||
# 拼音首字母检索(同花顺接口不支持拼音,本地补齐)
|
||||
# ---------------------------------------------------------------
|
||||
|
||||
# 全市场 A 股标的列表缓存(用于拼音首字母匹配)
|
||||
_ticker_cache: dict = {"data": None, "at": 0.0}
|
||||
_TICKER_CACHE_TTL = 12 * 3600 # 12 小时
|
||||
|
||||
|
||||
async def _ensure_a_share_tickers() -> list[dict]:
|
||||
"""拉取全市场 A 股标的列表并缓存(幂等,TTL 内复用)"""
|
||||
now = time.time()
|
||||
if _ticker_cache["data"] is not None and now - _ticker_cache["at"] < _TICKER_CACHE_TTL:
|
||||
return _ticker_cache["data"]
|
||||
items = await _run(_sdk.tickers_list, asset_type="a-share", limit=10000, offset=0)
|
||||
data = _item_list(items)
|
||||
_ticker_cache["data"] = data
|
||||
_ticker_cache["at"] = now
|
||||
return data
|
||||
|
||||
|
||||
def _pinyin_initials(name: str) -> str:
|
||||
"""中文名 → 拼音首字母(如 深科技→skj,TCL科技→tkj)"""
|
||||
try:
|
||||
from pypinyin import lazy_pinyin
|
||||
except ImportError:
|
||||
return ""
|
||||
return "".join(
|
||||
w[0] for w in lazy_pinyin(name)
|
||||
if w and w[0].isalpha()
|
||||
).lower()
|
||||
|
||||
|
||||
def _match_by_pinyin(query: str, tickers: list[dict], limit: int) -> list[dict]:
|
||||
"""按拼音首字母匹配:精确=前缀优先,包含匹配次之"""
|
||||
q = query.lower()
|
||||
exact: list[dict] = []
|
||||
prefix: list[dict] = []
|
||||
contains: list[dict] = []
|
||||
for t in tickers:
|
||||
name = t.get("name") or ""
|
||||
if not name:
|
||||
continue
|
||||
initials = _pinyin_initials(name)
|
||||
if not initials:
|
||||
continue
|
||||
if initials == q:
|
||||
exact.append(t)
|
||||
elif initials.startswith(q):
|
||||
prefix.append(t)
|
||||
elif q in initials:
|
||||
contains.append(t)
|
||||
return (exact + prefix + contains)[:limit]
|
||||
|
||||
|
||||
def _run(fn, *args, **kwargs):
|
||||
"""同步 SDK 调用桥接到 async"""
|
||||
return asyncio.to_thread(fn, *args, **kwargs)
|
||||
|
||||
|
||||
def _item_list(data, key="item"):
|
||||
"""把 SDK 返回的 dict 信封或 list 统一成 item list"""
|
||||
if isinstance(data, dict):
|
||||
return data.get(key, []) or []
|
||||
if isinstance(data, list):
|
||||
return data or []
|
||||
return []
|
||||
|
||||
|
||||
# ---------------------------------------------------------------
|
||||
# 基础数据:标的检索 / 标的列表
|
||||
# ---------------------------------------------------------------
|
||||
|
||||
async def ticker_search(q: str, exchange: Optional[str] = None,
|
||||
asset_type: Optional[str] = None, limit: int = 10) -> list[dict]:
|
||||
items = await _run(_sdk.tickers_search, q, exchange=exchange,
|
||||
asset_type=asset_type, limit=limit)
|
||||
result = _item_list(items)
|
||||
# 拼音首字母兜底:同花顺无结果 且 输入是纯字母(如 skj)时,本地按拼音首字母匹配
|
||||
if not result and q and q.strip().isalpha():
|
||||
try:
|
||||
tickers = await _ensure_a_share_tickers()
|
||||
result = await asyncio.to_thread(_match_by_pinyin, q, tickers, limit)
|
||||
except Exception:
|
||||
pass # 拼音匹配失败不影响主流程
|
||||
return result
|
||||
|
||||
|
||||
async def ticker_list(asset_type: Optional[str] = None, limit: int = 100,
|
||||
offset: int = 0) -> list[dict]:
|
||||
items = await _run(_sdk.tickers_list, asset_type=asset_type or "a-share",
|
||||
limit=limit, offset=offset)
|
||||
return _item_list(items)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------
|
||||
# A股行情 / 日历 / 竞价
|
||||
# ---------------------------------------------------------------
|
||||
|
||||
async def prices_snapshot(thscodes: str) -> list[dict]:
|
||||
"""行情快照(单只/多只,逗号分隔)"""
|
||||
codes = [c.strip() for c in thscodes.split(",") if c.strip()]
|
||||
items = await _run(_sdk.prices_snapshot, codes)
|
||||
return _item_list(items)
|
||||
|
||||
|
||||
async def prices_historical(thscode: str, start_ms: int, end_ms: int,
|
||||
adjust: str = "forward", offset: int = 0) -> list[dict]:
|
||||
"""历史日K(毫秒时间戳)。SDK 自动处理 >10 年窗口切片与去重排序。"""
|
||||
return await _run(_sdk.prices_historical, thscode, start_ms, end_ms,
|
||||
interval="1d", adjust=adjust)
|
||||
|
||||
|
||||
async def calendar_trading_days() -> list[dict]:
|
||||
return await _run(_sdk.calendar_trading_days)
|
||||
|
||||
|
||||
async def auction_snapshot(thscodes: str, stage: str = "final") -> dict:
|
||||
codes = [c.strip() for c in thscodes.split(",") if c.strip()]
|
||||
data = await _run(_sdk.a_share_auction_snapshot, codes, stage=stage)
|
||||
return data or {}
|
||||
|
||||
|
||||
async def auction_short_term_benchmark(date: Optional[str] = None) -> dict:
|
||||
data = await _run(_sdk.a_share_auction_short_term_benchmark, date=date)
|
||||
return data or {}
|
||||
|
||||
|
||||
# ---------------------------------------------------------------
|
||||
# A股财务 / 估值
|
||||
# ---------------------------------------------------------------
|
||||
|
||||
async def financials(statement: str, thscode: str, period: str = "annual",
|
||||
limit: int = 6) -> list[dict]:
|
||||
fn_map = {
|
||||
"income-statements": _sdk.financials_income_statements,
|
||||
"balance-sheets": _sdk.financials_balance_sheets,
|
||||
"cash-flow-statements": _sdk.financials_cash_flow_statements,
|
||||
}
|
||||
fn = fn_map[statement]
|
||||
items = await _run(fn, thscode, period=period, limit=limit)
|
||||
return _item_list(items)
|
||||
|
||||
|
||||
async def financial_indicators(thscode: str, report: str) -> list[dict]:
|
||||
data = await _run(_sdk.financials_indicators, thscode, report)
|
||||
return _item_list(data)
|
||||
|
||||
|
||||
async def valuations_snapshot(thscodes: str) -> list[dict]:
|
||||
codes = [c.strip() for c in thscodes.split(",") if c.strip()]
|
||||
data = await _run(_sdk.a_share_valuations_snapshot, codes)
|
||||
return _item_list(data)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------
|
||||
# 指数 / 板块
|
||||
# ---------------------------------------------------------------
|
||||
|
||||
async def index_catalog(tag: str = "industry") -> list[dict]:
|
||||
items = await _run(_sdk.index_catalog_ths_index_list, tag=tag)
|
||||
return _item_list(items)
|
||||
|
||||
|
||||
async def index_constituents(thscode: str) -> list[dict]:
|
||||
items = await _run(_sdk.index_constituents_ths_stock_list, thscode)
|
||||
return _item_list(items)
|
||||
|
||||
|
||||
async def index_prices_snapshot(thscodes: str) -> list[dict]:
|
||||
codes = [c.strip() for c in thscodes.split(",") if c.strip()]
|
||||
items = await _run(_sdk.index_prices_snapshot, codes)
|
||||
return _item_list(items)
|
||||
|
||||
|
||||
async def index_prices_historical(thscode: str, start_ms: int, end_ms: int) -> list[dict]:
|
||||
items = await _run(_sdk.index_prices_historical, thscode, start_ms, end_ms)
|
||||
return _item_list(items)
|
||||
Reference in New Issue
Block a user