Files
my_wiki/raw/期货/01-基本分析逻辑/焦煤日报/scripts/tushare_dual_client.py
T

154 lines
6.0 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/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_... (方案一 tokenduckdns
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())