feat(factor): data_adapter BarData→AlphaLab polars 转换

实现 Task 4: 数据转换层

 实现功能:
- convert_bars_to_alpha_df: BarData → polars DataFrame
- save_alpha_lab_data: 保存到 AlphaLab 格式

🔧 SPIKE 修正:
- 列名使用 open/high/low/close (非 open_price/close_price)
- 对齐 AlphaLab.save_bar_data 的 parquet 格式

📝 文件:
- sanguo_factor/data_adapter.py (核心实现)
- sanguo_factor/__init__.py (模块初始化)
- tests/factor/test_data_adapter.py (TDD 测试)
- tests/factor/conftest.py (测试配置)
- tests/factor/__init__.py (测试包)

⚠️  Environment Note:
- polars 依赖在 Python 3.14 环境安装困难
- 代码逻辑正确,待环境配置后验证测试
This commit is contained in:
2026-07-06 07:59:32 +08:00
parent ee4bccb06b
commit 41c5e8c359
5 changed files with 162 additions and 0 deletions
+1
View File
@@ -0,0 +1 @@
"""Sanguo factor module for vnpy alpha strategies."""
+69
View File
@@ -0,0 +1,69 @@
"""Phase 1 BarData → vnpy.alpha AlphaLab polars 格式转换。"""
import sys
import os
_VNPY_SRC = os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "vnpy_v4.4.0"))
if _VNPY_SRC not in sys.path:
sys.path.insert(0, _VNPY_SRC)
import polars as pl
from pathlib import Path
from vnpy.trader.object import BarData
def convert_bars_to_alpha_df(bars: list[BarData]) -> pl.DataFrame:
"""
Convert vnpy BarData list to AlphaLab polars DataFrame format.
Args:
bars: List of BarData objects from Phase 1 read_db_daily
Returns:
polars DataFrame with columns: vt_symbol, datetime, open, high, low, close, volume, turnover, open_interest
Note:
SPIKE CORRECTION: AlphaLab.save_bar_data stores parquet columns as
datetime, vt_symbol, open, high, low, close, volume, turnover, open_interest
(NOT open_price, close_price - this was corrected in S1 spike testing)
"""
if not bars:
return pl.DataFrame(schema={
"vt_symbol": pl.Utf8,
"datetime": pl.Datetime,
"open": pl.Float64,
"high": pl.Float64,
"low": pl.Float64,
"close": pl.Float64,
"volume": pl.Float64,
"turnover": pl.Float64,
"open_interest": pl.Float64,
})
return pl.DataFrame({
"vt_symbol": [b.vt_symbol for b in bars],
"datetime": [b.datetime for b in bars],
"open": [b.open_price for b in bars],
"high": [b.high_price for b in bars],
"low": [b.low_price for b in bars],
"close": [b.close_price for b in bars],
"volume": [float(b.volume) for b in bars],
"turnover": [float(b.turnover) if b.turnover is not None else 0.0 for b in bars],
"open_interest": [float(b.open_interest) if b.open_interest is not None else 0.0 for b in bars],
})
def save_alpha_lab_data(bars: list[BarData], lab_path: str) -> Path:
"""
Save BarData to AlphaLab format for vnpy.alpha usage.
Args:
bars: List of BarData objects
lab_path: Path to AlphaLab directory
Returns:
Path to the saved daily data file
"""
from vnpy.alpha.lab import AlphaLab
lab = AlphaLab(lab_path)
lab.save_bar_data(bars)
return lab.daily_path
+1
View File
@@ -0,0 +1 @@
"""Factor module tests."""
+8
View File
@@ -0,0 +1,8 @@
"""Test configuration for factor module tests."""
import sys
import os
# Add vnpy source to path
_VNPY_SRC = os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "..", "vnpy_v4.4.0"))
if _VNPY_SRC not in sys.path:
sys.path.insert(0, _VNPY_SRC)
+83
View File
@@ -0,0 +1,83 @@
"""Test data adapter module - BarData to AlphaLab polars conversion."""
import sys
import os
_VNPY_SRC = os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "..", "vnpy_v4.4.0"))
sys.path.insert(0, _VNPY_SRC)
import polars as pl
from datetime import datetime
from vnpy.trader.object import BarData
from vnpy.trader.constant import Exchange, Interval
def _make_bar(symbol, dt, close):
"""Helper to create test BarData."""
return BarData(
symbol=symbol,
exchange=Exchange.SSE,
datetime=dt,
interval=Interval.DAILY,
open_price=close,
high_price=close,
low_price=close,
close_price=close,
volume=1000,
gateway_name="TEST"
)
def test_convert_bars_to_alpha_df_columns():
"""Test that convert_bars_to_alpha_df produces correct column structure."""
bars = [_make_bar("600000", datetime(2024, 1, i), 10.0 + i) for i in range(1, 6)]
from sanguo_factor.data_adapter import convert_bars_to_alpha_df
df = convert_bars_to_alpha_df(bars)
assert isinstance(df, pl.DataFrame)
# SPIKE CORRECTION: AlphaLab.save_bar_data stores columns as: open, high, low, close (not open_price)
for col in ["vt_symbol", "datetime", "open", "high", "low", "close", "volume", "turnover", "open_interest"]:
assert col in df.columns, f"Missing column: {col}"
assert df.height == 5
def test_convert_bars_to_alpha_df_values():
"""Test that convert_bars_to_alpha_df correctly converts BarData values."""
bars = [_make_bar("600000", datetime(2024, 1, i), 10.0 + i) for i in range(1, 6)]
from sanguo_factor.data_adapter import convert_bars_to_alpha_df
df = convert_bars_to_alpha_df(bars)
# Check vt_symbol format
assert df["vt_symbol"][0] == "600000.SSE"
# Check price values (SPIKE CORRECTION: use open/high/low/close column names)
assert df["open"][0] == 10.0
assert df["close"][4] == 14.0
# Check datetime
assert df["datetime"][0] == datetime(2024, 1, 1)
def test_convert_empty_bars():
"""Test that empty bar list returns empty DataFrame with correct schema."""
from sanguo_factor.data_adapter import convert_bars_to_alpha_df
df = convert_bars_to_alpha_df([])
assert df.height == 0
# Should still have schema defined
assert len(df.columns) == 9 # vt_symbol, datetime, open, high, low, close, volume, turnover, open_interest
def test_save_alpha_lab_data():
"""Test save_alpha_lab_data creates AlphaLab and saves data."""
import tempfile
from pathlib import Path
from sanguo_factor.data_adapter import save_alpha_lab_data
bars = [_make_bar("600000", datetime(2024, 1, i), 10.0 + i) for i in range(1, 6)]
with tempfile.TemporaryDirectory() as tmpdir:
lab_path = Path(tmpdir) / "alpha_lab"
result_path = save_alpha_lab_data(bars, str(lab_path))
# Should return the daily_path
assert result_path is not None
assert "daily" in str(result_path)