"""CLI for the portfolio construction and execution-simulation phase.""" import os import click import pandas as pd from pipeline.portfolio.constraints import available_constraints, get_constraint from pipeline.portfolio.construct import construct_positions from pipeline.portfolio.research import evaluate_portfolio from pipeline.portfolio.simulator import ReferenceSimulator @click.group(name="portfolio") def portfolio(): """Construct tradable positions from weights and simulate execution.""" @portfolio.command("build") @click.option("--weights-path", required=True, help="Alpha or combo parquet (signed weights)") @click.option("--data-path", required=True, help="Data parquet file or dataset directory") @click.option("--booksize", type=float, required=True, help="Gross dollar exposure B") @click.option("--portfolio-name", required=True, help="Name for this portfolio run") @click.option("--price-field", default="close", help="Data column used as construction price") @click.option("--output-dir", default="portfolio", help="Directory to save the positions parquet") def build(weights_path, data_path, booksize, portfolio_name, price_field, output_dir): """Discretize target weights into a tradable integer position book.""" weights = pd.read_parquet(weights_path) data = pd.read_parquet(data_path) result = construct_positions( weights_df=weights, data_df=data, booksize=booksize, portfolio_name=portfolio_name, price_field=price_field, ) os.makedirs(output_dir, exist_ok=True) out_path = f"{output_dir}/{portfolio_name}.pq" result.to_parquet(out_path, index=False) click.echo(f"Saved positions: {out_path} ({len(result):,} rows)") per_date = result.groupby("date").agg( gross=("position_value", lambda s: s.abs().sum()), net=("position_value", "sum"), ) click.echo( f"Gross exposure — mean: {per_date['gross'].mean():,.0f} " f"(target {booksize:,.0f}); |net| mean: {per_date['net'].abs().mean():,.0f}" ) @portfolio.command("simulate") @click.option("--positions-path", required=True, help="Positions parquet from `portfolio build`") @click.option("--data-path", required=True, help="Data parquet file or dataset directory") @click.option("--constraint", "constraints", multiple=True, help=f"Trade constraint to apply (repeatable). Options: {available_constraints()}") @click.option("--cost-bps", type=float, default=0.0, help="Commission in basis points") @click.option("--slippage-bps", type=float, default=0.0, help="Slippage in basis points") @click.option("--volume-frac", type=float, default=0.10, help="Max traded value as a fraction of daily turnover (volume_cap)") @click.option("--output-dir", default=".", help="Base dir; writes fills/ and pnl/ subdirs") def simulate(positions_path, data_path, constraints, cost_bps, slippage_bps, volume_frac, output_dir): """Simulate next-open execution under A-share constraints, costs, slippage.""" positions = pd.read_parquet(positions_path) data = pd.read_parquet(data_path) name = positions["portfolio_name"].iloc[0] if len(positions) else "portfolio" built = [] for c in constraints: params = {"max_frac": volume_frac} if c == "volume_cap" else {} built.append(get_constraint(c, **params)) sim = ReferenceSimulator(constraints=built, cost_bps=cost_bps, slippage_bps=slippage_bps) fills, pnl = sim.run(positions, data) fills_dir = os.path.join(output_dir, "fills") pnl_dir = os.path.join(output_dir, "pnl") os.makedirs(fills_dir, exist_ok=True) os.makedirs(pnl_dir, exist_ok=True) fills.to_parquet(f"{fills_dir}/{name}.pq", index=False) pnl.to_parquet(f"{pnl_dir}/{name}.pq", index=False) click.echo(f"Saved fills: {fills_dir}/{name}.pq ({len(fills):,} rows)") click.echo(f"Saved pnl: {pnl_dir}/{name}.pq ({len(pnl):,} rows)") if len(pnl): click.echo( f"Total PnL: {pnl['pnl'].sum():,.0f} | total cost: {pnl['cost'].sum():,.0f} " f"| blocked trades: {int(fills['blocked'].sum()):,}" ) @portfolio.command("eval") @click.option("--positions-path", required=True, help="Positions parquet from `portfolio build`") @click.option("--data-path", required=True, help="Data parquet file or dataset directory") def eval_(positions_path, data_path): """Print Layer-1 research metrics (return/Sharpe/turnover/max-dd/Fitness; no IC).""" positions = pd.read_parquet(positions_path) data = pd.read_parquet(data_path) metrics = evaluate_portfolio(positions, data) click.echo("Research-portfolio metrics:") for key, value in metrics.items(): click.echo(f" {key:18s}: {value}")