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:
@@ -35,7 +35,7 @@ def evaluate_portfolio(positions_df: pd.DataFrame, data_df: pd.DataFrame) -> dic
|
||||
close = data_df.pivot_table(
|
||||
index="date", columns="symbol_id", values="close", aggfunc="first"
|
||||
).sort_index()
|
||||
returns = close.pct_change()
|
||||
returns = close.pct_change(fill_method=None)
|
||||
|
||||
weights = positions_df.pivot_table(
|
||||
index="date", columns="symbol_id", values="target_weight", aggfunc="first"
|
||||
@@ -43,22 +43,29 @@ def evaluate_portfolio(positions_df: pd.DataFrame, data_df: pd.DataFrame) -> dic
|
||||
|
||||
common = weights.index.intersection(returns.index)
|
||||
weights = weights.loc[common]
|
||||
returns = returns.loc[common]
|
||||
# Compute forward returns on the full market calendar before selecting
|
||||
# signal dates. This preserves next-period returns when the signal grid is
|
||||
# sparser than the data grid.
|
||||
fwd = returns.shift(-1).reindex(common)
|
||||
|
||||
empty = {
|
||||
"cumulative_return": 0.0, "sharpe_annual": 0.0, "turnover_annual": 0.0,
|
||||
"max_drawdown": 0.0, "fitness": 0.0, "hit_rate": 0.0,
|
||||
"n_dates": len(common),
|
||||
}
|
||||
if len(common) < 3:
|
||||
if len(common) < 1:
|
||||
empty["n_dates"] = 0
|
||||
return empty
|
||||
|
||||
gross = weights.abs().sum(axis=1)
|
||||
# Weights at t earn the return from t to t+1: shift returns back by one.
|
||||
fwd = returns.shift(-1)
|
||||
daily = (weights * fwd).sum(axis=1) / gross.replace(0.0, np.nan)
|
||||
# Weights at t earn the return from t to t+1.
|
||||
daily = (
|
||||
(weights * fwd).sum(axis=1, min_count=1)
|
||||
/ gross.replace(0.0, np.nan)
|
||||
)
|
||||
daily = daily.dropna()
|
||||
if len(daily) < 2:
|
||||
empty["n_dates"] = int(len(daily))
|
||||
return empty
|
||||
|
||||
cumulative_return = float((1.0 + daily).prod() - 1.0)
|
||||
|
||||
Reference in New Issue
Block a user