Files
2026-07-04 17:55:19 +08:00

192 lines
6.5 KiB
Python

"""End-to-end local smoke preparation for JoinQuant comparison."""
from __future__ import annotations
import json
from datetime import datetime, timezone
from pathlib import Path
import pandas as pd
from pipeline.common.schema import POSITION_COLUMNS
from pipeline.data.downloader import download_universe
from pipeline.portfolio.constraints import get_constraint
from pipeline.portfolio.simulator import ReferenceSimulator
from plugins.joinquant.export_targets import export_targets
from plugins.joinquant.wrapper_strategy import write_wrapper_strategy
def build_fixed_share_positions(
data: pd.DataFrame,
*,
trade_symbol: str,
portfolio_name: str,
shares: int,
booksize: float,
max_signal_dates: int | None = None,
) -> pd.DataFrame:
"""Create a deterministic long-only fixed-share position book.
The final available data date is excluded because the internal simulator
executes each signal date at the next available open.
"""
data = data.copy()
data["date"] = pd.to_datetime(data["date"]).dt.normalize()
symbol_data = (
data[data["symbol_id"].astype(str) == trade_symbol]
.sort_values("date")
.reset_index(drop=True)
)
if len(symbol_data) < 2:
raise ValueError(f"Need at least two daily bars for {trade_symbol}")
signal_data = symbol_data.iloc[:-1].copy()
if max_signal_dates is not None and max_signal_dates > 0:
signal_data = signal_data.tail(max_signal_dates)
if signal_data.empty:
raise ValueError("No signal dates available after excluding final data date")
rows: list[dict[str, object]] = []
for row in signal_data.itertuples(index=False):
price = float(row.close)
target_value = float(shares * price)
rows.append({
"symbol_id": trade_symbol,
"date": pd.Timestamp(row.date),
"portfolio_name": portfolio_name,
"target_weight": target_value / float(booksize),
"target_value": target_value,
"target_shares": float(shares),
"position_shares": int(shares),
"position_value": target_value,
"price": price,
})
return pd.DataFrame(rows, columns=POSITION_COLUMNS)
def prepare_smoke_test(
*,
out_dir: str | Path,
universe: str = "sh600000,sz000001,sh600519,sz002594,sz300750",
trade_symbol: str = "sh600000",
start_date: str = "2024-01-02",
end_date: str = "2024-01-12",
portfolio_name: str = "jq_smoke_one_stock_long",
shares: int = 1000,
booksize: float = 1_000_000.0,
max_signal_dates: int = 3,
cost_bps: float = 5.0,
slippage_bps: float = 5.0,
volume_frac: float = 0.02,
force: bool = False,
) -> dict[str, object]:
"""Run the local side of a tiny real-data JoinQuant smoke test."""
root = Path(out_dir)
root.mkdir(parents=True, exist_ok=True)
stats = download_universe(
universe=universe,
start_date=start_date,
end_date=end_date,
output_dir=root / "daily_bars",
max_symbols=0,
chunk_size=100,
adjust="qfq",
)
data_path = Path(stats["dataset_path"])
data = pd.read_parquet(data_path)
positions = build_fixed_share_positions(
data,
trade_symbol=trade_symbol,
portfolio_name=portfolio_name,
shares=shares,
booksize=booksize,
max_signal_dates=max_signal_dates,
)
portfolio_dir = root / "portfolio"
portfolio_dir.mkdir(parents=True, exist_ok=True)
positions_path = portfolio_dir / f"{portfolio_name}.pq"
positions.to_parquet(positions_path, index=False)
constraints = [
get_constraint("suspension"),
get_constraint("price_limit"),
get_constraint("volume_cap", max_frac=volume_frac),
]
sim = ReferenceSimulator(
constraints=constraints,
cost_bps=cost_bps,
slippage_bps=slippage_bps,
)
fills, pnl = sim.run(positions, data)
execution_dir = root / "execution"
fills_dir = execution_dir / "fills"
pnl_dir = execution_dir / "pnl"
fills_dir.mkdir(parents=True, exist_ok=True)
pnl_dir.mkdir(parents=True, exist_ok=True)
fills_path = fills_dir / f"{portfolio_name}.pq"
pnl_path = pnl_dir / f"{portfolio_name}.pq"
fills.to_parquet(fills_path, index=False)
pnl.to_parquet(pnl_path, index=False)
target_root = root / "plugins_output" / "joinquant" / "targets_aligned"
snapshots = export_targets(
positions_path=positions_path,
portfolio_name=portfolio_name,
mode="target_shares",
out_dir=target_root,
execution_calendar_path=data_path,
force=force,
)
wrapper_path = root / "plugins_output" / "joinquant" / f"wrapper_strategy_{portfolio_name}.py"
write_wrapper_strategy(
portfolio_name=portfolio_name,
mode="target_shares",
out_path=wrapper_path,
)
export_dir = root / "joinquant_exports"
export_dir.mkdir(parents=True, exist_ok=True)
manifest = {
"created_at": datetime.now(timezone.utc).isoformat(),
"portfolio_name": portfolio_name,
"universe": universe,
"trade_symbol": trade_symbol,
"start_date": start_date,
"end_date": end_date,
"shares": shares,
"booksize": booksize,
"data_path": str(data_path),
"positions_path": str(positions_path),
"fills_path": str(fills_path),
"pnl_path": str(pnl_path),
"targets_dir": str(target_root / portfolio_name),
"wrapper_path": str(wrapper_path),
"joinquant_export_dir": str(export_dir),
"expected_joinquant_csvs": {
"fills": str(export_dir / "jq_fills.csv"),
"positions": str(export_dir / "jq_positions.csv"),
"pnl": str(export_dir / "jq_pnl.csv"),
},
"target_snapshots": snapshots,
"local_summary": {
"n_data_rows": int(len(data)),
"n_position_rows": int(len(positions)),
"n_fill_rows": int(len(fills)),
"n_pnl_rows": int(len(pnl)),
"total_pnl": float(pnl["pnl"].sum()) if len(pnl) else 0.0,
"total_cost": float(pnl["cost"].sum()) if len(pnl) else 0.0,
"blocked_trades": int(fills["blocked"].sum()) if len(fills) else 0,
},
}
manifest_path = root / "joinquant_smoke_manifest.json"
manifest["manifest_path"] = str(manifest_path)
manifest_path.write_text(
json.dumps(manifest, indent=2, ensure_ascii=False) + "\n",
encoding="utf-8",
)
return manifest