feat(portfolio): LocalUnifiedProvider fundamentals baostock估值+akshare市值(Task3)
This commit is contained in:
@@ -340,3 +340,135 @@ class TestGetIndexStocks:
|
||||
p = LocalUnifiedProvider({"db_path": str(db), "data_dir": str(tmp_path)})
|
||||
stocks = p.get_index_stocks("000300", "2020-01-01")
|
||||
assert len(stocks) == 3
|
||||
|
||||
|
||||
# ======================== Task 3: get_fundamentals_df fixture ========================
|
||||
def _make_fundamentals_fixture(tmp_path):
|
||||
"""造 valuation_baostock + static/valuation + static/income + static/balance 样本。"""
|
||||
# 1. valuation_baostock/2024.parquet(baostock 权威: pe/pb/ps/pcf)
|
||||
vdir = tmp_path / "valuation_baostock"
|
||||
vdir.mkdir()
|
||||
pd.DataFrame({
|
||||
"symbol": ["600519"],
|
||||
"exchange": ["SH"],
|
||||
"date": ["2024-09-30"],
|
||||
"peTTM": [25.0],
|
||||
"psTTM": [15.0],
|
||||
"pcfNcfTTM": [20.0],
|
||||
"pbMRQ": [7.5],
|
||||
"turn": [0.1],
|
||||
"pctChg": [1.0],
|
||||
"isST": [0],
|
||||
}).to_parquet(vdir / "2024.parquet")
|
||||
|
||||
# 2. static/valuation akshare(市值/股本)
|
||||
sdir = tmp_path / "static" / "valuation"
|
||||
sdir.mkdir(parents=True)
|
||||
pd.DataFrame({
|
||||
"数据日期": ["2024-09-30"],
|
||||
"总市值": [2e12],
|
||||
"流通市值": [1.5e12],
|
||||
"总股本": [1.256e9],
|
||||
"PE(TTM)": [25.0],
|
||||
"市净率": [7.5],
|
||||
}).to_parquet(sdir / "600519.SH_valuation.parquet")
|
||||
|
||||
# 3. static/income akshare(eps + yoy + net_profit)
|
||||
idir = tmp_path / "static" / "income"
|
||||
idir.mkdir(parents=True)
|
||||
pd.DataFrame({
|
||||
"SECUCODE": ["600519.SH"],
|
||||
"REPORT_DATE": ["2024-09-30"],
|
||||
"REPORT_TYPE": ["Q3"],
|
||||
"BASIC_EPS": [41.0],
|
||||
"OPERATE_INCOME": [3.7e10],
|
||||
"PARENT_NETPROFIT": [9.5e9],
|
||||
"OPERATE_INCOME_YOY": [15.0],
|
||||
"OPERATE_PROFIT_YOY": [14.0],
|
||||
}).to_parquet(idir / "600519.SH_income.parquet")
|
||||
|
||||
# 4. static/balance akshare(资产/负债/权益)
|
||||
bdir = tmp_path / "static" / "balance"
|
||||
bdir.mkdir(parents=True)
|
||||
pd.DataFrame({
|
||||
"SECUCODE": ["600519.SH"],
|
||||
"REPORT_DATE": ["2024-09-30"],
|
||||
"REPORT_TYPE": ["Q3"],
|
||||
"TOTAL_ASSETS": [2.5e11],
|
||||
"TOTAL_LIABILITIES": [5.4e10],
|
||||
"TOTAL_PARENT_EQUITY": [2.2e11],
|
||||
"SURPLUS_RESERVE": [8e10],
|
||||
"UNASSIGN_RPOFIT": [7e10],
|
||||
}).to_parquet(bdir / "600519.SH_balance.parquet")
|
||||
|
||||
db = tmp_path / "t.db"
|
||||
c = sqlite3.connect(str(db))
|
||||
c.execute("CREATE TABLE dbbardata(symbol TEXT, exchange TEXT, datetime TEXT, interval TEXT)")
|
||||
c.execute("INSERT INTO dbbardata VALUES('600519','SSE','2024-09-30 00:00:00','d')")
|
||||
c.commit()
|
||||
c.close()
|
||||
return db
|
||||
|
||||
|
||||
# ======================== Task 3: get_fundamentals_df ========================
|
||||
class TestGetFundamentals:
|
||||
def test_pe_pb_from_baostock(self, tmp_path):
|
||||
db = _make_fundamentals_fixture(tmp_path)
|
||||
p = LocalUnifiedProvider({"db_path": str(db), "data_dir": str(tmp_path)})
|
||||
df = p.get_fundamentals_df(["600519.XSHG"], date="2024-09-30")
|
||||
assert not df.empty
|
||||
# baostock 权威: peTTM=25 / pbMRQ=7.5
|
||||
assert abs(df.loc["600519.XSHG", "pe_ratio"] - 25.0) < 1e-6
|
||||
assert abs(df.loc["600519.XSHG", "pb_ratio"] - 7.5) < 1e-6
|
||||
assert abs(df.loc["600519.XSHG", "ps_ratio"] - 15.0) < 1e-6
|
||||
assert abs(df.loc["600519.XSHG", "pcf_ratio"] - 20.0) < 1e-6
|
||||
|
||||
def test_market_cap_from_akshare(self, tmp_path):
|
||||
# baostock valuation 无市值列 → 从 static/valuation akshare 补
|
||||
db = _make_fundamentals_fixture(tmp_path)
|
||||
p = LocalUnifiedProvider({"db_path": str(db), "data_dir": str(tmp_path)})
|
||||
df = p.get_fundamentals_df(["600519.XSHG"], date="2024-09-30")
|
||||
# 2e12 元 → 2e4 亿
|
||||
assert abs(df.loc["600519.XSHG", "market_cap"] - 2e4) < 1
|
||||
# 1.5e12 元 → 1.5e4 亿
|
||||
assert abs(df.loc["600519.XSHG", "circulating_market_cap"] - 1.5e4) < 1
|
||||
|
||||
def test_three_tables_delegated(self, tmp_path):
|
||||
# 三表(income/balance)从 static akshare 读(委托 LocalParquetProvider)
|
||||
db = _make_fundamentals_fixture(tmp_path)
|
||||
p = LocalUnifiedProvider({"db_path": str(db), "data_dir": str(tmp_path)})
|
||||
df = p.get_fundamentals_df(["600519.XSHG"], date="2024-09-30")
|
||||
row = df.loc["600519.XSHG"]
|
||||
# eps 来自 income.BASIC_EPS
|
||||
assert abs(row["eps"] - 41.0) < 1e-6
|
||||
# 总负债 5.4e10 元 → 540 亿
|
||||
assert abs(row["total_liability"] - 540.0) < 1
|
||||
# 归母权益 2.2e11 元 → 2200 亿
|
||||
assert abs(row["total_sheet_owner_equities"] - 2200.0) < 1
|
||||
# 留存收益 = 盈余公积 8e10 + 未分配利润 7e10 = 1.5e11 元 → 1500 亿
|
||||
assert abs(row["retained_profit"] - 1500.0) < 1
|
||||
# OPERATE_INCOME_YOY 15.0% → 0.15
|
||||
assert abs(row["inc_revenue_year_on_year"] - 0.15) < 1e-6
|
||||
|
||||
def test_required_columns_present(self, tmp_path):
|
||||
db = _make_fundamentals_fixture(tmp_path)
|
||||
p = LocalUnifiedProvider({"db_path": str(db), "data_dir": str(tmp_path)})
|
||||
df = p.get_fundamentals_df(["600519.XSHG"], date="2024-09-30")
|
||||
# _FUNDAMENTAL_COLUMNS(对齐策略 all_weather)
|
||||
expected = [
|
||||
"code", "market_cap", "circulating_market_cap",
|
||||
"pe_ratio", "pb_ratio", "ps_ratio", "pcf_ratio",
|
||||
"roe", "roa", "eps", "gross_profit_margin", "net_profit_margin",
|
||||
"inc_revenue_year_on_year", "inc_operation_profit_year_on_year",
|
||||
"inc_total_revenue_year_on_year",
|
||||
"total_liability", "total_sheet_owner_equities", "retained_profit",
|
||||
"roic",
|
||||
]
|
||||
for col in expected:
|
||||
assert col in df.columns, f"missing col: {col}"
|
||||
|
||||
def test_empty_stocks_returns_empty(self, tmp_path):
|
||||
db = _make_fundamentals_fixture(tmp_path)
|
||||
p = LocalUnifiedProvider({"db_path": str(db), "data_dir": str(tmp_path)})
|
||||
df = p.get_fundamentals_df([], date="2024-09-30")
|
||||
assert df.empty
|
||||
|
||||
Reference in New Issue
Block a user