feat(pydantic): 集成 Pydantic 模型到 Flask API 层

1. models.py:
   - 添加 dataframe_to_ohlcv_response() 转换函数
   - 支持 DataFrame → OHLCVResponse 自动转换
   - 自动处理 nav、premium、attrs 等业务数据

2. flask_server.py:
   - 使用 Pydantic 模型构建响应(替代手动 Dict)
   - 错误响应使用 ErrorResponse 模型
   - 代码减少 20+ 行,类型安全提升

3. flask_api_source.py:
   - 使用 validate_ohlcv_response() 验证 API 响应
   - 类型安全访问 nav、premium、info 等字段
   - ETF 数据解析更可靠

测试通过:
 DataFrame → Pydantic 转换正常
 ETF 净值和溢价率正确处理
 线上 API 响应验证成功
 FlaskAPIDataSource 集成正常
This commit is contained in:
2026-05-24 01:13:33 +08:00
parent 72df18a28b
commit 226a27361f
3 changed files with 265 additions and 99 deletions

View File

@@ -44,6 +44,7 @@ import pandas as pd
from datasource.universal_fetcher import UniversalDataFetcher
from datasource.asset_type_detector import AssetTypeDetector, AssetType
from datasource.models import dataframe_to_ohlcv_response, OHLCVResponse, ErrorResponse
# ============================================================
@@ -730,91 +731,71 @@ def get_ohlcv():
result, is_cached = fetch_data_with_ttl(code, start, end, nocache, timeframe, adj, final_type)
if result is None:
return jsonify({
"code": code,
"asset_type": final_type.value,
"adj": adj,
"detected_type": detected_type.value if asset_type_param else None, # 仅当用户指定时显示
"error": "No data available",
"start": start,
"end": end,
}), 404
error_response = ErrorResponse(
error="No data available",
code=code,
asset_type=final_type.value,
adj=adj,
detected_type=detected_type.value if asset_type_param else None,
)
return error_response.model_dump(mode='json'), 404
if "error" in result:
return jsonify({
"code": code,
"asset_type": final_type.value,
"adj": adj,
"detected_type": detected_type.value if asset_type_param else None,
"error": result["error"],
}), 500
error_response = ErrorResponse(
error=result["error"],
code=code,
asset_type=final_type.value,
adj=adj,
detected_type=detected_type.value if asset_type_param else None,
)
return error_response.model_dump(mode='json'), 500
result['cached'] = is_cached
result['asset_type'] = final_type.value # 使用最终类型
result['adj'] = adj # 返回使用的 adj 参数
# ✅ 使用 Pydantic 模型构建响应(类型安全)
# 从 result 中提取数据
df_data = result.get('data', [])
attrs = result.get('attrs', {})
# API 层职责:决定如何使用 attrs 中的业务数据
if 'attrs' in result:
attrs = result['attrs']
# 根据资产类型决定日期格式精度
# 加密货币使用分钟级,其他使用天级
date_format = '%Y-%m-%d %H:%M:%S' if final_type == AssetType.CRYPTO else '%Y-%m-%d'
# 提取净值到顶层(方便调用方使用)
if 'nav' in attrs:
nav_df = attrs['nav']
if isinstance(nav_df, pd.DataFrame):
# 将 DataFrame 转换为列表格式JSON 可序列化)
nav_df_copy = nav_df.reset_index().copy()
nav_df_copy['date'] = nav_df_copy['date'].dt.strftime(date_format)
nav_dict = {
'data': nav_df_copy.to_dict(orient='records'),
'count': len(nav_df_copy)
}
result['nav'] = nav_dict
else:
result['nav'] = nav_df
# 提取溢价率到顶层(调用业务函数处理格式)
if 'premium' in attrs:
premium_result = build_premium_result_from_attrs(attrs['premium'])
if premium_result:
result.update(premium_result)
# 将 attrs 中的 DataFrame/Series 转换为字典格式(用于 JSON 序列化)
attrs_serializable = {}
for key, value in attrs.items():
if isinstance(value, pd.DataFrame):
df_copy = value.reset_index().copy()
if 'date' in df_copy.columns:
df_copy['date'] = df_copy['date'].dt.strftime(date_format)
attrs_serializable[key] = {
'data': df_copy.to_dict(orient='records'),
'count': len(df_copy)
}
elif isinstance(value, pd.Series):
# 将 Series 索引转换为字符串
series_copy = value.copy()
series_copy.index = series_copy.index.strftime(date_format)
attrs_serializable[key] = {
'type': 'series',
'data': series_copy.to_dict(),
'name': value.name
}
else:
attrs_serializable[key] = value
result['attrs'] = attrs_serializable
# 重建 DataFrame用于转换函数
if df_data:
df = pd.DataFrame(df_data)
if 'date' in df.columns:
df['date'] = pd.to_datetime(df['date'])
df = df.set_index('date')
else:
df = pd.DataFrame()
# 如果用户指定了类型但与自动检测不同,显示提示
if asset_type_param and detected_type != final_type:
result['type_override'] = {
# 提取 nav DataFrame
nav_df = attrs.get('nav') if isinstance(attrs.get('nav'), pd.DataFrame) else None
# 提取 premium Series
premium_series = attrs.get('premium') if isinstance(attrs.get('premium'), pd.Series) else None
# 构建响应模型
response = dataframe_to_ohlcv_response(
df=df if len(df) > 0 else None,
code=code,
asset_type=final_type.value,
adj=adj,
cached=is_cached,
nav_df=nav_df,
premium_series=premium_series,
info=attrs.get('info'),
attrs=attrs,
columns=result.get('columns'),
date_range=result.get('date_range'),
requested_range=result.get('requested_range'),
available_range=result.get('available_range'),
cache_strategy=result.get('cache_strategy'),
timeframe=result.get('timeframe'),
type_override={
"detected": detected_type.value,
"specified": final_type.value,
"hint": "用户强制覆盖了自动检测结果",
}
return jsonify(result)
} if (asset_type_param and detected_type != final_type) else None,
)
# ✅ 自动序列化为 JSON
return response.model_dump(mode='json')