Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
62 changes: 40 additions & 22 deletions scripts/research_cn_dividend_quality_snapshot_proxy_backtest.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,10 @@
ProxyBacktestResult,
compute_backtest_metrics,
)
from cn_equity_strategies.backtest.promotion_gate import ( # noqa: E402
attach_research_evidence,
build_research_evidence,
)
from cn_equity_strategies.strategies import cn_dividend_quality_snapshot as dividend_strategy # noqa: E402
from cn_equity_strategies.strategies.etf_rotation_core import normalize_symbol # noqa: E402

Expand Down Expand Up @@ -89,6 +93,17 @@ def pipeline_normalize_symbol(symbol: Any) -> str:
CACHE_DIR = ROOT / ".cache" / "cn_equity_strategies"
CACHE_VERSION = "v1"

DIVIDEND_SNAPSHOT_RESEARCH_EVIDENCE = build_research_evidence(
research_only=True,
promotion_eligible=False,
point_in_time=False,
historical_membership_complete=False,
historical_constituents_complete=False,
adjustment_provenance_complete=False,
universe_provenance_complete=False,
survivorship_bias_controlled=False,
)


def _cache_path(prefix: str, *, parts: tuple[str, ...]) -> Path:
digest = hashlib.sha1("|".join([CACHE_VERSION, prefix, *parts]).encode("utf-8")).hexdigest()[:16]
Expand Down Expand Up @@ -738,29 +753,32 @@ def main() -> None:
"2021_2022": ("2021-01-01", "2022-12-31"),
"2023_2026": ("2023-01-01", args.end),
}
output = {
"profile": dividend_strategy.PROFILE_NAME,
"start": args.start,
"end": args.end,
"universe": list(universe),
"universe_mode": args.universe_mode,
"panel_diagnostics": panel_diag,
"strategy_full": strategy.metrics,
"benchmark_510300_full": benchmark.metrics,
"periods": {
key: {
"dividend_quality": _metrics_slice(strategy.daily_returns, pstart, pend),
"benchmark_510300": _metrics_slice(benchmark.daily_returns, pstart, pend),
}
for key, (pstart, pend) in periods.items()
output = attach_research_evidence(
{
"profile": dividend_strategy.PROFILE_NAME,
"start": args.start,
"end": args.end,
"universe": list(universe),
"universe_mode": args.universe_mode,
"panel_diagnostics": panel_diag,
"strategy_full": strategy.metrics,
"benchmark_510300_full": benchmark.metrics,
"periods": {
key: {
"dividend_quality": _metrics_slice(strategy.daily_returns, pstart, pend),
"benchmark_510300": _metrics_slice(benchmark.daily_returns, pstart, pend),
}
for key, (pstart, pend) in periods.items()
},
"limitations": [
f"universe_mode={args.universe_mode}; expanded pool uses fhps dividend-yield filter, not full A-share",
"fhps table uses latest available report table, not fully point-in-time report selection",
"proxy uses 510300 calendar + per-symbol ffill; monthly panel filters symbols with price history at as_of",
"evidence gate only; not promotion-ready without PIT fhps and live data validation",
],
},
"limitations": [
f"universe_mode={args.universe_mode}; expanded pool uses fhps dividend-yield filter, not full A-share",
"fhps table uses latest available report table, not fully point-in-time report selection",
"proxy uses 510300 calendar + per-symbol ffill; monthly panel filters symbols with price history at as_of",
"evidence gate only; not promotion-ready without PIT fhps and live data validation",
],
}
research_evidence=DIVIDEND_SNAPSHOT_RESEARCH_EVIDENCE,
)

print("\n========== P3.5 cn_dividend_quality_snapshot proxy ==========")
print(
Expand Down
44 changes: 33 additions & 11 deletions scripts/research_cn_industry_etf_rotation_aggressive_matrix.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,11 @@
sys.path.insert(0, candidate_str)

from cn_equity_strategies.backtest.proxy_simulator import ProxyBacktestConfig, run_proxy_backtest # noqa: E402
from cn_equity_strategies.backtest.promotion_gate import evaluate_promotion # noqa: E402
from cn_equity_strategies.backtest.promotion_gate import ( # noqa: E402
attach_research_evidence,
build_research_evidence,
evaluate_promotion,
)
from cn_equity_strategies.strategies import cn_industry_etf_rotation as industry_rotation # noqa: E402
from cn_equity_strategies.strategies.industry_etf_rotation_presets import ( # noqa: E402
AGGRESSIVE_RESEARCH_PRESETS,
Expand All @@ -33,6 +37,17 @@
_period_metrics,
)

INDUSTRY_ETF_RESEARCH_EVIDENCE = build_research_evidence(
research_only=True,
promotion_eligible=False,
point_in_time=False,
historical_membership_complete=False,
historical_constituents_complete=False,
adjustment_provenance_complete=False,
universe_provenance_complete=False,
survivorship_bias_controlled=False,
)


def _run_preset(market_history, key: str, preset: dict[str, Any]) -> dict[str, Any]:
rebalance_frequency = str(preset.get("rebalance_frequency") or "monthly")
Expand Down Expand Up @@ -92,16 +107,23 @@ def run_matrix(*, start: str, end: str, suite: str = "etf") -> dict[str, Any]:
market_history = _download_market_history(start=download_start, end=end)
presets = {"conservative_v1": CONSERVATIVE_V1_PRESET, **AGGRESSIVE_RESEARCH_PRESETS}
results = {key: _run_preset(market_history, key, preset) for key, preset in presets.items()}
promotion = evaluate_promotion(results, PROMOTION_GATE)
return {
"start": start,
"end": end,
"suite": suite,
"status": "research_backtest_only",
"conservative_baseline": "conservative_v1",
"variants": results,
"promotion_review": promotion,
}
promotion = evaluate_promotion(
results,
PROMOTION_GATE,
research_evidence=INDUSTRY_ETF_RESEARCH_EVIDENCE,
)
return attach_research_evidence(
{
"start": start,
"end": end,
"suite": suite,
"status": "research_backtest_only",
"conservative_baseline": "conservative_v1",
"variants": results,
"promotion_review": promotion,
},
research_evidence=INDUSTRY_ETF_RESEARCH_EVIDENCE,
)


def _print_report(payload: dict[str, Any]) -> None:
Expand Down
57 changes: 38 additions & 19 deletions scripts/research_cn_momentum_stock_rotation_proxy.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,11 @@
if candidate.exists() and candidate_str not in sys.path:
sys.path.insert(0, candidate_str)

from cn_equity_strategies.backtest.promotion_gate import evaluate_promotion # noqa: E402
from cn_equity_strategies.backtest.promotion_gate import ( # noqa: E402
attach_research_evidence,
build_research_evidence,
evaluate_promotion,
)
from cn_equity_strategies.research.momentum_stock_history import ( # noqa: E402
BENCHMARK_SYMBOL,
active_stock_symbols_at_start,
Expand All @@ -40,6 +44,17 @@

ResearchTrack = Literal["momentum", "thematic", "both"]

MOMENTUM_STOCK_RESEARCH_EVIDENCE = build_research_evidence(
research_only=True,
promotion_eligible=False,
point_in_time=False,
historical_membership_complete=False,
historical_constituents_complete=False,
adjustment_provenance_complete=False,
universe_provenance_complete=False,
survivorship_bias_controlled=False,
)


def _extra_symbols_for_preset(preset: dict[str, Any]) -> tuple[str, ...]:
extras: list[str] = []
Expand Down Expand Up @@ -166,26 +181,30 @@ def run_momentum_stock_matrix(
promotion = evaluate_promotion(
{"conservative_v1": conservative, **results},
STOCK_MOMENTUM_PROMOTION_GATE,
research_evidence=MOMENTUM_STOCK_RESEARCH_EVIDENCE,
)

return {
"start": start,
"end": end,
"track": "cross_section_momentum_riskoff" if suite == "csi500_riskoff" else "cross_section_momentum",
"status": "research_backtest_only",
"default_universe_mode": suite or universe_mode or "all_presets",
"conservative_etf_baseline": conservative,
"variants": results,
"promotion_review": promotion,
"promotion_gate": STOCK_MOMENTUM_PROMOTION_GATE,
"universe_diagnostics": universe_diagnostics,
"limitations": [
"index constituents (CSI500/1000) use latest csindex table — not fully point-in-time",
"liquid_top uses current spot turnover snapshot — not historical liquidity PIT",
"selection is cross-sectional momentum within broad pool, not fixed thematic industry list",
"not runtime-enabled; compare vs ETF conservative_v1 and thematic 8-name sleeve separately",
],
}
return attach_research_evidence(
{
"start": start,
"end": end,
"track": "cross_section_momentum_riskoff" if suite == "csi500_riskoff" else "cross_section_momentum",
"status": "research_backtest_only",
"default_universe_mode": suite or universe_mode or "all_presets",
"conservative_etf_baseline": conservative,
"variants": results,
"promotion_review": promotion,
"promotion_gate": STOCK_MOMENTUM_PROMOTION_GATE,
"universe_diagnostics": universe_diagnostics,
"limitations": [
"index constituents (CSI500/1000) use latest csindex table — not fully point-in-time",
"liquid_top uses current spot turnover snapshot — not historical liquidity PIT",
"selection is cross-sectional momentum within broad pool, not fixed thematic industry list",
"not runtime-enabled; compare vs ETF conservative_v1 and thematic 8-name sleeve separately",
],
},
research_evidence=MOMENTUM_STOCK_RESEARCH_EVIDENCE,
)


def run_thematic_reference(*, start: str, end: str) -> dict[str, Any]:
Expand Down
57 changes: 38 additions & 19 deletions scripts/research_cn_thematic_stock_rotation_proxy.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,11 @@
if candidate.exists() and candidate_str not in sys.path:
sys.path.insert(0, candidate_str)

from cn_equity_strategies.backtest.promotion_gate import evaluate_promotion # noqa: E402
from cn_equity_strategies.backtest.promotion_gate import ( # noqa: E402
attach_research_evidence,
build_research_evidence,
evaluate_promotion,
)
from cn_equity_strategies.strategies.industry_etf_rotation_presets import ( # noqa: E402
CONSERVATIVE_V1_PRESET,
OPTICAL_COMPUTE_STOCK_SYMBOLS,
Expand All @@ -37,6 +41,17 @@

_ETF_SYMBOL_PREFIXES = ("51", "15", "58")

THEMATIC_STOCK_RESEARCH_EVIDENCE = build_research_evidence(
research_only=True,
promotion_eligible=False,
point_in_time=False,
historical_membership_complete=False,
historical_constituents_complete=False,
adjustment_provenance_complete=False,
universe_provenance_complete=False,
survivorship_bias_controlled=False,
)


def _is_etf_symbol(symbol: str) -> bool:
normalized = str(symbol).strip()
Expand Down Expand Up @@ -173,26 +188,30 @@ def _preset_universe(preset: dict[str, Any]) -> tuple[str, ...]:
promotion = evaluate_promotion(
{"conservative_v1": conservative, **results},
STOCK_THEMATIC_PROMOTION_GATE,
research_evidence=THEMATIC_STOCK_RESEARCH_EVIDENCE,
)

return {
"start": start,
"end": end,
"suite": suite,
"status": "research_backtest_only",
"active_symbols": list(active),
"candidate_symbols": list(OPTICAL_COMPUTE_STOCK_SYMBOLS),
"conservative_etf_baseline": conservative,
"variants": results,
"promotion_review": promotion,
"promotion_gate": STOCK_THEMATIC_PROMOTION_GATE,
"limitations": [
"fixed optical/compute stock list (survivorship / theme hindsight)",
"requires overlapping listing history; universe shrinks to active names at start",
"not runtime-enabled; stricter gate vs ETF conservative_v1",
"risk presets may include ETF benchmark symbols (510300/512760) for risk-off",
],
}
return attach_research_evidence(
{
"start": start,
"end": end,
"suite": suite,
"status": "research_backtest_only",
"active_symbols": list(active),
"candidate_symbols": list(OPTICAL_COMPUTE_STOCK_SYMBOLS),
"conservative_etf_baseline": conservative,
"variants": results,
"promotion_review": promotion,
"promotion_gate": STOCK_THEMATIC_PROMOTION_GATE,
"limitations": [
"fixed optical/compute stock list (survivorship / theme hindsight)",
"requires overlapping listing history; universe shrinks to active names at start",
"not runtime-enabled; stricter gate vs ETF conservative_v1",
"risk presets may include ETF benchmark symbols (510300/512760) for risk-off",
],
},
research_evidence=THEMATIC_STOCK_RESEARCH_EVIDENCE,
)


def _print_report(payload: dict[str, Any]) -> None:
Expand Down
Loading