Evaluate weights against next-period returns to avoid look-ahead

Weights formed from close[t] now earn the t→t+1 return: forward returns
are computed on the full market calendar before selecting signal dates,
so a sparse signal grid earns the next available return rather than the
next signal date, and the final signal date (no forward return) is
dropped. Signal pct_change uses fill_method=None so suspended names do
not inherit stale prices.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
Yuxuan Yan
2026-06-11 17:39:55 +08:00
parent 534b91aaa4
commit 0a6f367fbf
7 changed files with 150 additions and 22 deletions
+79 -2
View File
@@ -51,7 +51,7 @@ def test_reversal_sign_matches_negative_trailing_return():
data = _make_data()
alpha = compute_alpha(data, "rev5", "reversal", lookback=5)
close = data.pivot_table(index="date", columns="symbol_id", values="close").sort_index()
raw = -close.pct_change(5)
raw = -close.pct_change(5, fill_method=None)
last = raw.index[-1]
expected_top = raw.loc[last].idxmax()
got = alpha[alpha["date"] == last].set_index("symbol_id")["weight"].idxmax()
@@ -74,6 +74,83 @@ def test_evaluate_alpha_keys():
assert key in metrics
def test_evaluate_alpha_uses_next_period_returns():
dates = pd.date_range("2024-01-01", periods=4)
data = pd.concat([
pd.DataFrame({
"symbol_id": "sh600000",
"symbol_name": "sh600000",
"date": dates,
"open": [100.0, 200.0, 200.0, 200.0],
"high": [100.0, 200.0, 200.0, 200.0],
"low": [100.0, 200.0, 200.0, 200.0],
"close": [100.0, 200.0, 200.0, 200.0],
"volume": 1_000.0,
"amount": 1_000.0,
}),
pd.DataFrame({
"symbol_id": "sz000001",
"symbol_name": "sz000001",
"date": dates,
"open": [100.0, 100.0, 200.0, 200.0],
"high": [100.0, 100.0, 200.0, 200.0],
"low": [100.0, 100.0, 200.0, 200.0],
"close": [100.0, 100.0, 200.0, 200.0],
"volume": 1_000.0,
"amount": 1_000.0,
}),
], ignore_index=True)
alpha = pd.DataFrame({
"symbol_id": ["sh600000", "sz000001", "sh600000", "sz000001"],
"date": [dates[1], dates[1], dates[2], dates[2]],
"alpha_name": ["toy"] * 4,
"weight": [-1.0, 1.0, 1.0, -1.0],
})
metrics = evaluate_alpha(alpha, data)
assert metrics["n_dates"] == 2
assert np.isclose(metrics["cumulative_return"], 0.5)
def test_evaluate_alpha_excludes_signal_without_forward_return():
dates = pd.date_range("2024-01-01", periods=3)
data = pd.concat([
pd.DataFrame({
"symbol_id": "sh600000",
"symbol_name": "sh600000",
"date": dates,
"open": [100.0, 100.0, 200.0],
"high": [100.0, 100.0, 200.0],
"low": [100.0, 100.0, 200.0],
"close": [100.0, 100.0, 200.0],
"volume": 1_000.0,
"amount": 1_000.0,
}),
pd.DataFrame({
"symbol_id": "sz000001",
"symbol_name": "sz000001",
"date": dates,
"open": [100.0, 100.0, 100.0],
"high": [100.0, 100.0, 100.0],
"low": [100.0, 100.0, 100.0],
"close": [100.0, 100.0, 100.0],
"volume": 1_000.0,
"amount": 1_000.0,
}),
], ignore_index=True)
alpha = pd.DataFrame({
"symbol_id": ["sh600000", "sz000001", "sh600000", "sz000001"],
"date": [dates[1], dates[1], dates[2], dates[2]],
"alpha_name": ["toy"] * 4,
"weight": [1.0, -1.0, -1.0, 1.0],
})
metrics = evaluate_alpha(alpha, data)
assert metrics["n_dates"] == 1
def test_equal_weight_is_mean_of_alphas():
data = _make_data()
a = compute_alpha(data, "rev", "reversal", lookback=5)
@@ -163,7 +240,7 @@ def test_load_external_alpha_module(tmp_path):
self.span = span
def signal(self, close: pd.DataFrame) -> pd.DataFrame:
return -close.pct_change(self.span)
return -close.pct_change(self.span, fill_method=None)
'''))
load_alpha_module(str(module_path))
+25 -1
View File
@@ -65,7 +65,7 @@ def _make_data(n_days: int = 40, symbols=_SYMBOLS, start="2024-01-01",
def _make_weights(data: pd.DataFrame, name="combo") -> pd.DataFrame:
"""Demeaned per-date signed weights so the cross-section is dollar-neutral."""
close = data.pivot_table(index="date", columns="symbol_id", values="close").sort_index()
raw = -close.pct_change(5)
raw = -close.pct_change(5, fill_method=None)
demeaned = raw.sub(raw.mean(axis=1), axis=0)
long = demeaned.reset_index().melt(id_vars="date", var_name="symbol_id",
value_name="weight").dropna()
@@ -535,3 +535,27 @@ def test_evaluate_portfolio_keys_no_ic():
assert key in metrics
assert "ic" not in metrics
assert "rank_ic" not in metrics
def test_evaluate_portfolio_excludes_signal_without_forward_return():
dates = pd.date_range("2024-01-01", periods=3)
data = pd.DataFrame([
{"symbol_id": sym, "date": d, "close": price}
for d, prices in zip(dates, [(100.0, 100.0), (100.0, 100.0), (200.0, 100.0)])
for sym, price in zip(("sh600000", "sz000001"), prices)
])
positions = pd.DataFrame({
"symbol_id": ["sh600000", "sz000001", "sh600000", "sz000001"],
"date": [dates[1], dates[1], dates[2], dates[2]],
"portfolio_name": ["run1"] * 4,
"target_weight": [0.5, -0.5, -0.5, 0.5],
"target_value": [500.0, -500.0, -500.0, 500.0],
"target_shares": [5.0, -5.0, -2.5, 5.0],
"position_shares": [5, -5, -2, 5],
"position_value": [500.0, -500.0, -400.0, 500.0],
"price": [100.0, 100.0, 200.0, 100.0],
})
metrics = evaluate_portfolio(positions, data)
assert metrics["n_dates"] == 1