Add attribution beta semantics metadata

This commit is contained in:
2026-04-07 17:36:42 +08:00
parent 82a3e63c2b
commit 097131d962
2 changed files with 194 additions and 29 deletions

View File

@@ -1,5 +1,6 @@
import http.client
import contextlib
import json
import io
import socket
import ssl
@@ -640,6 +641,7 @@ class AttributionIntegrationTests(unittest.TestCase):
"model",
"factor_source",
"proxy_only",
"beta_semantics",
"start_date",
"end_date",
"n_obs",
@@ -664,6 +666,19 @@ class AttributionIntegrationTests(unittest.TestCase):
self.assertEqual(summary.loc[0, "model"], "ff5")
self.assertEqual(summary.loc[0, "factor_source"], "external+local")
self.assertFalse(bool(summary.loc[0, "proxy_only"]))
self.assertEqual(
json.loads(summary.loc[0, "beta_semantics"]),
{
"beta_mkt": "MKT_RF",
"beta_smb": "SMB",
"beta_hml": "HML",
"beta_rmw": "RMW",
"beta_cma": "CMA",
"beta_mom": "MOM",
"beta_lowvol": "LOWVOL",
"beta_recovery": "RECOVERY",
},
)
self.assertAlmostEqual(summary.loc[0, "beta_mkt"], 1.10, places=3)
self.assertAlmostEqual(summary.loc[0, "beta_smb"], -0.25, places=3)
self.assertAlmostEqual(summary.loc[0, "beta_hml"], 0.35, places=3)
@@ -704,6 +719,19 @@ class AttributionIntegrationTests(unittest.TestCase):
self.assertEqual(summary.loc[0, "model"], "proxy")
self.assertEqual(summary.loc[0, "factor_source"], "proxy_only")
self.assertTrue(bool(summary.loc[0, "proxy_only"]))
self.assertEqual(
json.loads(summary.loc[0, "beta_semantics"]),
{
"beta_mkt": "MKT",
"beta_smb": "SMB_PROXY",
"beta_hml": "HML_PROXY",
"beta_rmw": "RMW_PROXY",
"beta_cma": "CMA_PROXY",
"beta_mom": "MOM",
"beta_lowvol": "LOWVOL",
"beta_recovery": "RECOVERY",
},
)
self.assertNotIn("beta_smb_proxy", summary.columns)
self.assertNotIn("beta_hml_proxy", summary.columns)
self.assertNotIn("beta_rmw_proxy", summary.columns)
@@ -716,6 +744,13 @@ class AttributionIntegrationTests(unittest.TestCase):
set(loadings["factor"]),
{"MKT", "SMB_PROXY", "HML_PROXY", "RMW_PROXY", "CMA_PROXY", "MOM", "LOWVOL", "RECOVERY"},
)
loadings_by_factor = loadings.set_index("factor")["beta"]
semantics = json.loads(summary.loc[0, "beta_semantics"])
self.assertAlmostEqual(summary.loc[0, "beta_mkt"], loadings_by_factor[semantics["beta_mkt"]], places=10)
self.assertAlmostEqual(summary.loc[0, "beta_smb"], loadings_by_factor[semantics["beta_smb"]], places=10)
self.assertAlmostEqual(summary.loc[0, "beta_hml"], loadings_by_factor[semantics["beta_hml"]], places=10)
self.assertAlmostEqual(summary.loc[0, "beta_rmw"], loadings_by_factor[semantics["beta_rmw"]], places=10)
self.assertAlmostEqual(summary.loc[0, "beta_cma"], loadings_by_factor[semantics["beta_cma"]], places=10)
def test_attribute_strategies_without_benchmark_uses_equal_weight_proxy_market(self):
dates = pd.date_range("2025-01-01", periods=320, freq="B")
@@ -802,6 +837,18 @@ class AttributionIntegrationTests(unittest.TestCase):
"model": "proxy",
"factor_source": "proxy_only",
"proxy_only": True,
"beta_semantics": json.dumps(
{
"beta_mkt": "MKT",
"beta_smb": "SMB_PROXY",
"beta_hml": "HML_PROXY",
"beta_rmw": "RMW_PROXY",
"beta_cma": "CMA_PROXY",
"beta_mom": "MOM",
"beta_lowvol": "LOWVOL",
"beta_recovery": "RECOVERY",
}
),
"start_date": "2025-01-02",
"end_date": "2026-03-24",
"n_obs": 319,
@@ -834,6 +881,96 @@ class AttributionIntegrationTests(unittest.TestCase):
self.assertIn("SMB_PROXY", output)
self.assertNotIn(" beta_smb ", output)
def test_print_attribution_summary_splits_standard_and_proxy_sections_for_mixed_frames(self):
summary = pd.DataFrame(
[
{
"strategy": "US Strategy",
"market": "us",
"model": "ff5",
"factor_source": "external+local",
"proxy_only": False,
"beta_semantics": json.dumps(
{
"beta_mkt": "MKT_RF",
"beta_smb": "SMB",
"beta_hml": "HML",
"beta_rmw": "RMW",
"beta_cma": "CMA",
"beta_mom": "MOM",
"beta_lowvol": "LOWVOL",
"beta_recovery": "RECOVERY",
}
),
"start_date": "2025-01-02",
"end_date": "2026-03-24",
"n_obs": 319,
"alpha_daily": 0.0004,
"alpha_ann": 0.1008,
"alpha_t_stat": 2.1,
"alpha_p_value": 0.04,
"r_squared": 0.82,
"adj_r_squared": 0.81,
"residual_vol_ann": 0.12,
"beta_mkt": 1.05,
"beta_smb": -0.20,
"beta_hml": 0.30,
"beta_rmw": 0.05,
"beta_cma": np.nan,
"beta_mom": np.nan,
"beta_lowvol": np.nan,
"beta_recovery": np.nan,
},
{
"strategy": "CN Strategy",
"market": "cn",
"model": "proxy",
"factor_source": "proxy_only",
"proxy_only": True,
"beta_semantics": json.dumps(
{
"beta_mkt": "MKT",
"beta_smb": "SMB_PROXY",
"beta_hml": "HML_PROXY",
"beta_rmw": "RMW_PROXY",
"beta_cma": "CMA_PROXY",
"beta_mom": "MOM",
"beta_lowvol": "LOWVOL",
"beta_recovery": "RECOVERY",
}
),
"start_date": "2025-01-02",
"end_date": "2026-03-24",
"n_obs": 319,
"alpha_daily": 0.0002,
"alpha_ann": 0.0504,
"alpha_t_stat": 1.5,
"alpha_p_value": 0.12,
"r_squared": 0.72,
"adj_r_squared": 0.70,
"residual_vol_ann": 0.14,
"beta_mkt": 0.85,
"beta_smb": -0.30,
"beta_hml": 0.25,
"beta_rmw": 0.10,
"beta_cma": -0.05,
"beta_mom": 0.20,
"beta_lowvol": np.nan,
"beta_recovery": np.nan,
},
]
)
buffer = io.StringIO()
with contextlib.redirect_stdout(buffer):
print_attribution_summary(summary)
output = buffer.getvalue()
self.assertIn("Standard factor attribution", output)
self.assertIn("Proxy factor attribution", output)
self.assertIn("beta_smb_proxy", output)
self.assertIn("beta_smb ", output)
def _make_price_frame(self, dates: pd.DatetimeIndex, benchmark: str) -> pd.DataFrame:
steps = np.arange(len(dates), dtype=float)
data = {}