154 lines
6.0 KiB
Python
154 lines
6.0 KiB
Python
#!/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())
|