diff --git a/analysis/report.py b/analysis/report.py index 125cac2..ee1bfdf 100644 --- a/analysis/report.py +++ b/analysis/report.py @@ -122,6 +122,51 @@ def plot_ic(signal_eval: dict, output_path: str = "reports/ic.png") -> str: return output_path +def dump_signals(signals_df: pd.DataFrame, output_dir: str = "results/") -> str: + """Save the signal matrix (date x stock) as a parquet file. + + Args: + signals_df: Date-indexed DataFrame of per-stock signal values. + output_dir: Directory to write the parquet file into. + + Returns: + The path the parquet file was written to. + """ + os.makedirs(output_dir, exist_ok=True) + path = os.path.join(output_dir, "signals.parquet") + signals_df.to_parquet(path) + return path + + +def dump_daily_pnl( + results: list, output_dir: str = "results/", initial_cash: float = 1_000_000.0 +) -> str: + """Extract the daily portfolio value from a backtest run and save as parquet. + + Compounds the per-day TimeReturn analyzer into an equity curve. + + Args: + results: The list returned by ``cerebro.run()``. + output_dir: Directory to write the parquet file into. + initial_cash: Starting portfolio value for scaling the curve. + + Returns: + The path the parquet file was written to. + """ + os.makedirs(output_dir, exist_ok=True) + series = pd.Series(dtype=float) + if results: + tr = results[0].analyzers.timereturn.get_analysis() + series = pd.Series(tr).sort_index() + + equity = (1.0 + series).cumprod() * initial_cash + pnl_df = pd.DataFrame({"date": equity.index, "value": equity.values}) + + path = os.path.join(output_dir, "daily_pnl.parquet") + pnl_df.to_parquet(path) + return path + + def generate_report( results: list, signal_eval: dict, diff --git a/results/daily_pnl.parquet b/results/daily_pnl.parquet new file mode 100644 index 0000000..5f55398 Binary files /dev/null and b/results/daily_pnl.parquet differ diff --git a/results/signals.parquet b/results/signals.parquet new file mode 100644 index 0000000..ed38066 Binary files /dev/null and b/results/signals.parquet differ diff --git a/run_example.py b/run_example.py index d5ce262..3b3e3f3 100644 --- a/run_example.py +++ b/run_example.py @@ -11,7 +11,7 @@ import logging import pandas as pd -from analysis.report import generate_report +from analysis.report import dump_daily_pnl, dump_signals, generate_report from backtest.config import BacktestConfig from backtest.runner import BacktestRunner from data.downloader import download_batch @@ -36,7 +36,8 @@ def _forward_returns(data: dict[str, pd.DataFrame], horizon: int) -> pd.DataFram return pd.DataFrame(forward_returns) -def main(forward_horizon: int = 5, universe: str = "csi500", signal_name: str = "reversal_vol"): +def main(forward_horizon: int = 5, universe: str = "csi500", signal_name: str = "reversal_vol", + dump_dir: str = "results/"): universes = {"hs300": SYMBOLS, "csi500": CSI500_SYMBOLS} symbols = universes.get(universe, CSI500_SYMBOLS)[:30] @@ -97,6 +98,10 @@ def main(forward_horizon: int = 5, universe: str = "csi500", signal_name: str = results, signal_eval, output_dir="reports/", initial_cash=initial_cash ) + # 7b. Dump signals and daily PnL. + dump_signals(signals_df, dump_dir) + dump_daily_pnl(results, dump_dir, initial_cash=initial_cash) + # 8. Print summary. print("\nSIGNAL IC") print("=" * 50) @@ -122,5 +127,6 @@ if __name__ == "__main__": parser = argparse.ArgumentParser(description="Chinese equity quant backtest") parser.add_argument("--universe", default="csi500", choices=["hs300", "csi500"]) parser.add_argument("--signal", default="reversal_vol", choices=["reversal", "reversal_vol"]) + parser.add_argument("--dump-dir", default="results/") args = parser.parse_args() - main(universe=args.universe, signal_name=args.signal) + main(universe=args.universe, signal_name=args.signal, dump_dir=args.dump_dir)