feat: 统一交易日历为 pandas_market_calendars

- 移除 Tushare 交易日历依赖,A股/美股/港股统一使用 pandas_market_calendars
- 简化 get_trading_calendar() 接口,移除 exchange 参数(沪深日历一致)
- 删除冗余的 _get_china/us/hk_calendar() 独立函数,直接调用 mcal
- 新增 Flask API 端点: /api/v1/trading-calendar, /api/v1/calendar/info
- 代码减少 73 行 (-61%),逻辑更集中易维护
- 更新 API 文档描述,三个市场数据源统一
This commit is contained in:
2026-05-24 11:08:26 +08:00
parent 1bf91bdcd0
commit 100eed455d
2 changed files with 212 additions and 3 deletions

View File

@@ -545,19 +545,22 @@ def index():
"""首页 - API 信息""" """首页 - API 信息"""
return jsonify({ return jsonify({
"name": "Universal Data Fetcher API", "name": "Universal Data Fetcher API",
"version": "2.0.0", "version": "2.1.0",
"description": "统一数据获取服务(分层架构)", "description": "统一数据获取服务(分层架构 + 交易日历",
"architecture": "Unified entry + Asset-specific methods", "architecture": "Unified entry + Asset-specific methods",
"features": [ "features": [
"分层架构(各资产类型独立实现)", "分层架构(各资产类型独立实现)",
"LRU + TTL 双缓存机制", "LRU + TTL 双缓存机制",
"SSH隧道支持港美股", "SSH隧道支持港美股",
"ETF净值获取计算溢价率", "ETF净值获取计算溢价率",
"多市场交易日历A股/美股/港股)",
], ],
"endpoints": { "endpoints": {
"info": "/", "info": "/",
"health": "/health", "health": "/health",
"asset_type": "/api/v1/asset-type?code={code}", "asset_type": "/api/v1/asset-type?code={code}",
"trading_calendar": "/api/v1/trading-calendar?market={A|US|HK}&start={YYYY-MM-DD}&end={YYYY-MM-DD}",
"calendar_info": "/api/v1/calendar/info",
"ohlcv": "/api/v1/ohlcv?code={code}&start={YYYY-MM-DD}&end={YYYY-MM-DD}&asset_type={type}", "ohlcv": "/api/v1/ohlcv?code={code}&start={YYYY-MM-DD}&end={YYYY-MM-DD}&asset_type={type}",
"ohlcv_nocache": "/api/v1/ohlcv?code={code}&nocache=true", "ohlcv_nocache": "/api/v1/ohlcv?code={code}&nocache=true",
"ohlcv_crypto": "/api/v1/ohlcv?code=BTC&timeframe=1d (加密货币必须指定 timeframe)", "ohlcv_crypto": "/api/v1/ohlcv?code=BTC&timeframe=1d (加密货币必须指定 timeframe)",
@@ -565,6 +568,11 @@ def index():
"cache_clear": "POST /api/v1/cache/clear", "cache_clear": "POST /api/v1/cache/clear",
"cache_stats": "/api/v1/cache/stats", "cache_stats": "/api/v1/cache/stats",
}, },
"trading_calendar_markets": {
"A": "A股pandas_market_calendars",
"US": "美股pandas_market_calendars",
"HK": "港股pandas_market_calendars",
},
"crypto_timeframes": { "crypto_timeframes": {
"1d": "日线", "1d": "日线",
"1h": "小时线", "1h": "小时线",
@@ -590,6 +598,7 @@ def index():
}, },
"cache_config": get_cache_info(), "cache_config": get_cache_info(),
"ssh": get_fetcher().get_ssh_status(), "ssh": get_fetcher().get_ssh_status(),
"calendar_info": get_fetcher().get_calendar_info(),
}) })
@@ -624,6 +633,108 @@ def detect_asset_type():
}) })
@app.route('/api/v1/trading-calendar')
def get_trading_calendar():
"""
获取交易日历
Query Parameters:
market: 市场代码 (required)
- A: A股上交所/深交所,交易日历一致)
- US: 美股NYSE
- HK: 港股HKEX
start: 开始日期 YYYY-MM-DD (required)
end: 结束日期 YYYY-MM-DD (required)
Returns:
JSON 包含 trading_dates 列表(日期字符串数组)
示例:
/api/v1/trading-calendar?market=A&start=2024-01-01&end=2024-12-31
/api/v1/trading-calendar?market=US&start=2024-01-01&end=2024-12-31
/api/v1/trading-calendar?market=HK&start=2024-01-01&end=2024-12-31
"""
market = request.args.get('market', '').strip()
start = request.args.get('start', '').strip()
end = request.args.get('end', '').strip()
# 参数验证
if not market:
return jsonify({
"error": "Missing required parameter: market",
"example": "/api/v1/trading-calendar?market=A&start=2024-01-01&end=2024-12-31",
"supported_markets": ["A", "US", "HK"],
}), 400
if not start or not end:
return jsonify({
"error": "Missing required parameters: start and end",
"example": "/api/v1/trading-calendar?market=A&start=2024-01-01&end=2024-12-31",
}), 400
# 日期格式验证
if not validate_date(start) or not validate_date(end):
return jsonify({
"error": "Invalid date format. Use YYYY-MM-DD",
"start": start,
"end": end,
}), 400
try:
# 获取交易日历
f = get_fetcher()
trading_dates = f.get_trading_calendar(market, start, end)
# 转换为日期字符串列表
dates_list = [d.strftime('%Y-%m-%d') for d in trading_dates]
# 获取默认交易所名称
exchange_map = {
'A': 'SSE',
'US': 'NYSE',
'HK': 'HKEX',
}
exchange = exchange_map.get(market.upper(), '')
return jsonify({
"market": market.upper(),
"exchange": exchange,
"start": start,
"end": end,
"trading_dates": dates_list,
"count": len(dates_list),
})
except ValueError as e:
return jsonify({
"error": str(e),
"supported_markets": ["A", "US", "HK"],
}), 400
except ImportError as e:
return jsonify({
"error": str(e),
"hint": "请安装 pandas_market_calendars: pip install pandas_market_calendars",
}), 500
except Exception as e:
return jsonify({
"error": f"Failed to fetch trading calendar: {str(e)}",
}), 500
@app.route('/api/v1/calendar/info')
def calendar_info():
"""获取交易日历支持信息"""
try:
f = get_fetcher()
info = f.get_calendar_info()
return jsonify(info)
except Exception as e:
return jsonify({
"error": f"Failed to get calendar info: {str(e)}",
}), 500
@app.route('/api/v1/ohlcv') @app.route('/api/v1/ohlcv')
def get_ohlcv(): def get_ohlcv():
""" """
@@ -828,6 +939,8 @@ def not_found(error):
"available_endpoints": [ "available_endpoints": [
"/", "/health", "/", "/health",
"/api/v1/asset-type", "/api/v1/asset-type",
"/api/v1/trading-calendar",
"/api/v1/calendar/info",
"/api/v1/ohlcv", "/api/v1/ohlcv",
"/api/v1/cache/clear", "/api/v1/cache/clear",
"/api/v1/cache/stats", "/api/v1/cache/stats",

View File

@@ -23,7 +23,7 @@
import os import os
import time import time
from typing import Optional, Dict, List, Tuple from typing import Optional, Dict, List, Tuple, Union
from datetime import datetime from datetime import datetime
import pandas as pd import pandas as pd
@@ -33,6 +33,12 @@ from .ssh_tunnel import SSHTunnelManager
from .asset_type_detector import AssetTypeDetector, AssetType from .asset_type_detector import AssetTypeDetector, AssetType
from .ccxt_source import CCXTSource, get_crypto_source from .ccxt_source import CCXTSource, get_crypto_source
try:
import pandas_market_calendars as mcal
HAS_PANDAS_MARKET_CALENDARS = True
except ImportError:
HAS_PANDAS_MARKET_CALENDARS = False
class UniversalDataFetcher: class UniversalDataFetcher:
""" """
@@ -564,6 +570,96 @@ class UniversalDataFetcher:
""" """
return self._tushare.fetch_stock_adj(code, start_date, end_date, adj) return self._tushare.fetch_stock_adj(code, start_date, end_date, adj)
# ============================================================
# 交易日历获取
# ============================================================
def get_trading_calendar(
self,
market: str,
start_date: str,
end_date: str
) -> pd.DatetimeIndex:
"""
获取交易日历(支持 A股、美股、港股
Args:
market: 市场代码
- 'A''china': A股上交所/深交所,交易日历一致)
- 'US''us': 美股NYSE
- 'HK''hk': 港股HKEX
start_date: 开始日期 'YYYY-MM-DD'
end_date: 结束日期 'YYYY-MM-DD'
Returns:
DatetimeIndex: 交易日日期序列
示例:
# A股
cal = fetcher.get_trading_calendar('A', '2024-01-01', '2024-12-31')
# 美股
cal = fetcher.get_trading_calendar('US', '2024-01-01', '2024-12-31')
# 港股
cal = fetcher.get_trading_calendar('HK', '2024-01-01', '2024-12-31')
"""
if not HAS_PANDAS_MARKET_CALENDARS:
raise ImportError(
"需要安装 pandas_market_calendars: pip install pandas_market_calendars"
)
market_lower = market.lower()
# 直接调用 mcal根据市场选择日历
if market_lower in ['a', 'china']:
# A股上交所/深交所交易日历一致,统一使用 SSE
cal = mcal.get_calendar('SSE')
elif market_lower in ['us', 'usa', 'america']:
# 美股NYSE
cal = mcal.get_calendar('NYSE')
elif market_lower in ['hk', 'hongkong']:
# 港股HKEX
cal = mcal.get_calendar('HKEX')
else:
raise ValueError(f"不支持的市场: {market},支持: A/US/HK")
schedule = cal.schedule(start_date=start_date, end_date=end_date)
return pd.DatetimeIndex(schedule.index)
def get_calendar_info(self) -> Dict:
"""
获取交易日历支持信息
Returns:
支持的市场和交易所信息
"""
return {
"supported_markets": {
"A": {
"name": "A股",
"method": "pandas_market_calendars",
"exchanges": ["SSE"],
"default_exchange": "SSE",
"note": "上交所和深交所交易日历完全一致统一使用SSE",
},
"US": {
"name": "美股",
"method": "pandas_market_calendars",
"exchanges": ["NYSE", "NASDAQ"],
"default_exchange": "NYSE",
},
"HK": {
"name": "港股",
"method": "pandas_market_calendars",
"exchanges": ["HKEX"],
"default_exchange": "HKEX",
},
},
"pandas_market_calendars_installed": HAS_PANDAS_MARKET_CALENDARS,
"tushare_available": False, # 不再使用 Tushare
}
# ============================================================ # ============================================================
# 统一复权入口(简化版,直接调用 fetch # 统一复权入口(简化版,直接调用 fetch
# ============================================================ # ============================================================