feat: preserve Quant OS target-package vertical slice
This commit is contained in:
@@ -0,0 +1,228 @@
|
||||
import copy
|
||||
import unittest
|
||||
|
||||
from quant60.baseline_pipeline import (
|
||||
AlphaForecastInput,
|
||||
BaselinePipelineError,
|
||||
BaselinePipelinePolicy,
|
||||
CostForecastInput,
|
||||
FIVE_LAYER_ORDER,
|
||||
RiskForecastInput,
|
||||
SecurityStateInput,
|
||||
build_baseline_pipeline,
|
||||
verify_layer_trace,
|
||||
)
|
||||
from quant60.optimizer import PortfolioConstraints
|
||||
from quant60.universe import UniverseState
|
||||
|
||||
|
||||
SYMBOLS = (
|
||||
"600000.XSHG",
|
||||
"000001.XSHE",
|
||||
"000333.XSHE",
|
||||
)
|
||||
|
||||
|
||||
def inputs(*, first_variance=0.03, first_cost_bps=8.0):
|
||||
alpha = [
|
||||
AlphaForecastInput(
|
||||
SYMBOLS[0],
|
||||
expected_excess_return=0.030,
|
||||
model_version="ridge-frozen-fixture-v1",
|
||||
feature_version="transparent-price-v1",
|
||||
),
|
||||
AlphaForecastInput(
|
||||
SYMBOLS[1],
|
||||
expected_excess_return=0.020,
|
||||
model_version="ridge-frozen-fixture-v1",
|
||||
feature_version="transparent-price-v1",
|
||||
),
|
||||
AlphaForecastInput(
|
||||
SYMBOLS[2],
|
||||
expected_excess_return=0.010,
|
||||
model_version="ridge-frozen-fixture-v1",
|
||||
feature_version="transparent-price-v1",
|
||||
),
|
||||
]
|
||||
risk = [
|
||||
RiskForecastInput(
|
||||
SYMBOLS[0],
|
||||
first_variance,
|
||||
"diagonal-risk-fixture-v1",
|
||||
),
|
||||
RiskForecastInput(SYMBOLS[1], 0.03, "diagonal-risk-fixture-v1"),
|
||||
RiskForecastInput(SYMBOLS[2], 0.03, "diagonal-risk-fixture-v1"),
|
||||
]
|
||||
cost = [
|
||||
CostForecastInput(
|
||||
SYMBOLS[0],
|
||||
first_cost_bps,
|
||||
1.0,
|
||||
"dated-cost-fixture-v1",
|
||||
),
|
||||
CostForecastInput(SYMBOLS[1], 8.0, 1.0, "dated-cost-fixture-v1"),
|
||||
CostForecastInput(SYMBOLS[2], 8.0, 1.0, "dated-cost-fixture-v1"),
|
||||
]
|
||||
securities = [
|
||||
SecurityStateInput(
|
||||
SYMBOLS[0],
|
||||
UniverseState.OPENABLE,
|
||||
"bank",
|
||||
10.0,
|
||||
),
|
||||
SecurityStateInput(
|
||||
SYMBOLS[1],
|
||||
UniverseState.OPENABLE,
|
||||
"bank",
|
||||
20.0,
|
||||
),
|
||||
SecurityStateInput(
|
||||
SYMBOLS[2],
|
||||
UniverseState.OPENABLE,
|
||||
"consumer",
|
||||
50.0,
|
||||
),
|
||||
]
|
||||
return alpha, risk, cost, securities
|
||||
|
||||
|
||||
def policy():
|
||||
return BaselinePipelinePolicy(
|
||||
constraints=PortfolioConstraints(
|
||||
gross_target=0.80,
|
||||
single_name_cap=0.80,
|
||||
industry_cap=0.80,
|
||||
one_way_turnover_cap=0.80,
|
||||
risk_aversion=1.0,
|
||||
cost_aversion=1.0,
|
||||
turnover_aversion=0.0,
|
||||
),
|
||||
cash_buffer=0.10,
|
||||
)
|
||||
|
||||
|
||||
class BaselinePipelineTests(unittest.TestCase):
|
||||
def test_canonical_path_emits_exact_five_layer_hash_chain(self):
|
||||
alpha, risk, cost, securities = inputs()
|
||||
result = build_baseline_pipeline(
|
||||
alpha=alpha,
|
||||
risk=risk,
|
||||
cost=cost,
|
||||
securities=securities,
|
||||
equity=1_000_000,
|
||||
policy=policy(),
|
||||
)
|
||||
self.assertEqual(
|
||||
tuple(event["layer"] for event in result.layer_trace),
|
||||
FIVE_LAYER_ORDER,
|
||||
)
|
||||
self.assertTrue(result.risk_decision.approved)
|
||||
self.assertTrue(result.order_deltas)
|
||||
verify_layer_trace(result.layer_trace, result.layer_payloads)
|
||||
|
||||
def test_risk_and_cost_are_causal_inputs_to_portfolio(self):
|
||||
alpha, risk, cost, securities = inputs()
|
||||
base = build_baseline_pipeline(
|
||||
alpha=alpha,
|
||||
risk=risk,
|
||||
cost=cost,
|
||||
securities=securities,
|
||||
equity=1_000_000,
|
||||
policy=policy(),
|
||||
)
|
||||
alpha, risk, cost, securities = inputs(first_variance=0.30)
|
||||
high_risk = build_baseline_pipeline(
|
||||
alpha=alpha,
|
||||
risk=risk,
|
||||
cost=cost,
|
||||
securities=securities,
|
||||
equity=1_000_000,
|
||||
policy=policy(),
|
||||
)
|
||||
alpha, risk, cost, securities = inputs(first_cost_bps=250.0)
|
||||
high_cost = build_baseline_pipeline(
|
||||
alpha=alpha,
|
||||
risk=risk,
|
||||
cost=cost,
|
||||
securities=securities,
|
||||
equity=1_000_000,
|
||||
policy=policy(),
|
||||
)
|
||||
symbol = SYMBOLS[0]
|
||||
self.assertLess(
|
||||
high_risk.solution.weights[symbol],
|
||||
base.solution.weights[symbol],
|
||||
)
|
||||
self.assertLess(
|
||||
high_cost.solution.weights[symbol],
|
||||
base.solution.weights[symbol],
|
||||
)
|
||||
self.assertNotEqual(
|
||||
base.layer_trace[2]["event_sha256"],
|
||||
high_risk.layer_trace[2]["event_sha256"],
|
||||
)
|
||||
self.assertNotEqual(
|
||||
base.layer_trace[2]["event_sha256"],
|
||||
high_cost.layer_trace[2]["event_sha256"],
|
||||
)
|
||||
|
||||
def test_missing_layer_input_fails_closed(self):
|
||||
alpha, risk, cost, securities = inputs()
|
||||
with self.assertRaisesRegex(
|
||||
BaselinePipelineError,
|
||||
"symbol sets must match exactly",
|
||||
):
|
||||
build_baseline_pipeline(
|
||||
alpha=alpha,
|
||||
risk=risk[:-1],
|
||||
cost=cost,
|
||||
securities=securities,
|
||||
equity=1_000_000,
|
||||
policy=policy(),
|
||||
)
|
||||
|
||||
def test_smoke_model_cannot_claim_baseline_candidate(self):
|
||||
alpha, risk, cost, securities = inputs()
|
||||
alpha = [
|
||||
AlphaForecastInput(
|
||||
item.symbol,
|
||||
item.expected_excess_return,
|
||||
"portable-momentum-v1",
|
||||
item.feature_version,
|
||||
)
|
||||
for item in alpha
|
||||
]
|
||||
with self.assertRaisesRegex(
|
||||
BaselinePipelineError,
|
||||
"platform smoke strategy",
|
||||
):
|
||||
build_baseline_pipeline(
|
||||
alpha=alpha,
|
||||
risk=risk,
|
||||
cost=cost,
|
||||
securities=securities,
|
||||
equity=1_000_000,
|
||||
policy=policy(),
|
||||
)
|
||||
|
||||
def test_layer_trace_tampering_is_rejected(self):
|
||||
alpha, risk, cost, securities = inputs()
|
||||
result = build_baseline_pipeline(
|
||||
alpha=alpha,
|
||||
risk=risk,
|
||||
cost=cost,
|
||||
securities=securities,
|
||||
equity=1_000_000,
|
||||
policy=policy(),
|
||||
)
|
||||
trace = copy.deepcopy(list(result.layer_trace))
|
||||
trace[2]["output_sha256"] = "0" * 64
|
||||
with self.assertRaisesRegex(
|
||||
BaselinePipelineError,
|
||||
"output hash mismatch",
|
||||
):
|
||||
verify_layer_trace(trace, result.layer_payloads)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
Reference in New Issue
Block a user