287 lines
10 KiB
Python
287 lines
10 KiB
Python
|
|
"""
|
|||
|
|
Wind 客户端 — 直连 WindPy,自动检测登录状态,返回 pandas DataFrame。
|
|||
|
|
|
|||
|
|
用法:
|
|||
|
|
from wind_client import wsd, wss, wset, wsq, edb, health
|
|||
|
|
from wind_client import wsi, wst, wsee, wses, wsed
|
|||
|
|
from wind_client import tdays, tdaysoffset, tdayscount
|
|||
|
|
from wind_client import weqs, htocode, wai, wgel
|
|||
|
|
|
|||
|
|
df = wsd("000300.SH", "close,pct_chg", "-30D")
|
|||
|
|
df = wss("600519.SH,000858.SZ", "sec_name,close,pe_ttm", "tradeDate=20241231")
|
|||
|
|
df = wset("sectorconstituent", "date=20241231;windcode=000300.SH")
|
|||
|
|
df = wsq("600519.SH", "rt_last,rt_pct_chg")
|
|||
|
|
df = edb("M0001383", "2024-01-01", "2024-12-31")
|
|||
|
|
dates = tdays("20260101", "20260226")
|
|||
|
|
"""
|
|||
|
|
|
|||
|
|
import os
|
|||
|
|
import re
|
|||
|
|
import sys
|
|||
|
|
import atexit
|
|||
|
|
import pandas as pd
|
|||
|
|
|
|||
|
|
# ---------------------------------------------------------------------------
|
|||
|
|
# WindPy 连接管理
|
|||
|
|
# ---------------------------------------------------------------------------
|
|||
|
|
_w = None
|
|||
|
|
|
|||
|
|
|
|||
|
|
def _check_auto_login() -> bool:
|
|||
|
|
"""检查 Wind 是否配置了自动登录。"""
|
|||
|
|
config_path = os.path.expanduser("~/.Wind/WFT/users/Auth/user.config")
|
|||
|
|
if not os.path.exists(config_path):
|
|||
|
|
return False
|
|||
|
|
try:
|
|||
|
|
with open(config_path, "r", encoding="utf-8") as f:
|
|||
|
|
content = f.read()
|
|||
|
|
match = re.search(r'isAutoLogin="(\d+)"', content)
|
|||
|
|
return match is not None and match.group(1) == "1"
|
|||
|
|
except Exception:
|
|||
|
|
return False
|
|||
|
|
|
|||
|
|
|
|||
|
|
def _ensure_connected():
|
|||
|
|
"""确保 WindPy 已连接,首次调用时自动 w.start()。"""
|
|||
|
|
global _w
|
|||
|
|
if _w is not None and _w.isconnected():
|
|||
|
|
return _w
|
|||
|
|
|
|||
|
|
from WindPy import w as _wind
|
|||
|
|
_w = _wind
|
|||
|
|
|
|||
|
|
if _w.isconnected():
|
|||
|
|
return _w
|
|||
|
|
|
|||
|
|
if not _check_auto_login():
|
|||
|
|
print("[wind_client] 警告: Wind 未开启自动登录,可能弹出登录窗口", file=sys.stderr)
|
|||
|
|
print("[wind_client] 请在 Wind 终端设置中勾选「自动登录」", file=sys.stderr)
|
|||
|
|
|
|||
|
|
ret = _w.start(waitTime=30)
|
|||
|
|
if not _w.isconnected():
|
|||
|
|
raise RuntimeError(f"Wind 连接失败: {ret}")
|
|||
|
|
|
|||
|
|
atexit.register(_cleanup)
|
|||
|
|
return _w
|
|||
|
|
|
|||
|
|
|
|||
|
|
def _cleanup():
|
|||
|
|
global _w
|
|||
|
|
if _w is not None and _w.isconnected():
|
|||
|
|
_w.stop()
|
|||
|
|
_w = None
|
|||
|
|
|
|||
|
|
|
|||
|
|
# ---------------------------------------------------------------------------
|
|||
|
|
# WindData → DataFrame 转换
|
|||
|
|
# ---------------------------------------------------------------------------
|
|||
|
|
|
|||
|
|
def _is_synthetic_codes(codes: list) -> bool:
|
|||
|
|
"""判断 Codes 是否为 wset 返回的行号 ("1","2","3",...)。"""
|
|||
|
|
if not codes:
|
|||
|
|
return False
|
|||
|
|
return all(isinstance(c, str) and c.isdigit() for c in codes[:10])
|
|||
|
|
|
|||
|
|
|
|||
|
|
def _serialize_times(times) -> list:
|
|||
|
|
"""将 WindData.Times 转为字符串列表。"""
|
|||
|
|
if not times:
|
|||
|
|
return []
|
|||
|
|
return [str(t) for t in times]
|
|||
|
|
|
|||
|
|
|
|||
|
|
def _to_dataframe(result, endpoint: str = "") -> pd.DataFrame:
|
|||
|
|
"""将 WindData 对象转为 DataFrame。"""
|
|||
|
|
if hasattr(result, 'ErrorCode') and result.ErrorCode != 0:
|
|||
|
|
raise RuntimeError(f"Wind error {result.ErrorCode}: {result.Data}")
|
|||
|
|
|
|||
|
|
data = result.Data if hasattr(result, 'Data') and result.Data else []
|
|||
|
|
fields = result.Fields if hasattr(result, 'Fields') and result.Fields else []
|
|||
|
|
codes = [str(c) for c in result.Codes] if hasattr(result, 'Codes') and result.Codes else []
|
|||
|
|
times = result.Times if hasattr(result, 'Times') and result.Times else []
|
|||
|
|
|
|||
|
|
if not data:
|
|||
|
|
return pd.DataFrame()
|
|||
|
|
|
|||
|
|
# wset: Codes 为行号 ("1","2",...), 不是真实代码
|
|||
|
|
if _is_synthetic_codes(codes) and fields:
|
|||
|
|
return pd.DataFrame(dict(zip(fields, data)))
|
|||
|
|
|
|||
|
|
# 截面接口: 按代码索引
|
|||
|
|
_cross_section = {"wss", "wsq", "wsee", "wsed"}
|
|||
|
|
if endpoint in _cross_section and codes:
|
|||
|
|
df = pd.DataFrame(dict(zip(fields, data)), index=codes)
|
|||
|
|
df.index.name = "code"
|
|||
|
|
return df
|
|||
|
|
|
|||
|
|
# 判断是否为时间序列: data[0] 长度匹配 times 长度
|
|||
|
|
row_len = len(data[0]) if data else 0
|
|||
|
|
is_time_series = times and row_len == len(times)
|
|||
|
|
|
|||
|
|
if is_time_series:
|
|||
|
|
n_codes = len(codes) if codes else 0
|
|||
|
|
if n_codes > 1 and len(data) == n_codes:
|
|||
|
|
# 多代码单字段
|
|||
|
|
df = pd.DataFrame(dict(zip(codes, data)), index=pd.to_datetime(times))
|
|||
|
|
df.index.name = "date"
|
|||
|
|
return df
|
|||
|
|
# 单代码: 按字段分列
|
|||
|
|
df = pd.DataFrame(dict(zip(fields, data)), index=pd.to_datetime(times))
|
|||
|
|
df.index.name = "date"
|
|||
|
|
return df
|
|||
|
|
|
|||
|
|
# 截面 fallback
|
|||
|
|
if codes and row_len == len(codes):
|
|||
|
|
df = pd.DataFrame(dict(zip(fields, data)), index=codes)
|
|||
|
|
df.index.name = "code"
|
|||
|
|
return df
|
|||
|
|
|
|||
|
|
# fallback
|
|||
|
|
if fields:
|
|||
|
|
return pd.DataFrame(dict(zip(fields, data)))
|
|||
|
|
return pd.DataFrame(data)
|
|||
|
|
|
|||
|
|
|
|||
|
|
# ---------------------------------------------------------------------------
|
|||
|
|
# 公开 API — 对齐 WindPy 原生接口
|
|||
|
|
# ---------------------------------------------------------------------------
|
|||
|
|
|
|||
|
|
def wsd(codes: str, fields: str, begin: str = "", end: str = "", options: str = "") -> pd.DataFrame:
|
|||
|
|
"""日级时间序列,等价于 w.wsd()。"""
|
|||
|
|
w = _ensure_connected()
|
|||
|
|
result = w.wsd(codes, fields, begin, end, options)
|
|||
|
|
return _to_dataframe(result, "wsd")
|
|||
|
|
|
|||
|
|
|
|||
|
|
def wss(codes: str, fields: str, options: str = "") -> pd.DataFrame:
|
|||
|
|
"""截面快照,等价于 w.wss()。"""
|
|||
|
|
w = _ensure_connected()
|
|||
|
|
result = w.wss(codes, fields, options)
|
|||
|
|
return _to_dataframe(result, "wss")
|
|||
|
|
|
|||
|
|
|
|||
|
|
def wset(table_name: str, options: str = "") -> pd.DataFrame:
|
|||
|
|
"""报表数据集,等价于 w.wset()。"""
|
|||
|
|
w = _ensure_connected()
|
|||
|
|
result = w.wset(table_name, options)
|
|||
|
|
return _to_dataframe(result, "wset")
|
|||
|
|
|
|||
|
|
|
|||
|
|
def wsq(codes: str, fields: str, options: str = "") -> pd.DataFrame:
|
|||
|
|
"""实时行情快照,等价于 w.wsq()。"""
|
|||
|
|
w = _ensure_connected()
|
|||
|
|
result = w.wsq(codes, fields, options)
|
|||
|
|
return _to_dataframe(result, "wsq")
|
|||
|
|
|
|||
|
|
|
|||
|
|
def edb(codes: str, begin: str = "", end: str = "", options: str = "") -> pd.DataFrame:
|
|||
|
|
"""宏观经济指标,等价于 w.edb()。"""
|
|||
|
|
w = _ensure_connected()
|
|||
|
|
result = w.edb(codes, begin, end, options)
|
|||
|
|
return _to_dataframe(result, "edb")
|
|||
|
|
|
|||
|
|
|
|||
|
|
def wsi(codes: str, fields: str, begin: str = "", end: str = "", options: str = "") -> pd.DataFrame:
|
|||
|
|
"""分钟K线序列,等价于 w.wsi()。options 常用: BarSize=1/5/15/30/60"""
|
|||
|
|
w = _ensure_connected()
|
|||
|
|
result = w.wsi(codes, fields, begin, end, options)
|
|||
|
|
return _to_dataframe(result, "wsi")
|
|||
|
|
|
|||
|
|
|
|||
|
|
def wst(codes: str, fields: str, begin: str = "", end: str = "", options: str = "") -> pd.DataFrame:
|
|||
|
|
"""日内Tick跳价,等价于 w.wst()。"""
|
|||
|
|
w = _ensure_connected()
|
|||
|
|
result = w.wst(codes, fields, begin, end, options)
|
|||
|
|
return _to_dataframe(result, "wst")
|
|||
|
|
|
|||
|
|
|
|||
|
|
def wsee(codes: str, fields: str, options: str = "") -> pd.DataFrame:
|
|||
|
|
"""板块多维数据,等价于 w.wsee()。codes 为板块/指数代码。"""
|
|||
|
|
w = _ensure_connected()
|
|||
|
|
result = w.wsee(codes, fields, options)
|
|||
|
|
return _to_dataframe(result, "wsee")
|
|||
|
|
|
|||
|
|
|
|||
|
|
def wses(codes: str, fields: str, begin: str = "", end: str = "", options: str = "") -> pd.DataFrame:
|
|||
|
|
"""板块时间序列,等价于 w.wses()。"""
|
|||
|
|
w = _ensure_connected()
|
|||
|
|
result = w.wses(codes, fields, begin, end, options)
|
|||
|
|
return _to_dataframe(result, "wses")
|
|||
|
|
|
|||
|
|
|
|||
|
|
def wsed(codes: str, fields: str, options: str = "") -> pd.DataFrame:
|
|||
|
|
"""板块查询,等价于 w.wsed()。codes 为 SectorID。"""
|
|||
|
|
w = _ensure_connected()
|
|||
|
|
result = w.wsed(codes, fields, options)
|
|||
|
|
return _to_dataframe(result, "wsed")
|
|||
|
|
|
|||
|
|
|
|||
|
|
def tdays(begin: str = "", end: str = "", options: str = "") -> list:
|
|||
|
|
"""交易日列表,等价于 w.tdays()。返回日期字符串列表。"""
|
|||
|
|
w = _ensure_connected()
|
|||
|
|
result = w.tdays(begin, end, options)
|
|||
|
|
if hasattr(result, 'ErrorCode') and result.ErrorCode != 0:
|
|||
|
|
raise RuntimeError(f"Wind error {result.ErrorCode}: {result.Data}")
|
|||
|
|
data = result.Data if hasattr(result, 'Data') and result.Data else []
|
|||
|
|
return _serialize_times(data[0]) if data else []
|
|||
|
|
|
|||
|
|
|
|||
|
|
def tdaysoffset(offset: int, begin: str = "", options: str = "") -> str:
|
|||
|
|
"""交易日偏移,等价于 w.tdaysoffset()。返回偏移后的日期字符串。"""
|
|||
|
|
if not begin:
|
|||
|
|
from datetime import date
|
|||
|
|
begin = date.today().strftime("%Y%m%d")
|
|||
|
|
w = _ensure_connected()
|
|||
|
|
result = w.tdaysoffset(offset, begin, options)
|
|||
|
|
if hasattr(result, 'ErrorCode') and result.ErrorCode != 0:
|
|||
|
|
raise RuntimeError(f"Wind error {result.ErrorCode}: {result.Data}")
|
|||
|
|
data = result.Data if hasattr(result, 'Data') and result.Data else []
|
|||
|
|
return str(data[0][0]) if data and data[0] else ""
|
|||
|
|
|
|||
|
|
|
|||
|
|
def tdayscount(begin: str = "", end: str = "", options: str = "") -> int:
|
|||
|
|
"""交易日计数,等价于 w.tdayscount()。返回交易日数量。"""
|
|||
|
|
w = _ensure_connected()
|
|||
|
|
result = w.tdayscount(begin, end, options)
|
|||
|
|
if hasattr(result, 'ErrorCode') and result.ErrorCode != 0:
|
|||
|
|
raise RuntimeError(f"Wind error {result.ErrorCode}: {result.Data}")
|
|||
|
|
data = result.Data if hasattr(result, 'Data') and result.Data else []
|
|||
|
|
return int(data[0][0]) if data and data[0] else 0
|
|||
|
|
|
|||
|
|
|
|||
|
|
def weqs(filtername: str, options: str = "") -> pd.DataFrame:
|
|||
|
|
"""条件选股,等价于 w.weqs()。"""
|
|||
|
|
w = _ensure_connected()
|
|||
|
|
result = w.weqs(filtername, options)
|
|||
|
|
return _to_dataframe(result, "weqs")
|
|||
|
|
|
|||
|
|
|
|||
|
|
def htocode(codes: str, sec_type: str, options: str = "") -> pd.DataFrame:
|
|||
|
|
"""代码转换(名称/简称→Wind代码),等价于 w.htocode()。"""
|
|||
|
|
w = _ensure_connected()
|
|||
|
|
result = w.htocode(codes, sec_type, options)
|
|||
|
|
return _to_dataframe(result, "htocode")
|
|||
|
|
|
|||
|
|
|
|||
|
|
def wai(func: str, input: str, options: str = "") -> pd.DataFrame:
|
|||
|
|
"""智能API,等价于 w.wai()。"""
|
|||
|
|
w = _ensure_connected()
|
|||
|
|
result = w.wai(func, input, options)
|
|||
|
|
return _to_dataframe(result, "wai")
|
|||
|
|
|
|||
|
|
|
|||
|
|
def wgel(funname: str, windid: str, options: str = "") -> pd.DataFrame:
|
|||
|
|
"""企业库,等价于 w.wgel()。"""
|
|||
|
|
w = _ensure_connected()
|
|||
|
|
result = w.wgel(funname, windid, options)
|
|||
|
|
return _to_dataframe(result, "wgel")
|
|||
|
|
|
|||
|
|
|
|||
|
|
def health() -> dict:
|
|||
|
|
"""检查 Wind 连接状态。"""
|
|||
|
|
try:
|
|||
|
|
w = _ensure_connected()
|
|||
|
|
return {"status": "ok", "connected": w.isconnected()}
|
|||
|
|
except Exception as e:
|
|||
|
|
return {"status": "error", "message": str(e)}
|