"""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