industry-slice-core 0.1.2__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- industry_slice_core-0.1.2/PKG-INFO +8 -0
- industry_slice_core-0.1.2/README.md +43 -0
- industry_slice_core-0.1.2/industry_slice_core/__init__.py +1 -0
- industry_slice_core-0.1.2/industry_slice_core/_contract.py +7 -0
- industry_slice_core-0.1.2/industry_slice_core/artifact_data.py +15 -0
- industry_slice_core-0.1.2/industry_slice_core/comps_template.py +126 -0
- industry_slice_core-0.1.2/industry_slice_core/comps_template_loader.py +69 -0
- industry_slice_core-0.1.2/industry_slice_core/data/comps_templates/industry_comps_generic_v1.fixture.csv +20 -0
- industry_slice_core-0.1.2/industry_slice_core/data/comps_templates/industry_comps_generic_v1.yaml +264 -0
- industry_slice_core-0.1.2/industry_slice_core/data/comps_templates/operating_comps_grocers_v1.fixture.csv +15 -0
- industry_slice_core-0.1.2/industry_slice_core/data/comps_templates/operating_comps_grocers_v1.yaml +197 -0
- industry_slice_core-0.1.2/industry_slice_core/data/comps_templates/operating_comps_hr_payroll_v1.fixture.csv +12 -0
- industry_slice_core-0.1.2/industry_slice_core/data/comps_templates/operating_comps_hr_payroll_v1.yaml +161 -0
- industry_slice_core-0.1.2/industry_slice_core/data/industry_kpis/caption_map.yaml +64 -0
- industry_slice_core-0.1.2/industry_slice_core/data/industry_kpis/grocers.yaml +139 -0
- industry_slice_core-0.1.2/industry_slice_core/data/industry_kpis/hr_payroll.yaml +59 -0
- industry_slice_core-0.1.2/industry_slice_core/data/industry_taxonomy.yaml +15 -0
- industry_slice_core-0.1.2/industry_slice_core/enum_canonicalizers.py +168 -0
- industry_slice_core-0.1.2/industry_slice_core/industry_resolver.py +72 -0
- industry_slice_core-0.1.2/industry_slice_core/industry_types.py +59 -0
- industry_slice_core-0.1.2/industry_slice_core/kpi_registry.py +104 -0
- industry_slice_core-0.1.2/industry_slice_core/kpi_registry_loader.py +22 -0
- industry_slice_core-0.1.2/industry_slice_core/manifest_bindings.json +27 -0
- industry_slice_core-0.1.2/industry_slice_core/manifest_bindings.py +113 -0
- industry_slice_core-0.1.2/industry_slice_core/metric_registry.json +97 -0
- industry_slice_core-0.1.2/industry_slice_core/metric_registry.py +103 -0
- industry_slice_core-0.1.2/industry_slice_core/numeric.py +54 -0
- industry_slice_core-0.1.2/industry_slice_core/profile_normalization.py +168 -0
- industry_slice_core-0.1.2/industry_slice_core/source_registry.py +256 -0
- industry_slice_core-0.1.2/industry_slice_core/strategy_categories/__init__.py +13 -0
- industry_slice_core-0.1.2/industry_slice_core/strategy_categories/defaults.yaml +24 -0
- industry_slice_core-0.1.2/industry_slice_core/strategy_categories/registry.py +159 -0
- industry_slice_core-0.1.2/industry_slice_core/thesis_shared_industry.py +174 -0
- industry_slice_core-0.1.2/industry_slice_core/thesis_shared_slice_helpers.py +165 -0
- industry_slice_core-0.1.2/industry_slice_core/thesis_shared_sources.py +457 -0
- industry_slice_core-0.1.2/industry_slice_core.egg-info/PKG-INFO +8 -0
- industry_slice_core-0.1.2/industry_slice_core.egg-info/SOURCES.txt +50 -0
- industry_slice_core-0.1.2/industry_slice_core.egg-info/dependency_links.txt +1 -0
- industry_slice_core-0.1.2/industry_slice_core.egg-info/requires.txt +3 -0
- industry_slice_core-0.1.2/industry_slice_core.egg-info/top_level.txt +1 -0
- industry_slice_core-0.1.2/pyproject.toml +17 -0
- industry_slice_core-0.1.2/setup.cfg +4 -0
- industry_slice_core-0.1.2/tests/test_artifact_loading.py +66 -0
- industry_slice_core-0.1.2/tests/test_comps_template_operating_comps.py +133 -0
- industry_slice_core-0.1.2/tests/test_industry_resolver.py +79 -0
- industry_slice_core-0.1.2/tests/test_industry_slice_imports.py +43 -0
- industry_slice_core-0.1.2/tests/test_kpi_registry.py +156 -0
- industry_slice_core-0.1.2/tests/test_numeric.py +56 -0
- industry_slice_core-0.1.2/tests/test_operating_comps_hr_payroll_fixture.py +61 -0
- industry_slice_core-0.1.2/tests/test_operating_comps_hr_payroll_yaml.py +63 -0
- industry_slice_core-0.1.2/tests/test_source_registry.py +261 -0
- industry_slice_core-0.1.2/tests/test_thesis_shared_sources.py +22 -0
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# industry-slice-core
|
|
2
|
+
|
|
3
|
+
Engine-free industry, comps, source and KPI contracts, plus the static data they
|
|
4
|
+
read. Pure types and loaders: no provider calls, no model engine, no publication
|
|
5
|
+
of accepted artifacts. The add-in gateway and the Risk service install the same
|
|
6
|
+
version so both sides parse the same payloads.
|
|
7
|
+
|
|
8
|
+
Tests: `python3 -m pytest packages/industry-slice-core/tests -q`
|
|
9
|
+
|
|
10
|
+
## Public surface
|
|
11
|
+
|
|
12
|
+
| Module | Owns |
|
|
13
|
+
|---|---|
|
|
14
|
+
| `industry_types` | `CompanyProfile`, `Unit`, `DriverCategory` — plain company and assumption types shared with the compiler |
|
|
15
|
+
| `industry_resolver` | `resolve_industry_key` over `data/industry_taxonomy.yaml` |
|
|
16
|
+
| `kpi_registry`, `kpi_registry_loader` | `KPI`/`KPIRegistry` types and `load_kpi_registry` per industry key |
|
|
17
|
+
| `comps_template`, `comps_template_loader` | Comps manifest types, manifest/fixture loading |
|
|
18
|
+
| `metric_registry` | Canonical provider-neutral `metric_id` vocabulary; `require_registered_metric_id` raises `UnknownMetricIdError` |
|
|
19
|
+
| `source_registry` | Identity/excerpt hashing and `register_source` for cited evidence |
|
|
20
|
+
| `thesis_shared_industry`, `thesis_shared_sources`, `thesis_shared_slice_helpers` | Shared landscape, comparison, snapshot and source-record shapes |
|
|
21
|
+
| `numeric`, `enum_canonicalizers`, `profile_normalization` | Sign/scale parsing, enum canonicalization, FMP record to provider-port values |
|
|
22
|
+
| `strategy_categories` | Strategy-category registry and `validate_strategy_category_id` |
|
|
23
|
+
| `manifest_bindings` | FMP adapter configuration for legacy comps-manifest bindings |
|
|
24
|
+
| `artifact_data` | `artifact_path` — an accepted runtime artifact wins over the bundled default |
|
|
25
|
+
|
|
26
|
+
Contract models derive from `_contract._ContractModel`: `extra="forbid"` and
|
|
27
|
+
whitespace-stripped strings, so an unknown field is a validation error on both
|
|
28
|
+
sides of the wire rather than a silently dropped value.
|
|
29
|
+
|
|
30
|
+
## Static data and runtime overrides
|
|
31
|
+
|
|
32
|
+
`industry_slice_core/data/` holds the bundled taxonomy, per-industry KPI files,
|
|
33
|
+
and comps manifests with their fixture rows. When `INDUSTRY_ARTIFACTS_DIR` is
|
|
34
|
+
set, `artifact_path` prefers `<dir>/industry_kpis/<file>` and
|
|
35
|
+
`<dir>/comps_templates/<file>` when that file exists and falls back to the
|
|
36
|
+
bundled copy otherwise, so a reviewed replacement is picked up on the next
|
|
37
|
+
request without a restart. This package reads that directory; it never writes to
|
|
38
|
+
it and never decides what is accepted.
|
|
39
|
+
|
|
40
|
+
In the Hank monorepo, `docs/reference/financial-modeling-studio.md` ("Industry
|
|
41
|
+
configuration") documents the consumer contract — which loader reads which file,
|
|
42
|
+
who owns `INDUSTRY_ARTIFACTS_DIR`, and how a new industry is staged for review.
|
|
43
|
+
That reference is outside this package; `packages/README.md` is the tree map.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Engine-free industry, comps, source and KPI contracts."""
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
"""Read accepted runtime industry artifacts ahead of bundled defaults."""
|
|
2
|
+
|
|
3
|
+
from importlib.resources import files
|
|
4
|
+
from importlib.resources.abc import Traversable
|
|
5
|
+
import os
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def artifact_path(directory: str, filename: str) -> Traversable:
|
|
10
|
+
root = os.environ.get("INDUSTRY_ARTIFACTS_DIR")
|
|
11
|
+
if root:
|
|
12
|
+
runtime_path = Path(root).expanduser() / directory / filename
|
|
13
|
+
if runtime_path.is_file():
|
|
14
|
+
return runtime_path
|
|
15
|
+
return files("industry_slice_core").joinpath("data", directory, filename)
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Literal
|
|
4
|
+
|
|
5
|
+
from pydantic import Field, field_validator, model_validator
|
|
6
|
+
|
|
7
|
+
from value_semantics_core.numeric_display import NumericDisplaySpec
|
|
8
|
+
from ._contract import _ContractModel
|
|
9
|
+
from industry_slice_core.metric_registry import require_registered_metric_id
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class CompsManifestSourceBinding(_ContractModel):
|
|
13
|
+
kind: Literal["concept", "metric", "transcript_kpi", "derived", "kpi"]
|
|
14
|
+
concept_id: str | None = None
|
|
15
|
+
metric_id: str | None = None
|
|
16
|
+
kpi_key: str | None = None
|
|
17
|
+
derived_formula: str | None = None
|
|
18
|
+
|
|
19
|
+
@model_validator(mode="after")
|
|
20
|
+
def _validate_binding_identity(self) -> "CompsManifestSourceBinding":
|
|
21
|
+
if self.kind == "concept" and not self.concept_id:
|
|
22
|
+
raise ValueError("concept bindings require concept_id")
|
|
23
|
+
if self.kind == "metric" and not self.metric_id:
|
|
24
|
+
raise ValueError("metric bindings require metric_id")
|
|
25
|
+
if self.kind == "metric":
|
|
26
|
+
self.metric_id = require_registered_metric_id(
|
|
27
|
+
self.metric_id,
|
|
28
|
+
entry_point="comps-manifest metric",
|
|
29
|
+
)
|
|
30
|
+
if self.kind != "concept" and self.concept_id is not None:
|
|
31
|
+
raise ValueError("concept_id is only valid for concept bindings")
|
|
32
|
+
if self.kind != "metric" and self.metric_id is not None:
|
|
33
|
+
raise ValueError("metric_id is only valid for metric bindings")
|
|
34
|
+
return self
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class CompsManifestMetric(_ContractModel):
|
|
38
|
+
key: str = Field(min_length=1)
|
|
39
|
+
label: str = Field(min_length=1)
|
|
40
|
+
display: NumericDisplaySpec
|
|
41
|
+
order: int = Field(ge=1)
|
|
42
|
+
aggregation: Literal["median", "mean", "weighted"] = "median"
|
|
43
|
+
null_policy: Literal["skip", "zero", "fail"] = "skip"
|
|
44
|
+
source: CompsManifestSourceBinding
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
class CompsManifestSection(_ContractModel):
|
|
48
|
+
name: str = Field(min_length=1)
|
|
49
|
+
order: int = Field(ge=1)
|
|
50
|
+
metrics: list[CompsManifestMetric]
|
|
51
|
+
|
|
52
|
+
@field_validator("metrics")
|
|
53
|
+
@classmethod
|
|
54
|
+
def _reject_duplicate_metric_keys(
|
|
55
|
+
cls, value: list[CompsManifestMetric]
|
|
56
|
+
) -> list[CompsManifestMetric]:
|
|
57
|
+
keys = [metric.key for metric in value]
|
|
58
|
+
duplicates = sorted({key for key in keys if keys.count(key) > 1})
|
|
59
|
+
if duplicates:
|
|
60
|
+
raise ValueError(f"duplicate metric keys: {duplicates}")
|
|
61
|
+
orders = [metric.order for metric in value]
|
|
62
|
+
duplicate_orders = sorted({order for order in orders if orders.count(order) > 1})
|
|
63
|
+
if duplicate_orders:
|
|
64
|
+
raise ValueError(f"duplicate metric orders: {duplicate_orders}")
|
|
65
|
+
return value
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
class CompsTemplateManifest(_ContractModel):
|
|
69
|
+
template_id: str = Field(min_length=1)
|
|
70
|
+
template_kind: Literal["industry_comps", "operating_comps"]
|
|
71
|
+
industry_key: str | None = None
|
|
72
|
+
source_sheet_key: str | None = Field(
|
|
73
|
+
default=None,
|
|
74
|
+
description="Comps sheet registry key for manifests bound to reusable sheets.",
|
|
75
|
+
)
|
|
76
|
+
source_gsheet_id: str | None = Field(
|
|
77
|
+
default=None,
|
|
78
|
+
description=(
|
|
79
|
+
"Deprecated legacy direct Google Sheet id; accepted for backcompat."
|
|
80
|
+
),
|
|
81
|
+
)
|
|
82
|
+
source_gsheet_version: str | None = Field(
|
|
83
|
+
default=None,
|
|
84
|
+
description="Deprecated legacy/source version stamp; accepted for backcompat.",
|
|
85
|
+
)
|
|
86
|
+
years: list[int] | None = None
|
|
87
|
+
years_back: int | None = None
|
|
88
|
+
sections: list[CompsManifestSection]
|
|
89
|
+
needs_re_emission: bool = Field(default=False, exclude=True)
|
|
90
|
+
|
|
91
|
+
@model_validator(mode="after")
|
|
92
|
+
def _validate_manifest(self) -> "CompsTemplateManifest":
|
|
93
|
+
if self.source_sheet_key is not None and self.source_gsheet_id is not None:
|
|
94
|
+
raise ValueError(
|
|
95
|
+
"source_sheet_key and source_gsheet_id are mutually exclusive when non-null"
|
|
96
|
+
)
|
|
97
|
+
if self.template_kind == "operating_comps" and not self.industry_key:
|
|
98
|
+
raise ValueError("industry_key is required for operating_comps templates")
|
|
99
|
+
if self.template_kind == "operating_comps":
|
|
100
|
+
has_years = self.years is not None
|
|
101
|
+
has_years_back = self.years_back is not None
|
|
102
|
+
if has_years == has_years_back:
|
|
103
|
+
raise ValueError(
|
|
104
|
+
"operating_comps templates require exactly one of years or years_back"
|
|
105
|
+
)
|
|
106
|
+
section_names = [section.name for section in self.sections]
|
|
107
|
+
duplicate_names = sorted(
|
|
108
|
+
{name for name in section_names if section_names.count(name) > 1}
|
|
109
|
+
)
|
|
110
|
+
if duplicate_names:
|
|
111
|
+
raise ValueError(f"duplicate section names: {duplicate_names}")
|
|
112
|
+
section_orders = [section.order for section in self.sections]
|
|
113
|
+
duplicate_orders = sorted(
|
|
114
|
+
{order for order in section_orders if section_orders.count(order) > 1}
|
|
115
|
+
)
|
|
116
|
+
if duplicate_orders:
|
|
117
|
+
raise ValueError(f"duplicate section orders: {duplicate_orders}")
|
|
118
|
+
return self
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
__all__ = [
|
|
122
|
+
"CompsManifestMetric",
|
|
123
|
+
"CompsManifestSection",
|
|
124
|
+
"CompsManifestSourceBinding",
|
|
125
|
+
"CompsTemplateManifest",
|
|
126
|
+
]
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
from importlib.resources.abc import Traversable
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
import yaml
|
|
8
|
+
|
|
9
|
+
from industry_slice_core.comps_template import CompsTemplateManifest
|
|
10
|
+
from industry_slice_core.manifest_bindings import (
|
|
11
|
+
translate_legacy_manifest_payload,
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
from .artifact_data import artifact_path
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def load_comps_template_manifest(
|
|
19
|
+
template_id: str,
|
|
20
|
+
*,
|
|
21
|
+
template_root: Path | None = None,
|
|
22
|
+
) -> CompsTemplateManifest:
|
|
23
|
+
normalized_id = str(template_id or "").strip()
|
|
24
|
+
if not normalized_id:
|
|
25
|
+
raise ValueError("template_id is required")
|
|
26
|
+
path = (
|
|
27
|
+
template_root / f"{normalized_id}.yaml"
|
|
28
|
+
if template_root is not None
|
|
29
|
+
else artifact_path("comps_templates", f"{normalized_id}.yaml")
|
|
30
|
+
)
|
|
31
|
+
return load_comps_template_manifest_path(path)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def load_comps_template_manifest_path(path: Path | Traversable) -> CompsTemplateManifest:
|
|
35
|
+
payload = yaml.safe_load(path.read_text(encoding="utf-8")) or {}
|
|
36
|
+
if not isinstance(payload, dict):
|
|
37
|
+
raise ValueError(f"comps template manifest must be a mapping: {path}")
|
|
38
|
+
translated, _changed = translate_legacy_manifest_payload(payload)
|
|
39
|
+
return CompsTemplateManifest.model_validate(translated)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def manifest_fixture_rows(manifest: CompsTemplateManifest) -> list[dict[str, Any]]:
|
|
43
|
+
rows: list[dict[str, Any]] = []
|
|
44
|
+
for section in sorted(manifest.sections, key=lambda item: item.order):
|
|
45
|
+
for metric in sorted(section.metrics, key=lambda item: item.order):
|
|
46
|
+
rows.append(
|
|
47
|
+
{
|
|
48
|
+
"section_order": section.order,
|
|
49
|
+
"section_name": section.name,
|
|
50
|
+
"metric_order": metric.order,
|
|
51
|
+
"metric_key": metric.key,
|
|
52
|
+
"label": metric.label,
|
|
53
|
+
"aggregation": metric.aggregation,
|
|
54
|
+
"null_policy": metric.null_policy,
|
|
55
|
+
"source_kind": metric.source.kind,
|
|
56
|
+
"concept_id": metric.source.concept_id or "",
|
|
57
|
+
"metric_id": metric.source.metric_id or "",
|
|
58
|
+
"kpi_key": metric.source.kpi_key or "",
|
|
59
|
+
"derived_formula": metric.source.derived_formula or "",
|
|
60
|
+
}
|
|
61
|
+
)
|
|
62
|
+
return rows
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
__all__ = [
|
|
66
|
+
"load_comps_template_manifest",
|
|
67
|
+
"load_comps_template_manifest_path",
|
|
68
|
+
"manifest_fixture_rows",
|
|
69
|
+
]
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
section_order,section_name,metric_order,metric_key,label,aggregation,null_policy,source_kind,concept_id,metric_id,kpi_key,derived_formula
|
|
2
|
+
1,Performance (LTM),1,revenue_ltm,Revenues ($M),median,skip,concept,revenue,,,
|
|
3
|
+
1,Performance (LTM),2,ebitda_ltm,EBITDA ($M),median,skip,concept,ebitda,,,
|
|
4
|
+
1,Performance (LTM),3,ebit_ltm,EBIT ($M),median,skip,metric,,ebit,,
|
|
5
|
+
1,Performance (LTM),4,dna_ltm,D&A ($M),median,skip,concept,depreciation_amortization,,,
|
|
6
|
+
1,Performance (LTM),5,revenue_growth_ltm,Revenue Growth,median,skip,metric,,revenue_growth,,
|
|
7
|
+
1,Performance (LTM),6,roe_ltm,ROE,median,skip,metric,,return_on_equity,,
|
|
8
|
+
2,Valuation,1,ev_revenue_ltm,EV / Revenue,median,skip,metric,,ev_revenue,,
|
|
9
|
+
2,Valuation,2,ev_ebitda_ltm,EV / EBITDA,median,skip,metric,,ev_ebitda,,
|
|
10
|
+
2,Valuation,3,net_debt_ltm,Net Debt ($M),median,skip,metric,,net_debt,,
|
|
11
|
+
2,Valuation,4,cash_ltm,Cash ($M),median,skip,concept,cash_and_equivalents,,,
|
|
12
|
+
3,EPS forward,1,eps_fy1,FY1 EPS,median,skip,metric,,eps_avg,,
|
|
13
|
+
3,EPS forward,2,eps_fy2,FY2 EPS,median,skip,metric,,eps_avg,,
|
|
14
|
+
3,EPS forward,3,eps_fy3,FY3 EPS,median,skip,metric,,eps_avg,,
|
|
15
|
+
3,EPS forward,4,eps_2y_cagr,2Y EPS CAGR,median,skip,derived,,,,(eps_fy3 / eps_fy1) ** 0.5 - 1
|
|
16
|
+
4,P/E multiples,1,pe_ltm,P / E,median,skip,metric,,reported_pe_ratio,,
|
|
17
|
+
4,P/E multiples,2,pe_fy1,FY1 P / E,median,skip,derived,,,,price / eps_fy1
|
|
18
|
+
5,Dividend,1,dividend_yield,Dividend Yield,median,skip,metric,,dividend_yield,,
|
|
19
|
+
5,Dividend,2,dividend_per_share,Dividend / Share,median,skip,metric,,dividend_per_share,,
|
|
20
|
+
5,Dividend,3,dividends_paid_ltm,Dividends Paid ($M),median,skip,concept,dividends_paid,,,
|
industry_slice_core-0.1.2/industry_slice_core/data/comps_templates/industry_comps_generic_v1.yaml
ADDED
|
@@ -0,0 +1,264 @@
|
|
|
1
|
+
template_id: "industry_comps_generic_v1"
|
|
2
|
+
template_kind: "industry_comps"
|
|
3
|
+
industry_key: null
|
|
4
|
+
source_gsheet_id: null
|
|
5
|
+
source_gsheet_version: "track0_fixture_2026_05_07"
|
|
6
|
+
sections:
|
|
7
|
+
- name: "Performance (LTM)"
|
|
8
|
+
order: 1
|
|
9
|
+
metrics:
|
|
10
|
+
- key: "revenue_ltm"
|
|
11
|
+
label: "Revenues ($M)"
|
|
12
|
+
display:
|
|
13
|
+
numeric_kind: currency
|
|
14
|
+
scale: millions
|
|
15
|
+
currency: USD
|
|
16
|
+
precision: 1
|
|
17
|
+
order: 1
|
|
18
|
+
aggregation: "median"
|
|
19
|
+
null_policy: "skip"
|
|
20
|
+
source:
|
|
21
|
+
kind: "concept"
|
|
22
|
+
concept_id: "revenue"
|
|
23
|
+
- key: "ebitda_ltm"
|
|
24
|
+
label: "EBITDA ($M)"
|
|
25
|
+
display:
|
|
26
|
+
numeric_kind: currency
|
|
27
|
+
scale: millions
|
|
28
|
+
currency: USD
|
|
29
|
+
precision: 1
|
|
30
|
+
order: 2
|
|
31
|
+
aggregation: "median"
|
|
32
|
+
null_policy: "skip"
|
|
33
|
+
source:
|
|
34
|
+
kind: "concept"
|
|
35
|
+
concept_id: "ebitda"
|
|
36
|
+
- key: "ebit_ltm"
|
|
37
|
+
label: "EBIT ($M)"
|
|
38
|
+
display:
|
|
39
|
+
numeric_kind: currency
|
|
40
|
+
scale: millions
|
|
41
|
+
currency: USD
|
|
42
|
+
precision: 1
|
|
43
|
+
order: 3
|
|
44
|
+
aggregation: "median"
|
|
45
|
+
null_policy: "skip"
|
|
46
|
+
source:
|
|
47
|
+
kind: "metric"
|
|
48
|
+
metric_id: "ebit"
|
|
49
|
+
- key: "dna_ltm"
|
|
50
|
+
label: "D&A ($M)"
|
|
51
|
+
display:
|
|
52
|
+
numeric_kind: currency
|
|
53
|
+
scale: millions
|
|
54
|
+
currency: USD
|
|
55
|
+
precision: 1
|
|
56
|
+
order: 4
|
|
57
|
+
aggregation: "median"
|
|
58
|
+
null_policy: "skip"
|
|
59
|
+
source:
|
|
60
|
+
kind: "concept"
|
|
61
|
+
concept_id: "depreciation_amortization"
|
|
62
|
+
- key: "revenue_growth_ltm"
|
|
63
|
+
label: "Revenue Growth"
|
|
64
|
+
# arrives fraction
|
|
65
|
+
display:
|
|
66
|
+
numeric_kind: percentage
|
|
67
|
+
scale: unit
|
|
68
|
+
precision: 1
|
|
69
|
+
order: 5
|
|
70
|
+
aggregation: "median"
|
|
71
|
+
null_policy: "skip" # revenueGrowthTTM unavailable; data gap
|
|
72
|
+
source:
|
|
73
|
+
kind: "metric"
|
|
74
|
+
metric_id: "revenue_growth"
|
|
75
|
+
- key: "roe_ltm"
|
|
76
|
+
label: "ROE"
|
|
77
|
+
# arrives fraction
|
|
78
|
+
display:
|
|
79
|
+
numeric_kind: percentage
|
|
80
|
+
scale: unit
|
|
81
|
+
precision: 1
|
|
82
|
+
order: 6
|
|
83
|
+
aggregation: "median"
|
|
84
|
+
null_policy: "skip"
|
|
85
|
+
source:
|
|
86
|
+
kind: "metric"
|
|
87
|
+
metric_id: "return_on_equity"
|
|
88
|
+
- name: "Valuation"
|
|
89
|
+
order: 2
|
|
90
|
+
metrics:
|
|
91
|
+
- key: "ev_revenue_ltm"
|
|
92
|
+
label: "EV / Revenue"
|
|
93
|
+
display:
|
|
94
|
+
numeric_kind: multiple
|
|
95
|
+
scale: unit
|
|
96
|
+
precision: 1
|
|
97
|
+
order: 1
|
|
98
|
+
aggregation: "median"
|
|
99
|
+
null_policy: "skip"
|
|
100
|
+
source:
|
|
101
|
+
kind: "metric"
|
|
102
|
+
metric_id: "ev_revenue"
|
|
103
|
+
- key: "ev_ebitda_ltm"
|
|
104
|
+
label: "EV / EBITDA"
|
|
105
|
+
display:
|
|
106
|
+
numeric_kind: multiple
|
|
107
|
+
scale: unit
|
|
108
|
+
precision: 1
|
|
109
|
+
order: 2
|
|
110
|
+
aggregation: "median"
|
|
111
|
+
null_policy: "skip"
|
|
112
|
+
source:
|
|
113
|
+
kind: "metric"
|
|
114
|
+
metric_id: "ev_ebitda"
|
|
115
|
+
- key: "net_debt_ltm"
|
|
116
|
+
label: "Net Debt ($M)"
|
|
117
|
+
display:
|
|
118
|
+
numeric_kind: currency
|
|
119
|
+
scale: millions
|
|
120
|
+
currency: USD
|
|
121
|
+
precision: 1
|
|
122
|
+
order: 3
|
|
123
|
+
aggregation: "median"
|
|
124
|
+
null_policy: "skip"
|
|
125
|
+
source:
|
|
126
|
+
kind: "metric"
|
|
127
|
+
metric_id: "net_debt"
|
|
128
|
+
- key: "cash_ltm"
|
|
129
|
+
label: "Cash ($M)"
|
|
130
|
+
display:
|
|
131
|
+
numeric_kind: currency
|
|
132
|
+
scale: millions
|
|
133
|
+
currency: USD
|
|
134
|
+
precision: 1
|
|
135
|
+
order: 4
|
|
136
|
+
aggregation: "median"
|
|
137
|
+
null_policy: "skip"
|
|
138
|
+
source:
|
|
139
|
+
kind: "concept"
|
|
140
|
+
concept_id: "cash_and_equivalents"
|
|
141
|
+
- name: "EPS forward"
|
|
142
|
+
order: 3
|
|
143
|
+
metrics:
|
|
144
|
+
- key: "eps_fy1"
|
|
145
|
+
label: "FY1 EPS"
|
|
146
|
+
display:
|
|
147
|
+
numeric_kind: per_share
|
|
148
|
+
scale: unit
|
|
149
|
+
currency: USD
|
|
150
|
+
precision: 1
|
|
151
|
+
order: 1
|
|
152
|
+
aggregation: "median"
|
|
153
|
+
null_policy: "skip"
|
|
154
|
+
source:
|
|
155
|
+
kind: "metric"
|
|
156
|
+
metric_id: "eps_avg"
|
|
157
|
+
- key: "eps_fy2"
|
|
158
|
+
label: "FY2 EPS"
|
|
159
|
+
display:
|
|
160
|
+
numeric_kind: per_share
|
|
161
|
+
scale: unit
|
|
162
|
+
currency: USD
|
|
163
|
+
precision: 1
|
|
164
|
+
order: 2
|
|
165
|
+
aggregation: "median"
|
|
166
|
+
null_policy: "skip"
|
|
167
|
+
source:
|
|
168
|
+
kind: "metric"
|
|
169
|
+
metric_id: "eps_avg"
|
|
170
|
+
- key: "eps_fy3"
|
|
171
|
+
label: "FY3 EPS"
|
|
172
|
+
display:
|
|
173
|
+
numeric_kind: per_share
|
|
174
|
+
scale: unit
|
|
175
|
+
currency: USD
|
|
176
|
+
precision: 1
|
|
177
|
+
order: 3
|
|
178
|
+
aggregation: "median"
|
|
179
|
+
null_policy: "skip"
|
|
180
|
+
source:
|
|
181
|
+
kind: "metric"
|
|
182
|
+
metric_id: "eps_avg"
|
|
183
|
+
- key: "eps_2y_cagr"
|
|
184
|
+
label: "2Y EPS CAGR"
|
|
185
|
+
# arrives fraction
|
|
186
|
+
display:
|
|
187
|
+
numeric_kind: percentage
|
|
188
|
+
scale: unit
|
|
189
|
+
precision: 1
|
|
190
|
+
order: 4
|
|
191
|
+
aggregation: "median"
|
|
192
|
+
null_policy: "skip"
|
|
193
|
+
source:
|
|
194
|
+
kind: "derived"
|
|
195
|
+
derived_formula: "(eps_fy3 / eps_fy1) ** 0.5 - 1"
|
|
196
|
+
- name: "P/E multiples"
|
|
197
|
+
order: 4
|
|
198
|
+
metrics:
|
|
199
|
+
- key: "pe_ltm"
|
|
200
|
+
label: "P / E"
|
|
201
|
+
display:
|
|
202
|
+
numeric_kind: multiple
|
|
203
|
+
scale: unit
|
|
204
|
+
precision: 1
|
|
205
|
+
order: 1
|
|
206
|
+
aggregation: "median"
|
|
207
|
+
null_policy: "skip"
|
|
208
|
+
source:
|
|
209
|
+
kind: "metric"
|
|
210
|
+
metric_id: "reported_pe_ratio"
|
|
211
|
+
- key: "pe_fy1"
|
|
212
|
+
label: "FY1 P / E"
|
|
213
|
+
display:
|
|
214
|
+
numeric_kind: multiple
|
|
215
|
+
scale: unit
|
|
216
|
+
precision: 1
|
|
217
|
+
order: 2
|
|
218
|
+
aggregation: "median"
|
|
219
|
+
null_policy: "skip"
|
|
220
|
+
source:
|
|
221
|
+
kind: "derived"
|
|
222
|
+
derived_formula: "price / eps_fy1"
|
|
223
|
+
- name: "Dividend"
|
|
224
|
+
order: 5
|
|
225
|
+
metrics:
|
|
226
|
+
- key: "dividend_yield"
|
|
227
|
+
label: "Dividend Yield"
|
|
228
|
+
# arrives fraction
|
|
229
|
+
display:
|
|
230
|
+
numeric_kind: percentage
|
|
231
|
+
scale: unit
|
|
232
|
+
precision: 1
|
|
233
|
+
order: 1
|
|
234
|
+
aggregation: "median"
|
|
235
|
+
null_policy: "skip"
|
|
236
|
+
source:
|
|
237
|
+
kind: "metric"
|
|
238
|
+
metric_id: "dividend_yield"
|
|
239
|
+
- key: "dividend_per_share"
|
|
240
|
+
label: "Dividend / Share"
|
|
241
|
+
display:
|
|
242
|
+
numeric_kind: per_share
|
|
243
|
+
scale: unit
|
|
244
|
+
currency: USD
|
|
245
|
+
precision: 1
|
|
246
|
+
order: 2
|
|
247
|
+
aggregation: "median"
|
|
248
|
+
null_policy: "skip"
|
|
249
|
+
source:
|
|
250
|
+
kind: "metric"
|
|
251
|
+
metric_id: "dividend_per_share"
|
|
252
|
+
- key: "dividends_paid_ltm"
|
|
253
|
+
label: "Dividends Paid ($M)"
|
|
254
|
+
display:
|
|
255
|
+
numeric_kind: currency
|
|
256
|
+
scale: millions
|
|
257
|
+
currency: USD
|
|
258
|
+
precision: 1
|
|
259
|
+
order: 3
|
|
260
|
+
aggregation: "median"
|
|
261
|
+
null_policy: "skip"
|
|
262
|
+
source:
|
|
263
|
+
kind: "concept"
|
|
264
|
+
concept_id: "dividends_paid"
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
section_order,section_name,metric_order,metric_key,label,aggregation,null_policy,source_kind,concept_id,metric_id,kpi_key,derived_formula
|
|
2
|
+
1,Growth,1,revenue,Revenue,median,skip,concept,revenue,,,
|
|
3
|
+
1,Growth,2,sales_growth,Sales Growth,median,skip,derived,,,,revenue / revenue_prior - 1
|
|
4
|
+
2,Customer Base / Volume,1,total_employees,Total Associates Employed,median,skip,kpi,,,total_employees,
|
|
5
|
+
2,Customer Base / Volume,2,loyalty_program_members,Annual Households Served,median,skip,kpi,,,loyalty_program_members,
|
|
6
|
+
2,Customer Base / Volume,3,total_stores_in_operation,Total Supermarkets Operated,median,skip,kpi,,,total_stores_in_operation,
|
|
7
|
+
2,Customer Base / Volume,4,new_store_openings,Cumulative New Store Openings,median,skip,kpi,,,new_store_openings,
|
|
8
|
+
2,Customer Base / Volume,5,comparable_sales_growth,Identical Sales Growth Ex-fuel Ex-Labor Dispute,median,skip,kpi,,,comparable_sales_growth,
|
|
9
|
+
2,Customer Base / Volume,6,gross_margin_rate,GAAP Gross Margin Rate,median,skip,kpi,,,gross_margin_rate,
|
|
10
|
+
3,Investment,1,sm_expense,S&M Expense,median,skip,concept,sales_and_marketing,,,
|
|
11
|
+
3,Investment,2,rd_expense,R&D Expense,median,skip,concept,research_and_development,,,
|
|
12
|
+
3,Investment,3,capex_ratio,CapEx / Revenue,median,skip,metric,,capex_to_revenue,,
|
|
13
|
+
4,Margins,1,ebitda_margin,EBITDA Margin,median,skip,metric,,ebitda_margin,,
|
|
14
|
+
4,Margins,2,fcf_margin,FCF Margin,median,skip,derived,,,,freeCashFlow / revenue
|
|
15
|
+
4,Margins,3,fcf_conversion,FCF Conversion,median,skip,metric,,fcf_conversion,,
|