Restore reversal tutorial wording
This commit is contained in:
@@ -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.
|
||||
|
||||

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

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

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

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

|
||||
|
||||
`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.
|
||||
|
||||
@@ -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.
|
||||
|
||||

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

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

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

|
||||
|
||||
`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}
|
||||
|
||||

|
||||
|
||||
`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.
|
||||
"""
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user