Align JoinQuant targets to execution dates

This commit is contained in:
Yuxuan Yan
2026-07-04 17:50:55 +08:00
parent f25db279bf
commit c200508f9e
5 changed files with 107 additions and 11 deletions
+53 -6
View File
@@ -51,16 +51,41 @@ def _filter_dates(
df: pd.DataFrame,
start_date: str | None,
end_date: str | None,
*,
date_column: str = "date",
) -> pd.DataFrame:
out = df.copy()
out["date"] = pd.to_datetime(out["date"]).dt.normalize()
out[date_column] = pd.to_datetime(out[date_column]).dt.normalize()
if start_date:
out = out[out["date"] >= pd.Timestamp(start_date).normalize()]
out = out[out[date_column] >= pd.Timestamp(start_date).normalize()]
if end_date:
out = out[out["date"] <= pd.Timestamp(end_date).normalize()]
out = out[out[date_column] <= pd.Timestamp(end_date).normalize()]
return out
def _read_execution_calendar(path: str | Path) -> pd.DatetimeIndex:
data = pd.read_parquet(path)
if "date" not in data.columns:
raise ValueError("execution calendar parquet must contain a 'date' column")
dates = pd.to_datetime(data["date"], errors="coerce").dropna().dt.normalize()
return pd.DatetimeIndex(sorted(dates.unique()))
def _apply_execution_calendar(df: pd.DataFrame, calendar_path: str | Path) -> pd.DataFrame:
calendar = _read_execution_calendar(calendar_path)
if calendar.empty:
raise ValueError("execution calendar contains no dates")
source_dates = pd.to_datetime(df["date"]).dt.normalize()
positions = calendar.searchsorted(source_dates, side="right")
out = df.copy()
out["source_date"] = source_dates
out["export_date"] = pd.NaT
valid = positions < len(calendar)
out.loc[valid, "export_date"] = calendar.take(positions[valid])
return out[out["export_date"].notna()].copy()
def build_target_frame(
positions: pd.DataFrame,
*,
@@ -69,6 +94,7 @@ def build_target_frame(
start_date: str | None = None,
end_date: str | None = None,
snapshot_ids: dict[str, str] | None = None,
execution_calendar_path: str | Path | None = None,
) -> pd.DataFrame:
"""Build normalized JoinQuant target rows from portfolio positions.
@@ -79,7 +105,15 @@ def build_target_frame(
raise ValueError("mode must be 'target_shares' or 'target_value'")
_check_position_columns(positions)
df = _filter_dates(positions, start_date, end_date)
df = positions.copy()
df["date"] = pd.to_datetime(df["date"]).dt.normalize()
if execution_calendar_path is not None:
df = _apply_execution_calendar(df, execution_calendar_path)
df = df.drop(columns=["date"]).rename(columns={"export_date": "date"})
df["source_date"] = pd.to_datetime(df["source_date"]).dt.strftime("%Y-%m-%d")
df = _filter_dates(df, start_date, end_date)
else:
df = _filter_dates(df, start_date, end_date)
if portfolio_name is not None:
df = df[df["portfolio_name"].astype(str) == portfolio_name]
if df.empty:
@@ -113,6 +147,7 @@ def export_targets(
out_dir: str | Path = "plugins_output/joinquant/targets",
start_date: str | None = None,
end_date: str | None = None,
execution_calendar_path: str | Path | None = None,
force: bool = False,
) -> list[dict[str, object]]:
"""Export one daily CSV/parquet target file plus a snapshot JSON per date.
@@ -126,6 +161,9 @@ def export_targets(
sibling ``snapshots`` directory.
start_date: Optional inclusive start date.
end_date: Optional inclusive end date.
execution_calendar_path: Optional daily-bar parquet dataset used to
shift each position date to the next available execution session.
This matches the internal simulator's next-open convention.
force: If false, existing target or snapshot files are treated as
frozen and cause ``FileExistsError``.
@@ -141,7 +179,15 @@ def export_targets(
snapshots_portfolio_dir.mkdir(parents=True, exist_ok=True)
positions = pd.read_parquet(positions_path)
filtered = _filter_dates(positions, start_date, end_date)
filtered = positions.copy()
filtered["date"] = pd.to_datetime(filtered["date"]).dt.normalize()
if execution_calendar_path is not None:
filtered = _apply_execution_calendar(filtered, execution_calendar_path)
filtered = filtered.drop(columns=["date"]).rename(columns={"export_date": "date"})
filtered["source_date"] = pd.to_datetime(filtered["source_date"]).dt.strftime("%Y-%m-%d")
filtered = _filter_dates(filtered, start_date, end_date)
else:
filtered = _filter_dates(filtered, start_date, end_date)
filtered = filtered[filtered["portfolio_name"].astype(str) == portfolio_name]
if filtered.empty:
return []
@@ -156,6 +202,7 @@ def export_targets(
portfolio_name=portfolio_name,
mode=mode,
snapshot_ids=snapshot_ids,
execution_calendar_path=None,
)
snapshots: list[dict[str, object]] = []
@@ -184,6 +231,7 @@ def export_targets(
"date": date_text,
"export_mode": mode,
"source_positions_path": str(positions_path),
"execution_calendar_path": str(execution_calendar_path) if execution_calendar_path else None,
"created_at": datetime.now(timezone.utc).isoformat(),
"n_symbols": int(len(daily)),
"file_sha256": file_hash,
@@ -198,4 +246,3 @@ def export_targets(
snapshots.append(snapshot)
return snapshots