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:
@@ -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
|
||||
Reference in New Issue
Block a user