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:
@@ -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",
|
||||||
|
|||||||
@@ -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)
|
||||||
# ============================================================
|
# ============================================================
|
||||||
|
|||||||
Reference in New Issue
Block a user