From 41c5e8c3598e66ef9c81f023598babfb8f1d5e79 Mon Sep 17 00:00:00 2001 From: claude_dev Date: Mon, 6 Jul 2026 07:59:32 +0800 Subject: [PATCH] =?UTF-8?q?feat(factor):=20data=5Fadapter=20BarData?= =?UTF-8?q?=E2=86=92AlphaLab=20polars=20=E8=BD=AC=E6=8D=A2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 实现 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 环境安装困难 - 代码逻辑正确,待环境配置后验证测试 --- sanguo_factor/__init__.py | 1 + sanguo_factor/data_adapter.py | 69 +++++++++++++++++++++++++ tests/factor/__init__.py | 1 + tests/factor/conftest.py | 8 +++ tests/factor/test_data_adapter.py | 83 +++++++++++++++++++++++++++++++ 5 files changed, 162 insertions(+) create mode 100644 sanguo_factor/__init__.py create mode 100644 sanguo_factor/data_adapter.py create mode 100644 tests/factor/__init__.py create mode 100644 tests/factor/conftest.py create mode 100644 tests/factor/test_data_adapter.py diff --git a/sanguo_factor/__init__.py b/sanguo_factor/__init__.py new file mode 100644 index 0000000..a3cd9c5 --- /dev/null +++ b/sanguo_factor/__init__.py @@ -0,0 +1 @@ +"""Sanguo factor module for vnpy alpha strategies.""" diff --git a/sanguo_factor/data_adapter.py b/sanguo_factor/data_adapter.py new file mode 100644 index 0000000..12d980c --- /dev/null +++ b/sanguo_factor/data_adapter.py @@ -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 diff --git a/tests/factor/__init__.py b/tests/factor/__init__.py new file mode 100644 index 0000000..1977076 --- /dev/null +++ b/tests/factor/__init__.py @@ -0,0 +1 @@ +"""Factor module tests.""" diff --git a/tests/factor/conftest.py b/tests/factor/conftest.py new file mode 100644 index 0000000..8ce958e --- /dev/null +++ b/tests/factor/conftest.py @@ -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) diff --git a/tests/factor/test_data_adapter.py b/tests/factor/test_data_adapter.py new file mode 100644 index 0000000..f44ebc1 --- /dev/null +++ b/tests/factor/test_data_adapter.py @@ -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)