Use next-open returns for research eval
This commit is contained in:
@@ -6,9 +6,10 @@ trading constraints. Metrics are return / Sharpe / turnover / max-drawdown /
|
||||
convention that an alpha is a position weight, not a return predictor.
|
||||
|
||||
Return convention (documented): the target weight formed from information at
|
||||
date ``t`` earns the *next* period's close-to-close return, i.e. weights are
|
||||
shifted one day relative to realized returns, so there is no look-ahead:
|
||||
``R_t = sum_i w_{i,t} · r_{i,t+1}`` normalized by gross exposure.
|
||||
date ``t`` is assumed tradable at ``open[t+1]`` and held until ``open[t+2]``.
|
||||
This is a costless approximation of the next-open execution path: no lots,
|
||||
constraints, or costs, but no credit for an overnight gap that the new signal
|
||||
could not have owned.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
@@ -26,27 +27,27 @@ def evaluate_portfolio(positions_df: pd.DataFrame, data_df: pd.DataFrame) -> dic
|
||||
Args:
|
||||
positions_df: POSITION_COLUMNS (uses ``target_weight``; zero-gross
|
||||
construction carry dates remain flat in this research view).
|
||||
data_df: DATA_COLUMNS (uses ``close`` for returns).
|
||||
data_df: DATA_COLUMNS (uses ``open`` for returns).
|
||||
|
||||
Returns:
|
||||
Dict with ``cumulative_return, sharpe_annual, turnover_annual,
|
||||
max_drawdown, fitness, hit_rate, n_dates``. No IC key.
|
||||
"""
|
||||
close = data_df.pivot_table(
|
||||
index="date", columns="symbol_id", values="close", aggfunc="first"
|
||||
open_ = data_df.pivot_table(
|
||||
index="date", columns="symbol_id", values="open", aggfunc="first"
|
||||
).sort_index()
|
||||
returns = close.pct_change(fill_method=None)
|
||||
fwd = open_.shift(-2).divide(open_.shift(-1)) - 1.0
|
||||
|
||||
weights = positions_df.pivot_table(
|
||||
index="date", columns="symbol_id", values="target_weight", aggfunc="first"
|
||||
).sort_index()
|
||||
|
||||
common = weights.index.intersection(returns.index)
|
||||
common = weights.index.intersection(open_.index)
|
||||
weights = weights.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)
|
||||
# signal dates. This preserves the next available open-to-open holding
|
||||
# interval when the signal grid is sparser than the data grid.
|
||||
fwd = fwd.reindex(common)
|
||||
|
||||
empty = {
|
||||
"cumulative_return": 0.0, "sharpe_annual": 0.0, "turnover_annual": 0.0,
|
||||
@@ -58,7 +59,7 @@ def evaluate_portfolio(positions_df: pd.DataFrame, data_df: pd.DataFrame) -> dic
|
||||
return empty
|
||||
|
||||
gross = weights.abs().sum(axis=1)
|
||||
# Weights at t earn the return from t to t+1.
|
||||
# Weights at t earn the costless tradable interval open[t+1] -> open[t+2].
|
||||
daily = (
|
||||
(weights * fwd).sum(axis=1, min_count=1)
|
||||
/ gross.replace(0.0, np.nan)
|
||||
|
||||
Reference in New Issue
Block a user