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)