Raise coverage threshold to 95% and expand test coverage
- pyproject.toml: fail_under 80 → 95 - test_alpha: +79 lines - test_cli_workflow: +226 lines - test_derived: +121 lines - test_downloader_contracts: +169 lines - test_features: +16 lines - test_minute_downloader: +81 lines - test_portfolio: +208 lines
This commit is contained in:
@@ -13,12 +13,17 @@ from pipeline.portfolio.market_rules import (
|
||||
Board,
|
||||
LimitStatus,
|
||||
MarketRule,
|
||||
_to_date,
|
||||
compute_limit_status,
|
||||
detect_board,
|
||||
)
|
||||
from pipeline.portfolio.research import evaluate_portfolio
|
||||
from pipeline.portfolio.constraints import (
|
||||
TradeConstraint,
|
||||
available_constraints,
|
||||
get_constraint,
|
||||
PriceLimitConstraint,
|
||||
register_constraint,
|
||||
SuspensionConstraint,
|
||||
VolumeCapConstraint,
|
||||
)
|
||||
@@ -82,6 +87,7 @@ def test_detect_board():
|
||||
assert detect_board("sh688981") == Board.STAR
|
||||
assert detect_board("sz300750") == Board.CHINEXT
|
||||
assert detect_board("bj830000") == Board.UNKNOWN
|
||||
assert detect_board("sh") == Board.UNKNOWN
|
||||
|
||||
|
||||
# --- MarketRule date transitions ---------------------------------------------
|
||||
@@ -120,6 +126,26 @@ def test_get_rules_vectorized():
|
||||
assert list(limit) == [0.10, 0.20, 0.20]
|
||||
|
||||
|
||||
def test_market_rule_date_coercion_unknown_board_and_st_vector_override():
|
||||
rules = MarketRule()
|
||||
|
||||
assert _to_date(dt.datetime(2024, 1, 2, 15, 0)) == dt.date(2024, 1, 2)
|
||||
assert _to_date(pd.Timestamp("2024-01-03 09:30")) == dt.date(2024, 1, 3)
|
||||
assert _to_date("2024-01-04 10:00:00") == dt.date(2024, 1, 4)
|
||||
|
||||
unknown_rule = rules.get_rule("xx999999", "2024-01-02")
|
||||
assert unknown_rule.minimum_open_size == 100
|
||||
assert unknown_rule.share_increment == 100
|
||||
assert unknown_rule.price_limit_pct == 0.10
|
||||
|
||||
_, _, _, limit = rules.get_rules_vectorized(
|
||||
np.array(["sh600000", "sh600000"], dtype=object),
|
||||
"2024-01-02",
|
||||
np.array([0, 1]),
|
||||
)
|
||||
assert limit.tolist() == [0.10, 0.05]
|
||||
|
||||
|
||||
def test_compute_limit_status():
|
||||
price = np.array([110.0, 90.0, 100.0])
|
||||
preclose = np.array([100.0, 100.0, 100.0])
|
||||
@@ -130,6 +156,20 @@ def test_compute_limit_status():
|
||||
assert status[2] == LimitStatus.NORMAL.value
|
||||
|
||||
|
||||
def test_compute_limit_status_treats_bad_preclose_as_normal():
|
||||
status = compute_limit_status(
|
||||
price=np.array([np.nan, 110.0, 90.0]),
|
||||
preclose=np.array([100.0, np.nan, 0.0]),
|
||||
limit_pct=np.array([0.10, 0.10, 0.10]),
|
||||
)
|
||||
|
||||
assert status.tolist() == [
|
||||
LimitStatus.NORMAL.value,
|
||||
LimitStatus.NORMAL.value,
|
||||
LimitStatus.NORMAL.value,
|
||||
]
|
||||
|
||||
|
||||
# --- continuous targets ------------------------------------------------------
|
||||
|
||||
def test_continuous_targets_normalization():
|
||||
@@ -295,6 +335,70 @@ def test_repair_scales_to_4000_names():
|
||||
assert abs(gross - B) <= 0.03 * B
|
||||
|
||||
|
||||
def test_repair_handles_empty_input():
|
||||
result = repair_exposure(
|
||||
np.array([], dtype=np.int64),
|
||||
np.array([], dtype=float),
|
||||
np.array([], dtype=float),
|
||||
np.array([], dtype=np.int64),
|
||||
np.array([], dtype=np.int64),
|
||||
np.array([], dtype=np.int64),
|
||||
)
|
||||
|
||||
assert result.dtype == np.int64
|
||||
assert result.tolist() == []
|
||||
|
||||
|
||||
def test_repair_respects_max_iters_and_zero_increment_noop():
|
||||
q_round = np.array([100, -100], dtype=np.int64)
|
||||
q_target = np.array([300.0, -300.0])
|
||||
price = np.array([10.0, 10.0])
|
||||
min_open = np.array([100, 100])
|
||||
prev = np.zeros(2, dtype=np.int64)
|
||||
|
||||
capped = repair_exposure(
|
||||
q_round,
|
||||
q_target,
|
||||
price,
|
||||
increment=np.array([1, 1]),
|
||||
min_open=min_open,
|
||||
prev_shares=prev,
|
||||
booksize=10_000.0,
|
||||
gross_tol=0.0,
|
||||
max_iters=0,
|
||||
)
|
||||
zero_increment = repair_exposure(
|
||||
q_round,
|
||||
q_target,
|
||||
price,
|
||||
increment=np.array([0, 0]),
|
||||
min_open=min_open,
|
||||
prev_shares=prev,
|
||||
booksize=0.0,
|
||||
net_tol=0.0,
|
||||
gross_tol=0.0,
|
||||
)
|
||||
|
||||
assert capped.tolist() == [100, -100]
|
||||
assert zero_increment.tolist() == [100, -100]
|
||||
|
||||
|
||||
def test_repair_gross_growth_obeys_net_band():
|
||||
pos = repair_exposure(
|
||||
q_round=np.array([100], dtype=np.int64),
|
||||
q_target=np.array([300.0]),
|
||||
price=np.array([10.0]),
|
||||
increment=np.array([1]),
|
||||
min_open=np.array([100]),
|
||||
prev_shares=np.array([0], dtype=np.int64),
|
||||
booksize=2_000.0,
|
||||
net_tol=0.5,
|
||||
gross_tol=0.0,
|
||||
)
|
||||
|
||||
assert pos.tolist() == [100]
|
||||
|
||||
|
||||
# --- construct_positions -----------------------------------------------------
|
||||
|
||||
def test_construct_positions_schema():
|
||||
@@ -497,8 +601,81 @@ def test_constraints_compose_repeatably_regardless_of_order():
|
||||
assert np.array_equal(first_order.cost, reversed_order.cost)
|
||||
|
||||
|
||||
def test_constraint_registry_and_default_adjust_targets():
|
||||
class _NoopConstraint(TradeConstraint):
|
||||
name = "_coverage_noop_constraint"
|
||||
|
||||
def delta_bounds(self, ctx):
|
||||
return np.zeros(1), np.ones(1)
|
||||
|
||||
registered = register_constraint(_NoopConstraint)
|
||||
|
||||
assert registered is _NoopConstraint
|
||||
assert "_coverage_noop_constraint" in available_constraints()
|
||||
assert isinstance(get_constraint("_coverage_noop_constraint"), _NoopConstraint)
|
||||
assert get_constraint("_coverage_noop_constraint").adjust_targets(object()) is None
|
||||
with np.testing.assert_raises(KeyError):
|
||||
get_constraint("_missing_constraint")
|
||||
with np.testing.assert_raises(TypeError):
|
||||
register_constraint(object) # type: ignore[arg-type]
|
||||
with np.testing.assert_raises(ValueError):
|
||||
class _NoNameConstraint(TradeConstraint):
|
||||
def delta_bounds(self, ctx):
|
||||
return np.zeros(1), np.ones(1)
|
||||
|
||||
register_constraint(_NoNameConstraint)
|
||||
with np.testing.assert_raises(ValueError):
|
||||
class _DuplicateConstraint(TradeConstraint):
|
||||
name = "_coverage_noop_constraint"
|
||||
|
||||
def delta_bounds(self, ctx):
|
||||
return np.zeros(1), np.ones(1)
|
||||
|
||||
register_constraint(_DuplicateConstraint)
|
||||
|
||||
|
||||
# --- ReferenceSimulator ------------------------------------------------------
|
||||
|
||||
def test_simulator_applies_constraint_target_adjustment():
|
||||
class _HalveTarget(TradeConstraint):
|
||||
name = "_halve_target"
|
||||
|
||||
def adjust_targets(self, ctx):
|
||||
return ctx.target_shares // 2
|
||||
|
||||
def delta_bounds(self, ctx):
|
||||
return np.full(len(ctx.target_shares), -np.inf), np.full(len(ctx.target_shares), np.inf)
|
||||
|
||||
sl = _slice(1, price=np.array([10.0]))
|
||||
ctx = TradeContext(np.array([0], np.int64), np.array([100], np.int64), sl, 1e6)
|
||||
|
||||
result = ReferenceSimulator(constraints=[_HalveTarget()]).fill(ctx)
|
||||
|
||||
assert result.traded_shares.tolist() == [50]
|
||||
assert result.realized_shares.tolist() == [50]
|
||||
|
||||
|
||||
def test_simulator_empty_positions_uses_default_booksize():
|
||||
data = pd.DataFrame({
|
||||
"symbol_id": ["sh600000"],
|
||||
"date": [pd.Timestamp("2024-01-02")],
|
||||
"open": [10.0],
|
||||
"close": [10.0],
|
||||
"preclose": [10.0],
|
||||
"amount": [1e9],
|
||||
"tradestatus": [1],
|
||||
"isST": [0],
|
||||
})
|
||||
positions = pd.DataFrame(columns=POSITION_COLUMNS)
|
||||
|
||||
fills, pnl = ReferenceSimulator().run(positions, data)
|
||||
|
||||
assert list(fills.columns) == FILL_COLUMNS
|
||||
assert list(pnl.columns) == PNL_COLUMNS
|
||||
assert fills.empty
|
||||
assert pnl.empty
|
||||
|
||||
|
||||
def test_simulator_next_open_and_blocked_buy_holds_prev():
|
||||
data = _make_data(n_days=15)
|
||||
weights = _make_weights(data)
|
||||
@@ -749,3 +926,34 @@ def test_evaluate_portfolio_excludes_signal_without_forward_return():
|
||||
metrics = evaluate_portfolio(positions, data)
|
||||
|
||||
assert metrics["n_dates"] == 1
|
||||
|
||||
|
||||
def test_evaluate_portfolio_empty_and_single_return_paths():
|
||||
empty_metrics = evaluate_portfolio(
|
||||
pd.DataFrame(columns=POSITION_COLUMNS),
|
||||
pd.DataFrame(columns=["symbol_id", "date", "open"]),
|
||||
)
|
||||
assert empty_metrics["n_dates"] == 0
|
||||
assert empty_metrics["cumulative_return"] == 0.0
|
||||
|
||||
dates = pd.date_range("2024-01-01", periods=3)
|
||||
data = pd.DataFrame([
|
||||
{"symbol_id": "sh600000", "date": d, "open": price}
|
||||
for d, price in zip(dates, [100.0, 100.0, 110.0])
|
||||
])
|
||||
positions = pd.DataFrame({
|
||||
"symbol_id": ["sh600000"],
|
||||
"date": [dates[0]],
|
||||
"portfolio_name": ["single"],
|
||||
"target_weight": [1.0],
|
||||
"target_value": [1000.0],
|
||||
"target_shares": [10.0],
|
||||
"position_shares": [10],
|
||||
"position_value": [1000.0],
|
||||
"price": [100.0],
|
||||
})
|
||||
|
||||
single_metrics = evaluate_portfolio(positions, data)
|
||||
|
||||
assert single_metrics["n_dates"] == 1
|
||||
assert single_metrics["cumulative_return"] == 0.0
|
||||
|
||||
Reference in New Issue
Block a user