文件预览

schemas.py

查看 FP-DCF 技能包中的文件内容。

文件内容

fp_dcf/schemas.py

from __future__ import annotations

from dataclasses import asdict, dataclass, field


VALUATION_OUTPUT_CONTRACT_VERSION = "fp-dcf.valuation_output.v1"


@dataclass(slots=True)
class TaxAssumptions:
    effective_tax_rate: float | None = None
    effective_tax_rate_source: str | None = None
    marginal_tax_rate: float | None = None
    marginal_tax_rate_source: str | None = None


@dataclass(slots=True)
class CapitalStructure:
    equity_weight: float | None = None
    debt_weight: float | None = None
    source: str | None = None


@dataclass(slots=True)
class WACCInputs:
    risk_free_rate: float | None = None
    risk_free_rate_source: str | None = None
    equity_risk_premium: float | None = None
    equity_risk_premium_source: str | None = None
    beta: float | None = None
    beta_source: str | None = None
    cost_of_equity: float | None = None
    pre_tax_cost_of_debt: float | None = None
    pre_tax_cost_of_debt_source: str | None = None
    wacc: float | None = None


@dataclass(slots=True)
class FCFFSummary:
    anchor: float | None = None
    anchor_method: str | None = None
    selected_path: str | None = None
    anchor_ebiat_path: float | None = None
    anchor_cfo_path: float | None = None
    ebiat_path_available: bool | None = None
    cfo_path_available: bool | None = None
    after_tax_interest: float | None = None
    after_tax_interest_source: str | None = None
    reconciliation_gap: float | None = None
    reconciliation_gap_pct: float | None = None
    anchor_mode: str | None = None
    anchor_observation_count: int | None = None
    delta_nwc_source: str | None = None
    last_report_period: str | None = None

    @property
    def path_selected(self) -> str | None:
        return self.selected_path

    @path_selected.setter
    def path_selected(self, value: str | None) -> None:
        self.selected_path = value

    @property
    def ebiat_path_anchor(self) -> float | None:
        return self.anchor_ebiat_path

    @ebiat_path_anchor.setter
    def ebiat_path_anchor(self, value: float | None) -> None:
        self.anchor_ebiat_path = value

    @property
    def cfo_path_anchor(self) -> float | None:
        return self.anchor_cfo_path

    @cfo_path_anchor.setter
    def cfo_path_anchor(self, value: float | None) -> None:
        self.anchor_cfo_path = value

    @property
    def interest_adjustment_source(self) -> str | None:
        return self.after_tax_interest_source

    @interest_adjustment_source.setter
    def interest_adjustment_source(self, value: str | None) -> None:
        self.after_tax_interest_source = value


@dataclass(slots=True)
class MarketInputsSummary:
    enterprise_value_market: float | None = None
    enterprise_value_market_source: str | None = None
    equity_value_market: float | None = None
    market_price: float | None = None
    market_price_source: str | None = None
    shares_out: float | None = None
    shares_out_source: str | None = None
    net_debt: float | None = None
    net_debt_source: str | None = None

    def to_dict(self) -> dict:
        return asdict(self)


@dataclass(slots=True)
class DataFreshnessSummary:
    provider: str | None = None
    snapshot_as_of: str | None = None
    cache_created_at: str | None = None
    cache_age_hours: float | None = None
    freshness_class: str = "unknown"
    requires_refresh: bool = True

    def to_dict(self) -> dict:
        return asdict(self)


@dataclass(slots=True)
class MarketImpliedGrowthInput:
    enabled: bool = False
    lower_bound: float = -0.5
    upper_bound: float = 0.5
    solver: str = "auto"
    tolerance: float = 1e-6
    max_iterations: int = 100

    def to_dict(self) -> dict:
        return asdict(self)


@dataclass(slots=True)
class MarketImpliedGrowthOutput:
    enabled: bool = False
    success: bool = False
    valuation_model: str | None = None
    solved_field: str | None = None
    solved_value: float | None = None
    solver_used: str | None = None
    lower_bound: float | None = None
    upper_bound: float | None = None
    iterations: int | None = None
    residual: float | None = None
    market_price: float | None = None
    market_enterprise_value: float | None = None
    base_case_per_share_value: float | None = None
    base_case_enterprise_value: float | None = None
    message: str | None = None

    def to_dict(self) -> dict:
        return asdict(self)


@dataclass(slots=True)
class ValuationSummary:
    enterprise_value: float | None = None
    equity_value: float | None = None
    per_share_value: float | None = None
    terminal_growth_rate: float | None = None
    terminal_growth_rate_effective: float | None = None
    present_value_stage1: float | None = None
    present_value_stage2: float | None = None
    present_value_terminal: float | None = None
    terminal_value: float | None = None
    terminal_value_share: float | None = None
    explicit_forecast_years: int | None = None
    stage1_years: int | None = None
    stage2_years: int | None = None
    stage2_decay_mode: str | None = None

    def to_dict(self) -> dict:
        return asdict(self)


@dataclass(slots=True)
class ValuationOutput:
    ticker: str
    market: str
    valuation_model: str
    contract_version: str = VALUATION_OUTPUT_CONTRACT_VERSION
    requested_valuation_model: str | None = None
    effective_valuation_model: str | None = None
    degraded: bool = False
    degradation_reasons: list[str] = field(default_factory=list)
    currency: str | None = None
    as_of_date: str | None = None
    tax: TaxAssumptions = field(default_factory=TaxAssumptions)
    wacc_inputs: WACCInputs = field(default_factory=WACCInputs)
    capital_structure: CapitalStructure = field(default_factory=CapitalStructure)
    fcff: FCFFSummary = field(default_factory=FCFFSummary)
    valuation: ValuationSummary = field(default_factory=ValuationSummary)
    market_implied_growth: MarketImpliedGrowthOutput | None = None
    data_freshness: DataFreshnessSummary | None = None
    diagnostics: list[str] = field(default_factory=list)
    warnings: list[str] = field(default_factory=list)

    def to_dict(self) -> dict:
        payload = asdict(self)
        if payload.get("market_implied_growth") is None:
            payload.pop("market_implied_growth", None)
        if payload.get("data_freshness") is None:
            payload.pop("data_freshness", None)
        return payload


@dataclass(slots=True)
class SensitivityHeatmapOutput:
    ticker: str
    market: str
    valuation_model: str
    metric: str
    metric_label: str
    currency: str | None = None
    as_of_date: str | None = None
    base_wacc: float | None = None
    base_terminal_growth_rate: float | None = None
    base_metric_value: float | None = None
    market_price: float | None = None
    wacc_values: list[float] = field(default_factory=list)
    terminal_growth_values: list[float] = field(default_factory=list)
    matrix: list[list[float | None]] = field(default_factory=list)
    diagnostics: list[str] = field(default_factory=list)
    warnings: list[str] = field(default_factory=list)

    def to_dict(self) -> dict:
        return asdict(self)

    def to_summary_dict(
        self,
        *,
        include_grid: bool = False,
        exclude_diagnostics: set[str] | None = None,
        exclude_warnings: set[str] | None = None,
    ) -> dict:
        exclude_diagnostics = exclude_diagnostics or set()
        exclude_warnings = exclude_warnings or set()

        summary = {
            "metric": self.metric,
            "metric_label": self.metric_label,
            "base_wacc": self.base_wacc,
            "base_terminal_growth_rate": self.base_terminal_growth_rate,
            "base_metric_value": self.base_metric_value,
            "market_price": self.market_price,
            "wacc_axis": {
                "min": self.wacc_values[0] if self.wacc_values else None,
                "max": self.wacc_values[-1] if self.wacc_values else None,
                "points": len(self.wacc_values),
            },
            "terminal_growth_axis": {
                "min": self.terminal_growth_values[0] if self.terminal_growth_values else None,
                "max": self.terminal_growth_values[-1] if self.terminal_growth_values else None,
                "points": len(self.terminal_growth_values),
            },
            "diagnostics": [item for item in self.diagnostics if item not in exclude_diagnostics],
            "warnings": [item for item in self.warnings if item not in exclude_warnings],
        }

        if include_grid:
            summary["grid"] = {
                "wacc_values": list(self.wacc_values),
                "terminal_growth_values": list(self.terminal_growth_values),
                "matrix": [list(row) for row in self.matrix],
            }

        return summary