fix: 图表x轴显示完整日期 + 组合回测取数翻页

图表日期格式(4 个组件):KlineChart/EquityChart/CompareChart/
PortfolioCompareChart 的 xAxis 加 axisLabel.formatter,强制显示原始
日期字符串。ECharts 默认把 ISO 日期当时间对象解析,丢失年份只显示
月-日+00:00。

组合回测取数翻页(_fetch_portfolio_bars):固定 count=800 改为翻页循环
(与前端 fetchBars 同逻辑),拼接后 sort_values 排序再过滤。修复组合
回测图表从 800 根最早点(2023-03-14)开始、缺失早期数据的问题。

实测组合回测 2019-01-07 起:净值 1814 点(不再是 800),首日正确。
This commit is contained in:
Justin Gu
2026-07-03 04:54:04 +08:00
parent 2903d8c800
commit 80d62aec56
5 changed files with 49 additions and 14 deletions
+31 -12
View File
@@ -322,30 +322,49 @@ async def _fetch_portfolio_bars(
) -> list[Any]:
"""逐个标的取 K 线并组装 StockData 列表(async,必须在 event loop 内调用)。
单个标的取数失败时跳过(不中断整个组合),全部失败返回空列表。
当 start_date 超出单次 800 根覆盖范围时,自动翻页拉取(与前端 fetchBars
同逻辑)。单个标的取数失败时跳过(不中断整个组合),全部失败返回空列表。
"""
from easy_tdx.backtest.portfolio_engine import StockData
from easy_tdx.web.convert import category_from_str, market_from_str
max_pages = 10 # 翻页上限:10 × 800 = 8000 根
stock_data_list: list[StockData] = []
for symbol in stocks:
market_str, code = symbol.split(":", 1)
try:
df = await client.get_security_bars(
market_from_str(market_str),
code,
category_from_str(category),
0,
800, # 固定拉满,前端按日期过滤
)
except Exception:
continue # 单个标的失败不中断
if len(df) < 2:
frames: list[pd.DataFrame] = []
for page in range(max_pages):
try:
page_df = await client.get_security_bars(
market_from_str(market_str),
code,
category_from_str(category),
page * 800,
800,
)
except Exception:
break # 单页失败则停止该标的的翻页
if len(page_df) == 0:
break
frames.append(page_df)
# 已覆盖到 start_date(本页最早一根 ≤ start_date)则停止
if start_date and len(page_df) > 0:
dt_col = "datetime" if "datetime" in page_df.columns else "date"
oldest = str(page_df[dt_col].iloc[-1])[:10]
if oldest <= start_date:
break
if len(page_df) < 800:
break # 数据起点
if not frames:
continue
df = pd.concat(frames, ignore_index=True)
# 列名归一化:日线返回 date,分钟线返回 datetime
if "datetime" not in df.columns and "date" in df.columns:
df = df.copy()
df["datetime"] = df["date"]
# 翻页拼接后按时间正序排序(页间逆序)
df = df.sort_values("datetime").reset_index(drop=True)
# 日期范围过滤
if start_date or end_date:
dt_str = df["datetime"].astype(str).str.slice(0, 10)
+6 -1
View File
@@ -41,7 +41,12 @@ function buildOption(): echarts.EChartsCoreOption {
},
legend: { top: 0, data: seriesData.map((s) => s.name) },
grid: { left: '8%', right: '5%', top: 30, bottom: 50 },
xAxis: { type: 'category', data: allDates, boundaryGap: false },
xAxis: {
type: 'category',
data: allDates,
boundaryGap: false,
axisLabel: { formatter: (v: string) => v },
},
yAxis: {
type: 'value',
scale: true,
+1
View File
@@ -41,6 +41,7 @@ function buildOption(): echarts.EChartsCoreOption {
data: dates,
boundaryGap: false,
axisLine: { onZero: false },
axisLabel: { formatter: (v: string) => v },
},
yAxis: [
{
+5
View File
@@ -82,6 +82,11 @@ function buildOption(): echarts.EChartsCoreOption {
boundaryGap: true,
axisLine: { onZero: false },
splitLine: { show: false },
// 强制显示原始日期字符串,避免 ECharts 把 "2024-01-15" 当时间对象
// 解析后默认按 {MM}-{dd} {HH}:{mm} 显示(丢年份)
axisLabel: {
formatter: (v: string) => v,
},
},
yAxis: {
scale: true,
@@ -50,7 +50,12 @@ function buildOption(
},
legend: { top: 0, data: series.map((s) => s.name) },
grid: { left: '8%', right: '5%', top: 30, bottom: 50 },
xAxis: { type: 'category', data: allDates, boundaryGap: false },
xAxis: {
type: 'category',
data: allDates,
boundaryGap: false,
axisLabel: { formatter: (v: string) => v },
},
yAxis: {
type: 'value',
scale: true,