Files
sanguo_vnpy_v2/tests/portfolio/test_provider.py
T
claude_dev a68cf4905e feat(portfolio): sanguo_portfolio 组合策略框架(BulletTrade+miniQMT,不用jqdatasdk)
把聚宽"全天候轮动"(post48819)搬到 BulletTrade。融合=pip+扩展点注入
(SanguoMiniQmtProvider 继承 MiniQMTProvider 只 override get_fundamentals,
set_data_provider 公开 API 注入, BulletTrade 源码 0 改动)。

- providers: SanguoMiniQmtProvider 补 get_fundamentals(PershareIndex+自算PE/PS/PB/PCF/市值/ROIC)
- strategies/all_weather: 4选股函数+大小盘轮动+ETF兜底+涨停止损(聚宽风格翻译)
- factors(估值/ROIC自算) + filters(ST/涨跌停/次新/停牌)
- 88/88 测试 Mac+VPS 双过; VPS 回测 pipeline 跑通(修9bug:Capital单位/日期格式/百分数口径/11字段alias)
- 实盘 runner_live+runbook 就绪等交易日; DEFAULT_DATA_PROVIDER=miniqmt env 不装 jqdatasdk
- 文档: sanguo_portfolio_plan / portfolio_backtest_result / portfolio_live_runbook
2026-07-18 19:08:18 +08:00

144 lines
5.9 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""SanguoMiniQmtProvider 单元测试(mock xtquant)。
bullet-trade 装了(0.2.0+),MiniQMTProvider 基类可继承。
xtquant 没 装,通过 mock_xtquant fixture 注入 sys.modules。
"""
from __future__ import annotations
import math
import pandas as pd
import pytest
from sanguo_portfolio import SanguoMiniQmtProvider
pytestmark = pytest.mark.requires_bullet_trade
class TestSanguoMiniQmtProviderInstantiation:
def test_can_instantiate_with_mock_xtquant(self, mock_xtquant):
# Arrange + Act
provider = SanguoMiniQmtProvider({"mode": "backtest", "auto_download": False})
# Assert
assert provider.name == "sanguo_miniqmt"
# 应继承 MiniQMTProvider
from bullet_trade.data.providers.miniqmt import MiniQMTProvider
assert isinstance(provider, MiniQMTProvider)
class TestGetFundamentalsDf:
def test_returns_dataframe_with_required_columns(self, mock_xtquant):
# Arrange
provider = SanguoMiniQmtProvider({"mode": "backtest", "auto_download": False})
stocks = ["600519.XSHG", "601318.XSHG"]
# Act
df = provider.get_fundamentals_df(stocks, date="2024-09-30")
# Assert
assert isinstance(df, pd.DataFrame)
assert len(df) == 2
# 核心列都在
for col in [
"code", "market_cap", "circulating_market_cap",
"pe_ratio", "pb_ratio", "ps_ratio", "pcf_ratio",
"roe", "roa", "eps",
"total_liability", "total_sheet_owner_equities", "retained_profit",
"roic",
]:
assert col in df.columns, f"missing col: {col}"
# index 是 jq-style code
assert "600519.XSHG" in df.index
def test_empty_stocks_returns_empty_df(self, mock_xtquant):
provider = SanguoMiniQmtProvider({"mode": "backtest", "auto_download": False})
df = provider.get_fundamentals_df([], date="2024-09-30")
assert isinstance(df, pd.DataFrame)
assert len(df) == 0
# 空表也要有列定义,方便上层 select
assert "code" in df.columns
def test_market_cap_in_yi_unit(self, mock_xtquant):
"""close × total_capital / 1e8 = 亿元。茅台 1600 × 12.56e8 / 1e8 = 20096 亿。"""
provider = SanguoMiniQmtProvider({"mode": "backtest", "auto_download": False})
df = provider.get_fundamentals_df(["600519.XSHG"], date="2024-09-30")
mc = float(df.iloc[0]["market_cap"])
# 茅台市值应在 20000 亿左右(允许 close 1600±10)
assert 19000 < mc < 22000, f"market_cap 异常: {mc}"
def test_pe_ratio_finite_for_profitable_stock(self, mock_xtquant):
provider = SanguoMiniQmtProvider({"mode": "backtest", "auto_download": False})
df = provider.get_fundamentals_df(["600519.XSHG"], date="2024-09-30")
pe = float(df.iloc[0]["pe_ratio"])
assert math.isfinite(pe)
assert pe > 0
def test_roic_computed(self, mock_xtquant):
provider = SanguoMiniQmtProvider({"mode": "backtest", "auto_download": False})
df = provider.get_fundamentals_df(["600519.XSHG"], date="2024-09-30")
roic = float(df.iloc[0]["roic"])
# 茅台 mock 数据:oper=1.2e10, tax=25%, eqy=2.2e11, debt=0, cash=1.7e11
# NOPAT = 1.2e10 * 0.75 = 9e9
# IC = 2.2e11 + 0 - 1.7e11 = 5e10
# ROIC = 9e9 / 5e10 = 0.18
assert 0.05 < roic < 0.5, f"ROIC 异常: {roic}"
def test_financial_data_failure_returns_empty_df(self, mock_xtquant):
"""xtdata.get_financial_data 抛异常时返空表(不崩)。"""
mock_xtquant["xtdata"].get_financial_data.side_effect = Exception("QMT offline")
provider = SanguoMiniQmtProvider({"mode": "backtest", "auto_download": False})
df = provider.get_fundamentals_df(["600519.XSHG"], date="2024-09-30")
assert df.empty
class TestGetFundamentalsQueryDictMode:
def test_dict_with_stocks_returns_dataframe(self, mock_xtquant):
provider = SanguoMiniQmtProvider({"mode": "backtest", "auto_download": False})
query = {"stocks": ["600519.XSHG", "601318.XSHG"], "date": "2024-09-30"}
df = provider.get_fundamentals(query)
assert len(df) == 2
def test_dict_with_filter_callable_applied(self, mock_xtquant):
provider = SanguoMiniQmtProvider({"mode": "backtest", "auto_download": False})
query = {
"stocks": ["600519.XSHG", "601318.XSHG"],
"date": "2024-09-30",
"filter": lambda d: d["roe"] > 0.3, # 只保留茅台(归一后 roe=0.30)
}
df = provider.get_fundamentals(query)
assert len(df) == 1
assert df.iloc[0]["code"] == "600519.XSHG"
def test_dict_with_order_by_applied(self, mock_xtquant):
provider = SanguoMiniQmtProvider({"mode": "backtest", "auto_download": False})
query = {
"stocks": ["600519.XSHG", "601318.XSHG"],
"date": "2024-09-30",
"order_by": [("market_cap", "desc")],
}
df = provider.get_fundamentals(query)
assert df.iloc[0]["code"] == "600519.XSHG" # 茅台市值 > 平安
def test_dict_with_limit_applied(self, mock_xtquant):
provider = SanguoMiniQmtProvider({"mode": "backtest", "auto_download": False})
query = {
"stocks": ["600519.XSHG", "601318.XSHG"],
"date": "2024-09-30",
"limit": 1,
}
df = provider.get_fundamentals(query)
assert len(df) == 1
class TestSetDataProviderInjection:
def test_set_data_provider_accepts_sanguo_provider(self, mock_xtquant):
"""set_data_provider 注入 SanguoMiniQmtProvider 实例。"""
from bullet_trade.data.api import get_data_provider, set_data_provider
provider = SanguoMiniQmtProvider({"mode": "backtest", "auto_download": False})
# Act
set_data_provider(provider)
# Assert
active = get_data_provider()
assert active is provider
assert active.name == "sanguo_miniqmt"