Files
sanguo_vnpy_v2/sanguo_portfolio/factors/valuation.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

130 lines
4.5 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.
"""估值因子:市值 + PE/PB/PS/PCF 自算。
为什么自己算而不是直接读 miniQMT:
- miniQMT 的 PershareIndex 给的是现成指标(ROE/ROA/EPS/毛利率...),**没有** valuation
表(PE/PB/PS/PCF/总市值/流通市值)。聚宽的 valuation 表是单独一张快照表,miniQMT
对应能力 = ``xtdata.get_market_data_ex`` 拿 close + ``Capital`` 拿股本 → 乘出来。
- 单期×4 近似 TTM:报告期利润/现金流是单季累计值,直接×4 当 TTM 用是**近似口径**,
对账聚宽时会偏差(聚宽是滚 4 季度精确 TTM)。回测对相对排序影响小,绝对估值会偏。
精确 TTM 滚 4 季度留 v2。
口径约定(字段名对齐聚宽 valuation 表):
- ``market_cap`` → 总市值(亿元) = close × total_capital / 1e8
- ``circulating_market_cap`` → 流通市值(亿元) = close × circulating_capital / 1e8
- ``pe_ratio`` → close × total_capital / (归母净利润 × 4) 近似 TTM
- ``pb_ratio`` → close × total_capital / 归母净资产
- ``ps_ratio`` → close × total_capital / (营业收入 × 4) 近似 TTM
- ``pcf_ratio`` → close × total_capital / (经营现金流 × 4) 近似 TTM
所有 ``calc_*`` 接受标量或 pandas Series/numpy array(靠 pandas/numpy 广播)。
"""
from __future__ import annotations
import math
from typing import Union
import numpy as np
import pandas as pd
NumberLike = Union[float, int, "pd.Series", "np.ndarray"]
# 分母下限,避免除零产生 inf 污染后续排序。1e-9 远小于任何实际财务数据。
_EPS = 1e-9
def calc_market_cap(close: NumberLike, total_capital: NumberLike) -> NumberLike:
"""总市值(元) = close × 总股本。聚宽 ``market_cap`` 是亿元,见 ``to_yi``。"""
return close * total_capital
def calc_circulating_market_cap(
close: NumberLike, circulating_capital: NumberLike
) -> NumberLike:
"""流通市值(元) = close × 流通股本。聚宽 ``circulating_market_cap`` 是亿元。"""
return close * circulating_capital
def _safe_denominator(value: NumberLike) -> NumberLike:
"""分母保护:把 |x|<eps 的值替换成带符号的 eps,保留正负号。"""
if isinstance(value, (pd.Series, pd.DataFrame)):
sign = np.sign(value)
sign = sign.where(sign != 0, 1.0)
return sign * value.abs().where(value.abs() > _EPS, _EPS)
if isinstance(value, np.ndarray):
sign = np.sign(value)
sign[sign == 0] = 1.0
abs_v = np.abs(value)
return sign * np.where(abs_v > _EPS, abs_v, _EPS)
if value is None or (isinstance(value, float) and math.isnan(value)):
return _EPS
abs_v = abs(value)
return value if abs_v > _EPS else (1.0 if value >= 0 else -1.0) * _EPS
def calc_pe(
close: NumberLike,
net_profit_excl_min_int: NumberLike,
total_capital: NumberLike,
) -> NumberLike:
"""市盈率(近似 TTM) = close × total_capital / (归母净利润 × 4)。
口径:``net_profit_excl_min_int`` 是最近报告期**单期**归母净利润,
×4 近似 TTM(标注"近似口径,对账聚宽时校准")。
亏损股返回负 PE,保留排序信息(聚宽也是负值)。
"""
numerator = close * total_capital
denominator = _safe_denominator(net_profit_excl_min_int) * 4
return numerator / denominator
def calc_pb(
close: NumberLike,
tot_shrhldr_eqy_excl_min_int: NumberLike,
total_capital: NumberLike,
) -> NumberLike:
"""市净率 = close × total_capital / 归母净资产。
口径:净资产是时点存量,不需要×4。
"""
numerator = close * total_capital
denominator = _safe_denominator(tot_shrhldr_eqy_excl_min_int)
return numerator / denominator
def calc_ps(
close: NumberLike,
revenue: NumberLike,
total_capital: NumberLike,
) -> NumberLike:
"""市销率(近似 TTM) = close × total_capital / (营业收入 × 4)。"""
numerator = close * total_capital
denominator = _safe_denominator(revenue) * 4
return numerator / denominator
def calc_pcf(
close: NumberLike,
net_oper_cash_flow: NumberLike,
total_capital: NumberLike,
) -> NumberLike:
"""市现率(近似 TTM) = close × total_capital / (经营现金流 × 4)。"""
numerator = close * total_capital
denominator = _safe_denominator(net_oper_cash_flow) * 4
return numerator / denominator
def to_yi(value: NumberLike) -> NumberLike:
"""元 → 亿元(聚宽 valuation 口径)。"""
return value / 1e8
__all__ = [
"calc_market_cap",
"calc_circulating_market_cap",
"calc_pe",
"calc_pb",
"calc_ps",
"calc_pcf",
"to_yi",
]