#!/usr/bin/env python3 # -*- coding: utf-8 -*- """ Tushare 双通道客户端(方案一 + 方案二) 实测边界(2026-08-07 验证): - 方案一(duckdns SDK): 股票/指数/财务类接口可用(15000积分档、实时、集合竞价无限制); 期货类接口全部返回 "api not purchased"(该 token 账号未购买期货权限)。 - 方案二(REST 直连) : 期货 fut_daily 完整可用(昨结/结算/成交量/持仓量),股票 daily 亦可用; fut_basic / fut_holding / fut_wsr / fut_mapping 受日调用限流,间歇返回 {"detail": "daily call limit exceeded"},需重试或错峰调用。 - 调用顺序:期货类走方案二;股票/指数/财务等走方案一;方案二限流时自动重试 3 次。 凭据建议通过环境变量注入(NO_PROXY 必须在 import tushare 前设置): TUSHARE_TOKEN = tk_live_... (方案一 token,duckdns) TUSHARE_API_URL = https://quantdata888.duckdns.org TUSHARE_REST_URL = http://175.27.156.38/v1/tushare/query TUSHARE_REST_API_KEY = emp_... (方案二 key) 注意:若终端环境残留旧 TUSHARE_TOKEN(如 f94ec4... 官方 token),会覆盖方案一默认值, 导致 invalid token;先 `Remove-Item Env:TUSHARE_TOKEN` 或设置新值。 """ import os import time import json from typing import Optional, Union # 关键:绕过代理必须在 import requests/tushare 之前设置 os.environ.setdefault('NO_PROXY', '*') os.environ.setdefault('no_proxy', '*') import requests import pandas as pd import tushare as ts # ========== 默认配置(可用环境变量覆盖) ========== TOKEN = os.getenv('TUSHARE_TOKEN', 'tk_live_g67jmtTDaYN_ydreNGukHZTTSRfppw6zTIwa0VUMxOE') API_URL = os.getenv('TUSHARE_API_URL', 'https://quantdata888.duckdns.org') REST_URL = os.getenv('TUSHARE_REST_URL', 'http://175.27.156.38/v1/tushare/query') REST_KEY = os.getenv('TUSHARE_REST_API_KEY', 'emp_siAR1g6rVQkDSMFj6LVdOWrw3JAJU5cHNQ2X') _REST_HEADERS = { 'X-API-Key': REST_KEY, 'Content-Type': 'application/json', } _pro_instance = None _pro_fut_interfaces = {'fut_daily', 'fut_basic', 'fut_mapping', 'fut_holding', 'fut_wsr', 'fut_settle'} def _get_pro(): """懒加载方案一 tushare SDK 实例(指向自定义 URL)""" global _pro_instance if _pro_instance is None: _pro_instance = ts.pro_api(TOKEN) _pro_instance._DataApi__http_url = API_URL return _pro_instance def rest_query(api_name: str, params: Optional[dict] = None, fields: str = '', retries: int = 3, sleep: float = 1.0) -> list: """ 方案二 REST 直连查询(期货日线首选)。 Args: api_name: 接口名,如 'fut_daily' params: 查询参数 fields: 逗号分隔字段 retries: 遇限流重试次数 sleep: 重试间隔秒数 Returns: 记录列表(dict 数组);接口错误时抛 RuntimeError。 """ params = params or {} for attempt in range(retries + 1): try: r = requests.post(REST_URL, headers=_REST_HEADERS, json={'api_name': api_name, 'params': params, 'fields': fields}, timeout=60) j = r.json() except Exception as e: if attempt < retries: time.sleep(sleep * (attempt + 1)) continue raise RuntimeError(f'REST 请求异常: {e}') # 限流 if isinstance(j, dict) and j.get('detail') == 'daily call limit exceeded': if attempt < retries: time.sleep(sleep * (attempt + 1)) continue raise RuntimeError(f'{api_name} 日调用限流,请稍后重试或错峰调用') # 正常数据 if isinstance(j, dict) and 'data' in j and j['data'] and 'items' in j['data']: d = j['data'] return [dict(zip(d['fields'], row)) for row in d['items']] raise RuntimeError(f'{api_name} 返回异常: {json.dumps(j, ensure_ascii=False)[:300]}') raise RuntimeError(f'{api_name} 重试 {retries} 次后仍失败') def query(api_name: str, params: Optional[dict] = None, fields: str = '', prefer: str = 'auto') -> Union[pd.DataFrame, list]: """ 统一查询入口。 Args: api_name: 接口名(tushare 命名,如 daily / fut_daily / index_daily) params: 查询参数 dict fields: 逗号分隔字段串 prefer: 'auto' 期货走方案二、其余走方案一;也可强制 'plan1' / 'plan2' Returns: prefer='plan2' 或自动选中方案二时返回 list[dict]; 方案一返回 pd.DataFrame。 """ is_fut = api_name in _pro_fut_interfaces use_plan2 = (prefer == 'plan2') or (prefer == 'auto' and is_fut) if use_plan2: return rest_query(api_name, params, fields) pro = _get_pro() try: fn = getattr(pro, api_name) except AttributeError: raise ValueError(f'方案一 SDK 无接口: {api_name}') return fn(**params) if params else fn() def fut_daily(ts_code: str, start_date: str, end_date: str, fields: str = 'ts_code,trade_date,pre_close,pre_settle,open,high,low,close,settle,change1,change2,vol,amount,oi,oi_chg') -> list: """焦煤等商品期货日线(方案二),返回 list[dict],含昨结/结算/成交量/持仓量。""" return rest_query('fut_daily', {'ts_code': ts_code, 'start_date': start_date, 'end_date': end_date}, fields) def fut_mapping(ts_code: str, trade_date: str) -> list: """主力合约映射(方案二,有限流风险)。""" return rest_query('fut_mapping', {'ts_code': ts_code, 'trade_date': trade_date}) if __name__ == '__main__': import sys print('== 自检:方案二 焦煤 fut_daily ==') rows = fut_daily('JM2609.DCE', '20260803', '20260806') for r in rows: print(r) print(f'\n共 {len(rows)} 行') print('\n== 自检:方案一 股票 daily ==') df = query('daily', {'ts_code': '000001.SZ', 'start_date': '20260801', 'end_date': '20260804'}) print(df.head())