From 17fa75495de6cdec1fee9f106add5368f0cc41d8 Mon Sep 17 00:00:00 2001 From: Yuxuan Yan Date: Fri, 12 Jun 2026 22:58:22 +0800 Subject: [PATCH] Restore reversal tutorial wording --- ...eversal_5d_all_universe_pipeline_report.md | 84 +++++++++++++------ scripts/generate_reversal_5d_report.py | 68 +++++++++++---- 2 files changed, 112 insertions(+), 40 deletions(-) diff --git a/docs/reversal_5d_all_universe_pipeline_report.md b/docs/reversal_5d_all_universe_pipeline_report.md index 7c3aa1f..2b0ddeb 100644 --- a/docs/reversal_5d_all_universe_pipeline_report.md +++ b/docs/reversal_5d_all_universe_pipeline_report.md @@ -1,7 +1,5 @@ # Tutorial: Testing a 5-Day Reversal Alpha -Generated: 2026-06-12T18:30:56 - This document is a teaching walkthrough for someone who is new to this research framework and only lightly familiar with quant research. We will use one concrete experiment, a 5-day reversal alpha on the full downloaded Chinese @@ -9,6 +7,7 @@ A-share universe, to learn how the framework defines an alpha, stores it, tests it, turns it into a portfolio, and explains the gap between a research result and simulated trading PnL. +This generated version was refreshed at 2026-06-12T22:52:56. The important point is not the timestamp; it is the research method. ## The Research Question @@ -131,6 +130,22 @@ evaluator tests the weight formed on date `t` over the tradable interval from `open[t+1]` to `open[t+2]`; the later execution simulator is the separate layer that trades the constructed integer book at the next open. +The code lives in `pipeline/alpha/library/reversal.py`: + +```python +class ReversalAlpha(BaseAlpha): + name = "reversal" + + def __init__(self, lookback: int = 5): + self.lookback = lookback + + def signal(self, close: pd.DataFrame) -> pd.DataFrame: + return -close.pct_change(self.lookback, fill_method=None) +``` + +The alpha class only defines the raw signal. The base class then turns that +signal into weights. + ## Step 2: Turn A Signal Into Cross-Sectional Weights By default, `BaseAlpha.to_weights()` does a cross-sectional z-score each date: @@ -219,6 +234,13 @@ That last phrase, **before trading costs**, is essential. ![Research equity](assets/reversal_5d_research_equity.png) +When reading this chart, focus on the shape and relative behavior: + +- The naive z-score line shows why outlier-sensitive weighting is fragile. +- The rank full-universe line shows that robust weighting helps, but the full + universe still contains noisy and hard-to-trade names. +- The liquid rank line shows the signal-level edge before execution costs. + ## Step 5: Check That The Alpha File Is Sane Before trusting any metric, inspect the stored alpha artifact. The run checked: @@ -244,6 +266,10 @@ raw size of an abnormal stock move. ![Weight distributions](assets/reversal_5d_weight_distributions.png) +This is a good habit: when a backtest looks strange, plot the weights before +debugging the PnL. A broken or concentrated weight distribution often explains +the result. + ## Step 6: Understand The Alpha Evaluation Formula The costless alpha evaluator now asks: @@ -305,8 +331,18 @@ The continuous target portfolio matched the stored alpha almost exactly: | rank (full) | 0.0000 | 0.00e+00 | 1.000000 | 8,984,098 | 2,678,278 | | rank (liquid subset) | 0.0000 | 0.00e+00 | 1.000000 | 9,810,256 | 862,303 | +The integer book is not exact because small target positions can be rounded +away. The liquid subset has lower tracking error because it spreads the book +over fewer and more tradable names. + ![Portfolio tracking](assets/reversal_5d_portfolio_tracking.png) +When you research a new alpha, ask two separate questions: + +- Does the continuous target portfolio match the alpha? It should. +- Does the integer tradable portfolio still resemble the target? It may not, + especially for small books or very broad universes. + ## Step 8: Simulate Execution And Costs Research returns are not the same as tradable PnL. The simulator executes the @@ -335,9 +371,9 @@ The execution results explain the final research conclusion: | rank (liquid subset) | 0.8884 | 11,017,842 | 12,733,803 | -1,715,960 | 0.5715 | For the liquid rank run, simulated PnL before cost is about -11,017,842, but total -cost is about 12,733,803. That is why -the final net PnL is weak or negative. +11,017,842, but total cost is about +12,733,803. That is why the final net PnL is +weak or negative. This is not a contradiction. It is exactly what a research pipeline should show: @@ -378,25 +414,7 @@ researcher should be more precise: > The raw 5-day reversal idea has signal value in a liquid universe, but the > current daily trading rule has too much turnover for the assumed cost model. -## Step 10: Time Consumption By Phase - -| phase | rank full (s) | rank liquid (s) | -| --- | --- | --- | -| alpha compute | 94.1 | 107.8 | -| alpha eval | 93.3 | 96.9 | -| combo combine | 21.6 | 21.7 | -| portfolio build | 537.6 | 236.7 | -| portfolio eval | 95.1 | 88.3 | -| portfolio simulate | 139.6 | 139.1 | -| total | 981.3 | 690.5 | - -![Phase timings](assets/reversal_5d_phase_timings.png) - -`portfolio build` usually dominates because it iterates per signal date and -repairs a multi-thousand-name integer book under lot rules. The liquid run is -faster because it carries fewer non-zero names per date. - -## Step 11: Reproduce The Experiment +## Step 10: Reproduce The Experiment These commands reproduce the important artifacts, assuming the full daily-bar dataset already exists at `data/daily_bars/all`. @@ -471,3 +489,21 @@ The natural next experiments are: The most important habit is to keep the layers separate. A good alpha research workflow does not stop at a single performance number; it explains how the idea travels from hypothesis, to signal, to weights, to portfolio, to executable PnL. + +## Appendix: Phase Timings From This Rerun + +| phase | rank full (s) | rank liquid (s) | +| --- | --- | --- | +| alpha compute | 94.1 | 107.8 | +| alpha eval | 93.3 | 96.9 | +| combo combine | 21.6 | 21.7 | +| portfolio build | 537.6 | 236.7 | +| portfolio eval | 95.1 | 88.3 | +| portfolio simulate | 139.6 | 139.1 | +| total | 981.3 | 690.5 | + +![Phase timings](assets/reversal_5d_phase_timings.png) + +`portfolio build` usually dominates because it iterates per signal date and +repairs a multi-thousand-name integer book under lot rules. The liquid run is +faster because it carries fewer non-zero names per date. diff --git a/scripts/generate_reversal_5d_report.py b/scripts/generate_reversal_5d_report.py index ea023e0..c547113 100644 --- a/scripts/generate_reversal_5d_report.py +++ b/scripts/generate_reversal_5d_report.py @@ -600,8 +600,6 @@ def render_report(results: dict, data_summary: dict, timings: dict, return f"""# Tutorial: Testing a 5-Day Reversal Alpha -Generated: {datetime.now().isoformat(timespec="seconds")} - This document is a teaching walkthrough for someone who is new to this research framework and only lightly familiar with quant research. We will use one concrete experiment, a 5-day reversal alpha on the full downloaded Chinese @@ -609,6 +607,7 @@ A-share universe, to learn how the framework defines an alpha, stores it, tests it, turns it into a portfolio, and explains the gap between a research result and simulated trading PnL. +This generated version was refreshed at {datetime.now().isoformat(timespec="seconds")}. The important point is not the timestamp; it is the research method. ## The Research Question @@ -731,6 +730,22 @@ evaluator tests the weight formed on date `t` over the tradable interval from `open[t+1]` to `open[t+2]`; the later execution simulator is the separate layer that trades the constructed integer book at the next open. +The code lives in `pipeline/alpha/library/reversal.py`: + +```python +class ReversalAlpha(BaseAlpha): + name = "reversal" + + def __init__(self, lookback: int = 5): + self.lookback = lookback + + def signal(self, close: pd.DataFrame) -> pd.DataFrame: + return -close.pct_change(self.lookback, fill_method=None) +``` + +The alpha class only defines the raw signal. The base class then turns that +signal into weights. + ## Step 2: Turn A Signal Into Cross-Sectional Weights By default, `BaseAlpha.to_weights()` does a cross-sectional z-score each date: @@ -819,6 +834,13 @@ That last phrase, **before trading costs**, is essential. ![Research equity](assets/reversal_5d_research_equity.png) +When reading this chart, focus on the shape and relative behavior: + +- The naive z-score line shows why outlier-sensitive weighting is fragile. +- The rank full-universe line shows that robust weighting helps, but the full + universe still contains noisy and hard-to-trade names. +- The liquid rank line shows the signal-level edge before execution costs. + ## Step 5: Check That The Alpha File Is Sane Before trusting any metric, inspect the stored alpha artifact. The run checked: @@ -840,6 +862,10 @@ raw size of an abnormal stock move. ![Weight distributions](assets/reversal_5d_weight_distributions.png) +This is a good habit: when a backtest looks strange, plot the weights before +debugging the PnL. A broken or concentrated weight distribution often explains +the result. + ## Step 6: Understand The Alpha Evaluation Formula The costless alpha evaluator now asks: @@ -897,8 +923,18 @@ The continuous target portfolio matched the stored alpha almost exactly: {closeness} +The integer book is not exact because small target positions can be rounded +away. The liquid subset has lower tracking error because it spreads the book +over fewer and more tradable names. + ![Portfolio tracking](assets/reversal_5d_portfolio_tracking.png) +When you research a new alpha, ask two separate questions: + +- Does the continuous target portfolio match the alpha? It should. +- Does the integer tradable portfolio still resemble the target? It may not, + especially for small books or very broad universes. + ## Step 8: Simulate Execution And Costs Research returns are not the same as tradable PnL. The simulator executes the @@ -923,9 +959,9 @@ The execution results explain the final research conclusion: {exec_close} For the liquid rank run, simulated PnL before cost is about -{_money(results['rank_liquid']['execution']['total_pnl_before_cost']) if rliq and results['rank_liquid']['execution'].get('exists') else 'n/a'}, but total -cost is about {_money(results['rank_liquid']['execution']['total_cost']) if rliq and results['rank_liquid']['execution'].get('exists') else 'n/a'}. That is why -the final net PnL is weak or negative. +{_money(results['rank_liquid']['execution']['total_pnl_before_cost']) if rliq and results['rank_liquid']['execution'].get('exists') else 'n/a'}, but total cost is about +{_money(results['rank_liquid']['execution']['total_cost']) if rliq and results['rank_liquid']['execution'].get('exists') else 'n/a'}. That is why the final net PnL is +weak or negative. This is not a contradiction. It is exactly what a research pipeline should show: @@ -962,17 +998,7 @@ researcher should be more precise: > The raw 5-day reversal idea has signal value in a liquid universe, but the > current daily trading rule has too much turnover for the assumed cost model. -## Step 10: Time Consumption By Phase - -{timing_tbl} - -![Phase timings](assets/reversal_5d_phase_timings.png) - -`portfolio build` usually dominates because it iterates per signal date and -repairs a multi-thousand-name integer book under lot rules. The liquid run is -faster because it carries fewer non-zero names per date. - -## Step 11: Reproduce The Experiment +## Step 10: Reproduce The Experiment These commands reproduce the important artifacts, assuming the full daily-bar dataset already exists at `data/daily_bars/all`. @@ -1047,6 +1073,16 @@ The natural next experiments are: The most important habit is to keep the layers separate. A good alpha research workflow does not stop at a single performance number; it explains how the idea travels from hypothesis, to signal, to weights, to portfolio, to executable PnL. + +## Appendix: Phase Timings From This Rerun + +{timing_tbl} + +![Phase timings](assets/reversal_5d_phase_timings.png) + +`portfolio build` usually dominates because it iterates per signal date and +repairs a multi-thousand-name integer book under lot rules. The liquid run is +faster because it carries fewer non-zero names per date. """