92 lines
2.8 KiB
Python
92 lines
2.8 KiB
Python
import unittest
|
|
|
|
from quant60.factor_risk import (
|
|
FactorExposureSnapshot,
|
|
RiskModelError,
|
|
build_factor_risk_forecast,
|
|
estimate_factor_returns,
|
|
factor_stress_loss,
|
|
portfolio_factor_variance,
|
|
)
|
|
|
|
|
|
class FactorRiskTests(unittest.TestCase):
|
|
def _exposure(self):
|
|
return FactorExposureSnapshot(
|
|
as_of="2026-07-24T15:00:00+08:00",
|
|
exposures={
|
|
"600000.SH": {"market": 1.0, "style": 1.0},
|
|
"000001.SZ": {"market": 1.0, "style": 1.0},
|
|
"300750.SZ": {"market": 1.0, "style": -1.0},
|
|
},
|
|
)
|
|
|
|
def _forecast(self):
|
|
return build_factor_risk_forecast(
|
|
factor_return_history=[
|
|
{"market": 0.01, "style": 0.02},
|
|
{"market": -0.01, "style": -0.02},
|
|
{"market": 0.02, "style": 0.01},
|
|
{"market": -0.02, "style": -0.01},
|
|
],
|
|
residual_history={
|
|
"600000.SH": [0.01, -0.01, 0.005],
|
|
"000001.SZ": [0.01, -0.01, 0.005],
|
|
"300750.SZ": [0.01, -0.01, 0.005],
|
|
},
|
|
shrinkage=0.2,
|
|
)
|
|
|
|
def test_factor_return_fit_and_residuals_are_complete(self):
|
|
fit = estimate_factor_returns(
|
|
returns={
|
|
"600000.SH": 0.03,
|
|
"000001.SZ": 0.02,
|
|
"300750.SZ": -0.01,
|
|
},
|
|
exposure=self._exposure(),
|
|
)
|
|
self.assertEqual(set(fit.factor_returns), {"market", "style"})
|
|
self.assertEqual(len(fit.residuals), 3)
|
|
|
|
def test_correlated_pair_has_more_risk_than_style_diversified_pair(self):
|
|
exposure = self._exposure()
|
|
forecast = self._forecast()
|
|
correlated = portfolio_factor_variance(
|
|
portfolio_weights={
|
|
"600000.SH": 0.5,
|
|
"000001.SZ": 0.5,
|
|
},
|
|
exposure=exposure,
|
|
forecast=forecast,
|
|
)
|
|
diversified = portfolio_factor_variance(
|
|
portfolio_weights={
|
|
"600000.SH": 0.5,
|
|
"300750.SZ": 0.5,
|
|
},
|
|
exposure=exposure,
|
|
forecast=forecast,
|
|
)
|
|
self.assertGreater(correlated, diversified)
|
|
|
|
def test_missing_risk_input_fails_closed(self):
|
|
with self.assertRaisesRegex(RiskModelError, "missing"):
|
|
portfolio_factor_variance(
|
|
portfolio_weights={"000333.SZ": 1.0},
|
|
exposure=self._exposure(),
|
|
forecast=self._forecast(),
|
|
)
|
|
|
|
def test_market_down_stress_is_positive_loss(self):
|
|
loss = factor_stress_loss(
|
|
portfolio_weights={"600000.SH": 0.5, "300750.SZ": 0.5},
|
|
exposure=self._exposure(),
|
|
factor_shocks={"market": -0.10},
|
|
)
|
|
self.assertAlmostEqual(loss, 0.10)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main()
|