diff --git a/docs/reversal_5d_all_universe_pipeline_report.md b/docs/reversal_5d_all_universe_pipeline_report.md index 10b54f1..924adc1 100644 --- a/docs/reversal_5d_all_universe_pipeline_report.md +++ b/docs/reversal_5d_all_universe_pipeline_report.md @@ -1,134 +1,368 @@ -# 5-Day Reversal — End-to-End Pipeline Report +# Tutorial: Testing a 5-Day Reversal Alpha -Generated: 2026-06-11T17:17:34 +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 +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 report runs the **5-day reversal** signal end to end through the decoupled -pipeline (`data → alpha → combo → portfolio build → portfolio simulate/eval`) on -the full downloaded A-share universe, and answers the seven review questions: -alpha storage, metric sanity, NaN/look-ahead handling, alpha↔portfolio -closeness, alpha↔PnL closeness, per-phase timing, and visualizations. +The original experiment was generated on 2026-06-11. The important point is not +the timestamp; it is the research method. -Per this repo's convention an **alpha is a signed cross-sectional position -weight, not a return predictor**, so evaluation is return / Sharpe / turnover / -drawdown — there is deliberately **no IC/IR** anywhere. +## The Research Question -## TL;DR +A quant research project starts with a hypothesis: -The naive built-in `reversal` alpha (raw `-pct_change(5)` then cross-sectional -**z-score**) loses **-87.45%** in costless research on the full -~5,200-name universe. That is **not** evidence the signal is bad — it is an -artifact of z-score weighting on A-shares: a handful of newly listed / -post-suspension / limit-up names produce huge `pct_change` outliers, and -z-scoring pours the book into exactly those names (stored weights reach --52σ). +> If a stock fell a lot over the last few trading days, it may rebound soon; if +> it rose a lot, it may cool off soon. -Switching only the **weighting** to a bounded cross-sectional **rank** -(`reversal_rank`) and restricting to a per-date **liquid, non-ST, tradable** -universe recovers a genuine reversal edge: **72.24%** -costless research cumulative return at Sharpe -**0.73** with a -54.31% daily hit rate. +This is called **short-horizon reversal**. It is a simple idea: recent losers +are candidates to buy, and recent winners are candidates to sell or underweight. +In this repo, the tested version looks back 5 trading days. -The binding constraint is **cost, not signal**: at ~148×/year -turnover, a 10 bps one-way per-trade cost (5 bps commission + 5 bps slippage, -charged on each leg — so ~20 bps per round trip) erases the edge — every variant -is negative after costs. A tradable 5-day reversal needs -turnover control, not a different signal. +The central research question is: -## Headline Metrics +> Does this 5-day reversal rule create useful portfolio returns after the +> framework applies realistic storage, portfolio construction, execution +> constraints, and trading costs? -| run | weighting | research cum | research Sharpe | research turn/yr | exec before cost | exec net | exec net Sharpe | -| --- | --- | --- | --- | --- | --- | --- | --- | -| naive z-score (full) | z-score | -87.45% | -2.4515 | 160× | 18.39% | -111.94% | -1.4508 | -| rank (full) | rank | -3.48% | -0.0198 | 143× | 50.52% | -66.61% | -1.1839 | -| rank (liquid subset) | rank | 72.24% | 0.7310 | 148× | 110.18% | -17.16% | -0.2226 | +The answer from this run is nuanced: -*Research = costless, no-look-ahead weights · next-day return. Execution = next-open -fills on the discretized integer book under suspension / price-limit / volume-cap -constraints, 5 bps commission + 5 bps slippage.* +- The naive built-in version loses badly on the full universe because raw + z-score weighting is too sensitive to A-share outliers. +- A rank-weighted version on a liquid, non-ST, tradable universe has a positive + costless research result. +- The daily-traded implementation is still not tradable after costs because + turnover is too high. + +That is a normal research outcome. Good research is not just asking "did the +backtest go up?" It is asking **which layer explains the result**: signal, +weighting, universe, construction, execution, or cost. + +## How This Framework Defines An Alpha + +In many quant textbooks, an alpha is described as a **prediction** of future +returns. This framework uses a stricter and more practical convention: + +> An alpha is a signed cross-sectional position weight. + +That sentence is the key to the whole repo. + +- **Signed** means positive values are long exposure and negative values are + short exposure. +- **Cross-sectional** means the alpha compares stocks to other stocks on the + same date. +- **Position weight** means the output is already an instruction about what the + portfolio wants to own. It is not merely a score to correlate with future + returns. + +The stored alpha file always has this schema: + +| column | meaning | +| --- | --- | +| `symbol_id` | Stock identifier such as `sh600000` or `sz000001`. | +| `date` | The signal date. The alpha is formed using information known by this date's close. | +| `alpha_name` | A label for this particular run, such as `reversal_5d_all`. | +| `weight` | Signed desired exposure. Positive means long; negative means short. | + +Because the framework treats alphas as position weights, it evaluates them with +portfolio metrics: return, Sharpe, turnover, drawdown, and hit rate. It does +**not** use IC/IR, because IC/IR would treat the alpha as a return predictor. + +## The Pipeline In One Picture + +Every phase reads parquet files and writes parquet files. That makes the system +easy to inspect and rerun one layer at a time. + +```text +daily bars + -> alpha weights + -> combined weights + -> portfolio targets and integer positions + -> simulated fills and PnL + -> evaluation metrics +``` + +For this experiment, the important phases are: + +| phase | command family | what it teaches you | +| --- | --- | --- | +| Data | `cli.py data download` | What market data is available. | +| Alpha compute | `cli.py alpha compute` | How a raw research idea becomes stored weights. | +| Alpha eval | `cli.py alpha eval` | How those weights perform in a clean costless research view. | +| Combo | `cli.py combo combine` | How one or more alphas become one combined book. | +| Portfolio build | `cli.py portfolio build` | How weights become target values and integer shares. | +| Portfolio simulate | `cli.py portfolio simulate` | How the integer book trades at next open with constraints and costs. | +| Portfolio eval | `cli.py portfolio eval` | How the continuous target portfolio behaves as a research portfolio. | + +In a real research workflow, you should learn to pause after every phase and +inspect the parquet output. Most mistakes are easier to find at the interface +between two phases than at the final PnL line. + +## Step 1: Define The Raw Reversal Signal + +The built-in 5-day reversal alpha is implemented as: + +```python +signal = -close.pct_change(5, fill_method=None) +``` + +For stock `i` on date `t`, this is approximately: + +```text +signal[i, t] = -(close[i, t] / close[i, t-5] - 1) +``` + +So: + +- If a stock rose by 10% over the last 5 trading days, the raw signal is `-10%`. + It becomes a candidate short or underweight. +- If a stock fell by 10% over the last 5 trading days, the raw signal is `+10%`. + It becomes a candidate long or overweight. + +Notice the timing. The signal uses prices through date `t`. It must not use the +return from `t` to `t+1`, because that is the future. The costless alpha +evaluator tests the weight formed on date `t` against the next close-to-close +return; 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: + +```text +weight[i, t] = (signal[i, t] - mean_signal[t]) / std_signal[t] +``` + +This means the framework asks: + +> On this date, which stocks have stronger reversal scores than the rest of the +> market, and by how much? + +That is useful, but it has a weakness. If a few stocks have extreme trailing +returns because they are newly listed, suspended, illiquid, or limit-constrained, +z-scoring can put a very large amount of relative exposure into exactly those +names. + +That is what happened in the naive full-universe run. Stored weights reached +about `-52` standard deviations. The research result collapsed: + +| run | weighting | research cumulative return | research Sharpe | research turnover/year | +| --- | --- | --- | --- | --- | +| naive z-score, full universe | z-score | -87.45% | -2.4515 | 160x | + +The lesson is not "reversal is bad." The lesson is: + +> The same raw signal can become a bad portfolio if the weighting method reacts +> badly to outliers. + +## Step 3: Make The Weighting More Robust + +The repo also has a rank-weighted version, `reversal_rank`. It uses the same raw +5-day reversal signal, but converts the cross-section to ranks instead of +z-scores: + +```python +ranks = signal.rank(axis=1) +weights = ranks.subtract(ranks.mean(axis=1), axis=0) +``` + +Rank weighting keeps the ordering of stocks but removes the importance of the +exact outlier magnitude. A stock can be "the worst recent loser" or "the best +recent winner," but it cannot become 52 standard deviations important just +because its raw percentage move is unusual. + +The full-universe rank version was much less pathological, but still not a +clean signal: + +| run | weighting | research cumulative return | research Sharpe | research turnover/year | +| --- | --- | --- | --- | --- | +| rank, full universe | rank | -3.48% | -0.0198 | 143x | + +That tells us the weighting fix helped, but the universe still contains many +names that are poor candidates for a daily reversal strategy. + +## Step 4: Define The Investable Universe + +An alpha should be tested on stocks that could plausibly be traded. The liquid +run applies a per-date mask before weights are created. A stock must be: + +- seasoned, with at least 60 observed closes; +- currently tradable, using `tradestatus == 1`; +- not ST, using `isST == 0`; +- inside the top 1000 names by trailing 20-day average traded amount. + +This mask is applied to the signal, not to the price history used to compute the +5-day return. That distinction matters. We still compute `pct_change(5)` on the +full contiguous price history, then decide which names are eligible to hold on +each signal date. + +The liquid rank result is the cleanest research result: + +| run | weighting | universe | research cumulative return | research Sharpe | hit rate | +| --- | --- | --- | --- | --- | --- | +| rank, liquid subset | rank | top 1000 liquid, tradable, non-ST | 72.24% | 0.7310 | 54.31% | + +This is the first point where a researcher can say: + +> There appears to be a real 5-day reversal effect in a cleaner A-share +> universe, before trading costs. + +That last phrase, **before trading costs**, is essential. ![Research equity](assets/reversal_5d_research_equity.png) -## 1. Are Alpha Values Properly Stored? +When reading this chart, focus on the shape and relative behavior: -All alpha artifacts conform to `ALPHA_COLUMNS` (`symbol_id, date, alpha_name, -weight`), carry no null / non-finite weights, no duplicate `(symbol_id, date)` -keys, and have numerically-zero daily cross-sectional means (weights are -demeaned per date). +- The naive z-score line shows the outlier problem. +- The rank full-universe line shows that robust weighting helps but does not + fully solve the universe problem. +- The liquid rank line shows the signal-level edge before execution costs. -| run | schema ok | null w | non-finite w | dup keys | max |daily mean| | weight range | combo identity Δ | +## Step 5: Check That The Alpha File Is Sane + +Before trusting any metric, inspect the stored alpha artifact. The run checked: + +- The columns match `ALPHA_COLUMNS`. +- There are no null weights. +- There are no non-finite weights. +- There are no duplicate `(symbol_id, date)` rows. +- The daily cross-sectional mean is approximately zero. +- A one-alpha combo is an exact identity transform. + +| run | schema ok | null weights | non-finite weights | duplicate keys | max abs daily mean | weight range | combo identity diff | | --- | --- | --- | --- | --- | --- | --- | --- | | naive z-score (full) | True | 0 | 0 | 0 | 3.32e-16 | [-52.2, 19.2] | 0.00e+00 | | rank (full) | True | 0 | 0 | 0 | 0.00e+00 | [-2603.0, 2603.0] | 0.00e+00 | | rank (liquid subset) | True | 0 | 0 | 0 | 0.00e+00 | [-498.5, 498.5] | 0.00e+00 | -The decisive storage signal is the **weight range**. The naive z-score alpha -stores weights as extreme as -`[-52, 19]` — -single names tens of sigma from the cross-section. Rank weighting is bounded by -construction, so its stored weights are well-behaved. Same signal, completely -different book. +The rank ranges look numerically large because rank weights scale with the +number of names. That is fine: later evaluation divides by gross exposure, and +portfolio construction normalizes by `sum(abs(weight))`. The important +difference is that rank weights are bounded by cross-sectional rank, not by the +raw size of an abnormal stock move. ![Weight distributions](assets/reversal_5d_weight_distributions.png) -## 2. Do The Alpha Metrics Make Sense? +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. -Yes, and they tell a coherent story: +## Step 6: Understand The Alpha Evaluation Formula -- The **z-score full** run is dominated by a few outlier names; its research - Sharpe of -2.45 reflects a - book that is effectively long/short a tiny set of extreme movers, which in - A-shares keep trending — so the reversal bet loses. -- **Rank full** (-3.48%) is roughly flat: - the direction is right (hit rate - 51.18%) but - the long tail of illiquid / ST / freshly listed names adds noise. -- **Rank liquid** is the clean result: a positive, monotone reversal premium - (72.24%, Sharpe - 0.73) once the - investable universe is sane. +The costless alpha evaluator asks: -This matches the prior literature that short-horizon reversal is a real but -liquidity- and cost-sensitive A-share effect. +> If we held the alpha weights from date `t`, what close-to-close return would +> we earn from `t` to `t+1`? -## 3. NaN And Look-Ahead Handling +This is intentionally a **research-layer approximation**, not the trading +simulator. At this stage the framework has only an alpha weight file. It has not +yet rounded shares, checked limits, clipped trades, or paid costs. The purpose +is to answer a clean signal question: "Do these close-formed weights line up +with the next day's returns?" -- The raw signal uses `close.pct_change(5, fill_method=None)` — missing prices - are **not** forward-filled, so a suspended name does not silently inherit a - stale price. -- Weights are formed at close `t` and earn the **next** close-to-close return - `t → t+1`. Forward returns are computed on the full market calendar *before* - selecting signal dates, so a sparse signal grid still earns the next - *available* return rather than the next signal date. The final signal date, - which has no forward return, is dropped from metrics (that is why the - research day count is one less than the stored signal-date count). -- The liquid-universe mask is applied to the **signal**, not to the price - history: `pct_change(5)` is always computed on contiguous prices, and the mask - only decides what is *held*. It uses `tradestatus`, `isST`, a ≥60-session - seasoning rule, and a trailing-20-day liquidity rank — all backward-looking. +The actual trading layer comes later. `portfolio simulate` takes the integer +`position_shares` from the portfolio builder, executes the target from signal +date `t` at `open[t+1]`, then marks PnL as overnight movement on the old book +plus intraday movement on the newly filled book, minus trading cost. -## 4. How Close Are Alpha And Constructed Portfolio? +The daily research return is: -`portfolio build` normalizes the alpha to `target_weight = w / Σ|w|` and scales -by booksize. The continuous target portfolio is an exact normalization of the -stored alpha (research return correlation ≈ 1.0); the **integer** book then -diverges because small per-name targets are rounded away under A-share lot -rules. +```text +R[t] = sum_i(weight[i, t] * return[i, t+1]) / sum_i(abs(weight[i, t])) +``` -| run | target_value identity max|Δ| | alpha→target max|Δ| | research corr(alpha,portfolio) | mean integer gross | mean L1 tracking | +This has three important consequences: + +- The alpha is normalized by its gross exposure, so the scale of raw weights + does not by itself create a higher return. +- The next day's return is used, so the test avoids look-ahead. +- The last signal date is dropped from performance metrics because there is no + next return for it. + +Turnover is also measured from the weights: + +```text +turnover[t] = sum_i(abs(weight[i, t] - weight[i, t-1])) / sum_i(abs(weight[i, t-1])) +``` + +The annualized turnover numbers around 143x to 160x are a warning. Even a +positive signal can be hard to monetize if it asks the portfolio to trade too +much every day. + +## Step 7: Build A Portfolio From The Alpha + +The alpha file is still an abstract research book. `portfolio build` turns it +into target exposures and integer shares. + +The main normalization is: + +```text +target_weight[i, t] = weight[i, t] / sum_i(abs(weight[i, t])) +target_value[i, t] = booksize * target_weight[i, t] +target_shares[i, t] = target_value[i, t] / construction_price[i, t] +``` + +Then the framework creates an integer A-share book using lot rules and repair +logic. This is where a research portfolio starts to become a tradable portfolio. + +The continuous target portfolio matched the stored alpha almost exactly: + +| run | target value identity max abs diff | alpha to target max abs diff | research correlation alpha vs portfolio | mean integer gross | mean L1 tracking | | --- | --- | --- | --- | --- | --- | | naive z-score (full) | 0.0000 | 0.00e+00 | 1.000000 | 9,138,331 | 2,542,655 | | 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) -## 5. How Close Are Alpha Metrics And Final PnL? +When you research a new alpha, ask two separate questions: -The costless research metric and the simulated net PnL diverge for two -mechanical reasons, both quantified below: (a) **execution friction** — next-open -fills, integer shares, and constraints; and (b) **cost** — the dominant term -here. +- 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 +integer `position_shares` at the next available open and applies constraints: + +- suspension; +- price limit; +- volume cap; +- proportional trading cost. + +The cost model is: + +```text +cost = abs(traded_shares * open) * (cost_bps + slippage_bps) / 10000 +``` + +For this run, cost is 5 bps commission plus 5 bps slippage. Slippage is treated +as cash cost, not as an additional execution price adjustment. + +The execution results explain the final research conclusion: | run | corr(alpha, exec net) | PnL before cost | total cost | net PnL | mean daily turnover | | --- | --- | --- | --- | --- | --- | @@ -136,59 +370,118 @@ here. | rank (full) | 0.9613 | 5,052,067 | 11,713,451 | -6,661,383 | 0.5133 | | rank (liquid subset) | 0.9762 | 11,017,842 | 12,733,803 | -1,715,960 | 0.5715 | -The research↔execution-net daily-return correlation stays high (the book *does* -track the signal), but the level collapses after cost. For the liquid run, gross -costless edge is real yet total cost -(**12,733,803**) -swamps it. This is the central finding: 5-day reversal is a signal you must trade -*slowly* to monetize. +The liquid rank run made about 11.0 million before cost, but paid about 12.7 +million in cost. That is why the final net PnL is negative. + +This is not a contradiction. It is exactly what a research pipeline should show: + +> The signal exists in the costless layer, but the daily implementation trades +> too much to keep the edge. ![Execution vs research](assets/reversal_5d_exec_vs_research.png) -## 6. Time Consumption By Phase +## Step 9: Read The Headline Metrics Like A Researcher -| phase | rank full (s) | rank liquid (s) | -| --- | --- | --- | -| alpha compute | 108.3 | 116.1 | -| alpha eval | 98.0 | 118.7 | -| combo combine | 22.9 | 22.5 | -| portfolio build | 599.8 | 254.3 | -| portfolio eval | 94.2 | 90.0 | -| portfolio simulate | 168.7 | 163.3 | -| total | 1091.8 | 764.9 | +The complete summary is: -![Phase timings](assets/reversal_5d_phase_timings.png) +| run | weighting | research cumulative return | research Sharpe | research turnover/year | exec before cost | exec net | exec net Sharpe | +| --- | --- | --- | --- | --- | --- | --- | --- | +| naive z-score (full) | z-score | -87.45% | -2.4515 | 160x | 18.39% | -111.94% | -1.4508 | +| rank (full) | rank | -3.48% | -0.0198 | 143x | 50.52% | -66.61% | -1.1839 | +| rank (liquid subset) | rank | 72.24% | 0.7310 | 148x | 110.18% | -17.16% | -0.2226 | -`portfolio build` dominates because it iterates per signal date and repairs a -multi-thousand-name integer book under lot rules. The liquid run is faster -across the board because it carries far fewer non-zero names per date. +Here is the interpretation: -## 7. Reproduce The Run +- **Naive z-score full universe**: not a useful test of the reversal idea, + because the weighting scheme lets outliers dominate the book. +- **Rank full universe**: a better test of the same idea, but still noisy + because the universe includes too many problematic names. +- **Rank liquid subset**: the best signal-level test; it finds a positive + costless reversal effect. +- **Execution net**: all variants lose after cost at daily rebalance frequency, + so the implementation is not yet tradable. + +A beginner might look only at the final net PnL and say "the alpha failed." A +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. + +That distinction tells you what to try next. + +## Step 10: Reproduce The Experiment + +These commands reproduce the important artifacts, assuming the full daily-bar +dataset already exists at `data/daily_bars/all`. ```bash -# naive z-score baseline (full universe) — the built-in alpha, unchanged +# Naive z-score baseline: built-in reversal alpha, full universe. uv run python cli.py alpha compute --data-path data/daily_bars/all \ - --alpha-name reversal_5d_all --alpha-type reversal --lookback 5 --output-dir alphas + --alpha-name reversal_5d_all --alpha-type reversal --lookback 5 \ + --output-dir alphas -# robust rank weighting, full + liquid universe (one script, both runs) +# Rank-weighted full and liquid runs. bash scripts/run_reversal_rank_e2e.sh -# regenerate this report + figures +# Regenerate figures, diagnostics, and the older auto-generated report. +# This command rewrites this markdown file, so run it only when you want +# generated output to replace the tutorial. uv run python scripts/generate_reversal_5d_report.py ``` -## Interpretation & Next Steps +If you are learning the framework, do not run the whole pipeline blindly. Run +one phase, inspect the output parquet, then continue. -The pipeline is internally consistent end to end: storage validates, the trivial -one-alpha combo is an exact identity, the continuous target portfolio matches the -alpha, and the execution layer cleanly explains the gap to net PnL via friction -and cost. The premise that 5-day reversal "produces not-bad PnL" holds **at the -signal level** once weighting and universe are sane (rank + liquid), but **fails -net of cost** at daily rebalance frequency. +## How To Research Your Own Alpha -Recommended next diagnostics: +Use this checklist for a new idea. -- **Turnover control** — the highest-leverage lever: hold bands / no-trade zones, - weight smoothing, or longer rebalance spacing to cut the ~150×/yr turnover. -- Volatility-scaled or decayed reversal to reduce churn. -- Sweep the liquidity cutoff and lookback to map the cost/edge frontier. +1. State the hypothesis in plain language. + Example: "Stocks with poor 5-day returns may rebound over the next day." + +2. Write the raw signal. + Implement `signal(close) -> wide DataFrame` in an alpha class. Higher values + should mean stronger long preference. + +3. Choose the weighting method. + The default z-score is useful, but it can be fragile. Consider rank weights, + caps, neutralization, or liquidity-aware filters if outliers dominate. + +4. Define the investable universe before trusting results. + Make sure the strategy is not depending on suspended, ST, newly listed, or + illiquid names. + +5. Evaluate the alpha as a portfolio, not as a prediction. + Check cumulative return, Sharpe, drawdown, hit rate, and turnover. Do not add + IC/IR unless the framework's alpha convention changes. + +6. Build the portfolio and inspect tracking. + Confirm that target weights match the alpha, then check whether integer + shares still track the target book. + +7. Simulate execution with costs. + The final research question is not only "is there a signal?" It is "is there + enough signal left after realistic trading?" + +8. Diagnose the failure layer. + If results are bad, identify whether the problem is the raw signal, weighting, + universe, construction, execution constraints, turnover, or cost. + +For this 5-day reversal study, the diagnosis is clear: **the signal-level result +is promising only after robust weighting and a liquid universe filter, but the +current implementation needs turnover control before it can be considered +tradable.** + +## Next Research Directions + +The natural next experiments are: + +- Add turnover control: no-trade bands, slower rebalancing, or weight smoothing. +- Sweep the lookback window: compare 3-day, 5-day, 10-day, and 20-day reversal. +- Sweep liquidity filters: top 500, top 1000, top 1500 by traded amount. +- Add position caps so no single name can dominate after normalization. +- Compare rank weighting with volatility-scaled reversal. + +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. diff --git a/scripts/generate_reversal_5d_report.py b/scripts/generate_reversal_5d_report.py index 619ceae..3fad624 100644 --- a/scripts/generate_reversal_5d_report.py +++ b/scripts/generate_reversal_5d_report.py @@ -527,7 +527,7 @@ def render_report(results: dict, data_summary: dict, timings: dict, ]) storage = _md_table( ["run", "schema ok", "null w", "non-finite w", "dup keys", - "max |daily mean|", "weight range", "combo identity Δ"], + "max \\|daily mean\\|", "weight range", "combo identity Δ"], storage_rows, ) @@ -547,7 +547,7 @@ def render_report(results: dict, data_summary: dict, timings: dict, _money(p["l1_tracking_mean"]), ]) closeness = _md_table( - ["run", "target_value identity max|Δ|", "alpha→target max|Δ|", + ["run", "target_value identity max\\|Δ\\|", "alpha→target max\\|Δ\\|", "research corr(alpha,portfolio)", "mean integer gross", "mean L1 tracking"], close_rows, ) @@ -647,8 +647,8 @@ constraints, 5 bps commission + 5 bps slippage.* ## 1. Are Alpha Values Properly Stored? -All alpha artifacts conform to `ALPHA_COLUMNS` (`symbol_id, date, alpha_name, -weight`), carry no null / non-finite weights, no duplicate `(symbol_id, date)` +All alpha artifacts conform to `ALPHA_COLUMNS` (`symbol_id`, `date`, `alpha_name`, +`weight`), carry no null / non-finite weights, no duplicate `(symbol_id, date)` keys, and have numerically-zero daily cross-sectional means (weights are demeaned per date).