a68cf4905e
把聚宽"全天候轮动"(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
144 lines
5.9 KiB
Python
144 lines
5.9 KiB
Python
"""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"
|