<?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: Pavel Kazantsev 💎</title>
    <description>The latest articles on DEV Community by Pavel Kazantsev 💎 (@pavel_kkkkazantsev).</description>
    <link>https://dev.to/pavel_kkkkazantsev</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%2F4134503%2Ffa19808d-55bb-4682-9e96-cb3962636da8.PNG</url>
      <title>DEV Community: Pavel Kazantsev 💎</title>
      <link>https://dev.to/pavel_kkkkazantsev</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/pavel_kkkkazantsev"/>
    <language>en</language>
    <item>
      <title>I Built an AI That Checks Whether Digital Products Deliver What They Promise</title>
      <dc:creator>Pavel Kazantsev 💎</dc:creator>
      <pubDate>Tue, 13 Oct 2026 20:17:51 +0000</pubDate>
      <link>https://dev.to/pavel_kkkkazantsev/i-built-an-ai-that-checks-whether-digital-products-deliver-what-they-promise-4nf2</link>
      <guid>https://dev.to/pavel_kkkkazantsev/i-built-an-ai-that-checks-whether-digital-products-deliver-what-they-promise-4nf2</guid>
      <description>&lt;p&gt;&lt;em&gt;This is a submission for the &lt;a href="https://dev.to/challenges/hacktoberfest-week2-2026-10-12"&gt;Hacktoberfest Open-Source AI Challenge Week 2: Side Hustle&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

&lt;h1&gt;
  
  
  I Built an AI That Checks Whether Digital Products Deliver What They Promise
&lt;/h1&gt;

&lt;p&gt;A digital-product listing can promise “48 pages”, “A4 and US Letter editions”, and “debt repayment worksheets.” Then the customer opens the ZIP and finds something else.&lt;/p&gt;

&lt;p&gt;For a small seller, that turns into refunds, support messages, rough reviews, and the awful feeling that you missed something obvious. PromiseDiff is a local-first pre-publish check for Etsy, Gumroad, template, and educational-PDF sellers.&lt;/p&gt;

&lt;p&gt;Paste in the listing. Upload the actual PDF or ZIP. PromiseDiff shows what the files can back up, what they contradict, and what they simply cannot prove.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The rule I cared about most:&lt;/strong&gt; missing text is not a failure. A measured mismatch is a contradiction; a claim the files cannot prove is just unverified.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Flpe6rlvasqozstimn3g8.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Flpe6rlvasqozstimn3g8.png" alt="PromiseDiff checks listing promises against delivered files" width="800" height="420"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What I built
&lt;/h2&gt;

&lt;p&gt;Every claim gets one of four plain outcomes:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Result&lt;/th&gt;
&lt;th&gt;Meaning&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;SUPPORTED&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;The supplied evidence supports this exact promise.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;CONTRADICTED&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;A reproducible check established a conflicting fact.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;UNVERIFIED&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;The files do not establish the claim. Missing evidence is not a contradiction.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;OUT_OF_SCOPE&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;The claim needs a capability the MVP cannot test.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Every result keeps the method and the evidence that led to it.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fuianwxb6jpbm4owiwpkg.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fuianwxb6jpbm4owiwpkg.png" alt="Four verdicts distinguish evidence from uncertainty" width="799" height="373"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;For example, a page-count claim is deterministic:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Listing: “The main PDF contains exactly 48 pages.”
File:    budget_planner_A4.pdf → 48 pages
Result:  SUPPORTED — DETERMINISTIC
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;“Includes debt repayment worksheets” is different. A PDF might call that page a “Loan Balance Reduction Tracker.” Here Gemma looks at the extracted text, but PromiseDiff only accepts the result after checking the suggested filename, page, and quote against the actual PDF.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fzyoyehxy75kli9qewz5k.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fzyoyehxy75kli9qewz5k.png" alt="The verification flow combines direct measurement with semantic evidence" width="799" height="413"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Demo
&lt;/h2&gt;

&lt;p&gt;The demo uses a real 48-page A4 Budget Planner PDF. Nothing is mocked.&lt;/p&gt;

&lt;p&gt;I used &lt;a href="https://github.com/icesurf666/progress-cut" rel="noopener noreferrer"&gt;ProgressCut&lt;/a&gt; to cut the screen recording down to a short walkthrough.&lt;/p&gt;

&lt;p&gt;The listing promises:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;The main PDF contains exactly 48 pages.
Includes both A4 and US Letter PDF editions.
Contains monthly budget review worksheets.
Includes savings goal trackers.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The upload only has the A4 edition. PromiseDiff confirms the page count, flags the missing US Letter version, and lets the seller open the evidence or export the report as Markdown.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F044a4a0yk9l655phxv23.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F044a4a0yk9l655phxv23.gif" alt="A short PromiseDiff walkthrough, recorded and cut with ProgressCut" width="640" height="360"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  How I built it
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Why a hybrid system instead of “ask the model”?
&lt;/h3&gt;

&lt;p&gt;Some questions have a simple answer; others need interpretation.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Question&lt;/th&gt;
&lt;th&gt;Correct tool&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;How many PDF pages exist?&lt;/td&gt;
&lt;td&gt;PyMuPDF metadata&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Is this page actually A4?&lt;/td&gt;
&lt;td&gt;PDF geometry with a 3-point tolerance&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Does a listed file exist?&lt;/td&gt;
&lt;td&gt;Complete file inventory&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Does “Loan Balance Reduction Tracker” plausibly satisfy a debt-repayment claim?&lt;/td&gt;
&lt;td&gt;Gemma plus validated quotation&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Asking a language model to count pages is slower and worse than reading the PDF metadata. But keyword search alone will not reliably connect “debt repayment” and “loan balance reduction.” So each tool only gets the jobs it can support with evidence.&lt;/p&gt;

&lt;p&gt;The rule is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;missing evidence != contradiction
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fm80696fodwn5xo4zbpfg.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fm80696fodwn5xo4zbpfg.png" alt="A model citation must pass the evidence gate before it can support a claim" width="799" height="373"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;If the document is scanned, the claim scope is ambiguous, a citation points to a nonexistent page, or a quote does not occur in extracted text, the answer is &lt;code&gt;UNVERIFIED&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Architecture and safety
&lt;/h3&gt;

&lt;p&gt;It is a small React + FastAPI app. The verification-flow diagram above shows how the pieces connect.&lt;/p&gt;

&lt;p&gt;Uploads are untrusted, so the backend puts tight boundaries around them: size, PDF count, total pages, extracted text, ZIP ratio, file types, and expanded ZIP size. It rejects traversal paths, duplicate entries, encrypted PDFs, nested archives, and corrupt files. The original upload is never saved.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fkzr0pk0535i18umh5dbw.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fkzr0pk0535i18umh5dbw.png" alt="The upload boundary rejects unsafe archives before analysis" width="799" height="373"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The model work is deliberately kept small:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Gemma extracts structured promises from the listing.&lt;/li&gt;
&lt;li&gt;Python validates source quotations and normalizes measurable claims.&lt;/li&gt;
&lt;li&gt;Local retrieval selects compact, potentially relevant PDF excerpts.&lt;/li&gt;
&lt;li&gt;Gemma evaluates semantic correspondence in batch.&lt;/li&gt;
&lt;li&gt;Evidence validation checks filename, page, and exact quote before serialization.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Why does open innovation matter?
&lt;/h2&gt;

&lt;p&gt;Many sellers are working on unpublished material: course notes, templates, client downloads, and draft listings. With the local setup, Ollama keeps the listing text and extracted PDF text on the seller’s machine.&lt;/p&gt;

&lt;p&gt;Open-weight Gemma also makes this affordable for a small operation. A local run has no per-audit third-party API bill, although someone still pays for the hardware or hosting. Sellers can repeat the same check before every release without handing their draft material to another service.&lt;/p&gt;

&lt;h2&gt;
  
  
  Reproducible benchmark, including a failure
&lt;/h2&gt;

&lt;p&gt;I made ten independently labelled synthetic PDF cases and ran them through the same local API the UI uses. The runner saves the raw responses and timings in &lt;code&gt;benchmark/results.json&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;exact-label accuracy = correct labels / total cases = 9 / 10 = 90%
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fawlzkit8rrktrvyvx51b.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fawlzkit8rrktrvyvx51b.png" alt="Nine benchmark cases matched their expected labels; one paraphrase stayed unverified" width="799" height="413"&gt;&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;

&lt;pre data-lang="mermaid"&gt;&lt;code&gt;xychart-beta
  title "Exact classification by category (10 synthetic cases)"
  x-axis [Supported, Contradicted, Unverified, OutOfScope]
  y-axis "Correct cases" 0 --&amp;gt; 4
  bar [3, 2, 3, 1]&lt;/code&gt;&lt;/pre&gt;



&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Metric&lt;/th&gt;
&lt;th&gt;Result&lt;/th&gt;
&lt;th&gt;Caveat&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Exact-label accuracy&lt;/td&gt;
&lt;td&gt;90% (9/10)&lt;/td&gt;
&lt;td&gt;Tiny synthetic evaluation, not a generalization claim.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Contradiction precision&lt;/td&gt;
&lt;td&gt;100% (2/2)&lt;/td&gt;
&lt;td&gt;Both measured failures were page/dimension facts.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Contradiction recall&lt;/td&gt;
&lt;td&gt;100% (2/2)&lt;/td&gt;
&lt;td&gt;Same small set.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Supported recall&lt;/td&gt;
&lt;td&gt;75% (3/4)&lt;/td&gt;
&lt;td&gt;One semantic paraphrase was conservatively left unverified.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Median API latency&lt;/td&gt;
&lt;td&gt;2.11 s&lt;/td&gt;
&lt;td&gt;Local Gemma on this machine.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Mean API latency&lt;/td&gt;
&lt;td&gt;3.50 s&lt;/td&gt;
&lt;td&gt;Prompt-injection case took 13.81 s.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The miss is the useful bit. The listing said &lt;strong&gt;debt repayment worksheet&lt;/strong&gt; and the PDF said &lt;strong&gt;Loan Balance Reduction Tracker&lt;/strong&gt;. The model did not have strong enough evidence to connect them, so PromiseDiff returned &lt;code&gt;UNVERIFIED&lt;/code&gt; instead of pretending it knew. That is a limitation to fix, not a result to hide.&lt;/p&gt;

&lt;h2&gt;
  
  
  Known limitations
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;The benchmark is small and synthetic. It is not a claim about every kind of seller PDF.&lt;/li&gt;
&lt;li&gt;Scanned documents without extractable text can still receive metadata checks, but semantic claims remain unverified.&lt;/li&gt;
&lt;li&gt;The current retrieval can miss paraphrases. In the benchmark, “debt repayment worksheet” and “Loan Balance Reduction Tracker” stayed unverified rather than becoming a false positive.&lt;/li&gt;
&lt;li&gt;Public deployments should isolate PDF processing with strict resource limits.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What I would build next
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Better paraphrase retrieval before semantic evaluation.&lt;/li&gt;
&lt;li&gt;Seller feedback on which promise types produce the most costly mistakes.&lt;/li&gt;
&lt;li&gt;Batch release audits and version-to-version diffs.&lt;/li&gt;
&lt;li&gt;Isolated PDF processing for public deployments.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Code
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://github.com/icesurf666/PromiseDiff" rel="noopener noreferrer"&gt;PromiseDiff source code&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The repository has the local setup, real example PDF, test commands, security notes, and benchmark runner.&lt;/p&gt;

&lt;h2&gt;
  
  
  Prize categories
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Best Use of Gemma&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>devchallenge</category>
      <category>hf26challenge</category>
      <category>gemma</category>
      <category>opensource</category>
    </item>
    <item>
      <title>I Tested 58 Trading Hypotheses. 48 Failed. Here’s the Framework That Killed Them.</title>
      <dc:creator>Pavel Kazantsev 💎</dc:creator>
      <pubDate>Fri, 09 Oct 2026 14:53:19 +0000</pubDate>
      <link>https://dev.to/pavel_kkkkazantsev/i-tested-58-trading-hypotheses-48-failed-heres-the-framework-that-killed-them-1poe</link>
      <guid>https://dev.to/pavel_kkkkazantsev/i-tested-58-trading-hypotheses-48-failed-heres-the-framework-that-killed-them-1poe</guid>
      <description>&lt;p&gt;58 hypotheses went in. 48 were rejected, 9 remained promising, and 1 could not be evaluated because the required data was unavailable.&lt;/p&gt;

&lt;p&gt;None of the 9 earned the label "edge." They earned the right to be tested further.&lt;/p&gt;

&lt;p&gt;Some ideas should die before a backtest is ever run. The rest should face the cheapest falsification test first. A hypothesis that cannot answer "who pays us?" and "why does this persist?" is not a hypothesis. It is a guess with a Python file attached.&lt;/p&gt;

&lt;p&gt;The kill pipeline that processed all 58, in the order most ideas die:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;hypothesis idea
    ↓
cost filter        (does it survive fees and turnover at all?)
    ↓
robustness check   (is the result a plateau or a lucky parameter?)
    ↓
null benchmark     (does it beat a structure-preserving random version?)
    ↓
untouched holdout  (does it hold on a year it never saw?)
    ↓
contamination audit (is the result real or did the code lie to me?)
    ↓
survived research → forward / shadow validation → capital
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each gate catches a different type of false alpha. A strategy can clear the first four and die on the fifth. Passing all five is not a green light. It is permission to start collecting forward data.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fpo85kaonmubsvqtv7i28.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fpo85kaonmubsvqtv7i28.png" alt="Full kill pipeline — from hypothesis through five gates to locked capital" width="800" height="620"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Gate 1: costs
&lt;/h2&gt;

&lt;p&gt;The most common way a backtest looks good: skip costs. Cross-sectional reversal on 15-minute Bybit bars had a gross Sharpe of 7 to 13, positive in 80%+ of out-of-sample windows. The gross effect was clearly present before costs. At taker fees, rebalancing every bar, trading costs ran 5 to 10 times the size of the raw edge. After costs, zero out of 76 out-of-sample windows remained positive.&lt;/p&gt;

&lt;p&gt;I tried entry/exit thresholds, hysteresis, coarser timeframes, slower rebalancing. No plateau anywhere near break-even. Adjacent parameter settings swung from +45% to −60%.&lt;/p&gt;

&lt;p&gt;Run the cost test before you believe the signal, not after.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fsw2pd6n8hzhpydq0g2bw.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fsw2pd6n8hzhpydq0g2bw.png" alt="Before costs: Sharpe 7–13, 80%+ OOS positive. After costs: 0/76 windows positive" width="800" height="380"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Gate 2: robustness
&lt;/h2&gt;

&lt;p&gt;Daily momentum had one configuration that returned +97% net at taker fees. The lookback sweep: l=7 gave +33%, l=10 gave −23%, l=14 gave +97%, l=21 gave +35%. No plateau. The +97% was a lucky draw from a jagged surface.&lt;/p&gt;

&lt;p&gt;Half the return came from the 2021 bull alone. 2022 was negative. A strategy that only makes money in bull markets and loses in bear is more consistent with market beta than neutral alpha.&lt;/p&gt;

&lt;p&gt;One good result on a swept parameter is not evidence. The whole surface needs to hold up.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fqywzn80nl2eada0p8fiq.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fqywzn80nl2eada0p8fiq.png" alt="Momentum lookback sweep: l=7 +33%, l=10 -23%, l=14 +97%, l=21 +35% — no plateau" width="800" height="440"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Gate 3: null benchmark
&lt;/h2&gt;

&lt;p&gt;Funding carry looked strong in development: +48.6% net over 4.5 years, Sharpe 1.17. Before trusting it, I ran a null test: scramble which coin's funding the strategy reads, keep everything else identical. The scrambled version returned +28%.&lt;/p&gt;

&lt;p&gt;More than half the return was the structural funding premium: exposure to perpetual market mechanics rather than signal-driven selection. The selection component showed evidence beyond this null in development (empirical permutation p=0.024), but it was much smaller than the headline return implied.&lt;/p&gt;

&lt;p&gt;Before believing the result, ask whether a random version of the signal would also make money.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fsol5j9xz7qnu9jk21hvb.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fsol5j9xz7qnu9jk21hvb.png" alt="Funding carry +48.6% vs scrambled null +28%, selection component +20.6%, p=0.024" width="800" height="360"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Gate 4: untouched holdout
&lt;/h2&gt;

&lt;p&gt;The lockbox is a date range fixed in advance, never touched until a single opening event. No research, no tuning, no plotting inside that range until development is complete.&lt;/p&gt;

&lt;p&gt;Funding carry passed development and validation. The lockbox was a 12-month period (2025-09 to 2026-09), sealed from day one. The frozen strategy made −0.5% on it. The premium that paid from 2021 to 2024 was absent in the lockbox year. One year of absence is not a permanent verdict. It is exactly the kind of regime dependence the lockbox is designed to expose early.&lt;/p&gt;

&lt;p&gt;Once a lockbox is opened, it is burned. Any further confirmation requires a new, still-unseen date range. Development results are hypotheses, and only sealed, untouched data can confirm them.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F37fjbx5u2o4oqf7tw296.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F37fjbx5u2o4oqf7tw296.png" alt="Timeline 2021–2026: dev period +48.6%, lockbox 2025-09 to 2026-09 returned -0.5%" width="800" height="360"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Gate 5: contamination audit
&lt;/h2&gt;

&lt;p&gt;One BTC funding-trend hypothesis had a development t-stat of 2.6. The 1,248 events had 72-hour holding periods, so observations were heavily overlapping. The Newey-West corrected t-stat fell from 2.6 to 1.07, removing the apparent statistical evidence from that test. A real-looking result from a real-looking test, broken by one statistical assumption.&lt;/p&gt;

&lt;p&gt;On a separate project, a football prediction model passed closing-line value (CLV) validation with +152bps, 95% CI excluded zero. A five-check forensic audit found two contaminated features. Four of five checks failed. The metric that reported the good result was the last place to look for the error.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fz75s53wsykhdgntvam70.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fz75s53wsykhdgntvam70.png" alt="1,248 overlapping 72h events, OLS t-stat 2.6 drops to 1.07 after Newey-West NW-9 correction" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Where the 58 actually died
&lt;/h2&gt;

&lt;p&gt;Each hypothesis is assigned to the first decisive failure that stopped further evaluation. Some failed more than one check.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Kill gate&lt;/th&gt;
&lt;th&gt;Count&lt;/th&gt;
&lt;th&gt;Example&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Costs&lt;/td&gt;
&lt;td&gt;~20&lt;/td&gt;
&lt;td&gt;Price MR: gross Sharpe 7–13, net negative&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Robustness&lt;/td&gt;
&lt;td&gt;~10&lt;/td&gt;
&lt;td&gt;Daily momentum: +97% was one lucky lookback&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Null benchmark&lt;/td&gt;
&lt;td&gt;~8&lt;/td&gt;
&lt;td&gt;Funding carry null still returned +28%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Holdout / fresh OOS&lt;/td&gt;
&lt;td&gt;~7&lt;/td&gt;
&lt;td&gt;Funding carry −0.5% on sealed year; agent ensemble −35% OOS&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;No detectable signal / other rejection&lt;/td&gt;
&lt;td&gt;~3&lt;/td&gt;
&lt;td&gt;New-listing cohort: corr≈0, null p=0.94&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Total rejected&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;48&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Data unavailable&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;Required data unavailable&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Remaining PROMISING&lt;/td&gt;
&lt;td&gt;9&lt;/td&gt;
&lt;td&gt;Passed development and validation gates&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Benjamini–Hochberg (BH) significant at FDR=0.05&lt;/td&gt;
&lt;td&gt;8&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Independent mechanisms&lt;/td&gt;
&lt;td&gt;4–5&lt;/td&gt;
&lt;td&gt;roughly 4–5 distinct economic bets&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;9 survivors does not mean 9 strategies. Several share the same economic mechanism with different entry filters. 8 Benjamini–Hochberg (BH) significant hypotheses at FDR=0.05 collapse to roughly 4–5 independent bets on market structure by economic mechanism.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fog0gpwz829ps7qqpjel3.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fog0gpwz829ps7qqpjel3.png" alt="58 → 48 killed + 9 promising + 1 unavailable → 8 BH-significant → 4–5 independent mechanisms" width="800" height="470"&gt;&lt;/a&gt;&lt;/p&gt;

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

&lt;p&gt;Two cases that took longer than they should have.&lt;/p&gt;

&lt;p&gt;An LLM-generated factor-residual ensemble showed +76% in development, Sharpe 0.6, tuned over multiple iterations. On a fresh holdout it reversed to −35%. The development result held inside the data it was tuned on. The fresh holdout showed what the agent had actually learned: the development period.&lt;/p&gt;

&lt;p&gt;In a funding carry reproduction audit a month after the original publication, the holdout result changed from −0.5% to +1.3%. Same code, same data cache, fingerprints matched. I still do not have a clean explanation. I published the discrepancy table rather than quietly resolving it. I scrutinized the original negative result far less than I would have scrutinized a positive one. Negative results get a lower evidentiary standard by default. They should not.&lt;/p&gt;

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

&lt;p&gt;A handful of hypotheses passed enough gates to justify forward validation. They are currently shadow-deployed: signals recorded in real time, parameters locked, no capital.&lt;/p&gt;

&lt;p&gt;None of them earned the right to be trusted based on the backtest. The backtest is permission to collect more data, not permission to trade.&lt;/p&gt;

&lt;p&gt;The specific signals are not published here. Forward validation is ongoing and the gate has not been reached.&lt;/p&gt;

&lt;h2&gt;
  
  
  Steal this framework
&lt;/h2&gt;

&lt;p&gt;Before trusting any strategy result:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;[ ] Realistic costs included from the start, not added after
[ ] Parameter robustness: result holds over adjacent settings (plateau, not spike)
[ ] Null benchmark: structure-preserving random version loses
[ ] Untouched holdout: a date range sealed before any research on that data
[ ] Contamination audit: no look-ahead, no survivorship, no feature leakage
[ ] Forward validation: signals collected in real time, parameters locked
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Costs and robustness killed roughly half of the 58 before a holdout was needed. Run the cheap gates first.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fdw9de57fmgkll91ne17g.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fdw9de57fmgkll91ne17g.png" alt="Research kill checklist: 6 gates from costs to forward validation" width="800" height="460"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The real lesson
&lt;/h2&gt;

&lt;p&gt;A high kill rate is not evidence that the pipeline is good. But a research process that rarely kills its own ideas is one I would distrust.&lt;/p&gt;

&lt;p&gt;48 of 58 died. If the kill rate were much lower, I would not trust the process.&lt;/p&gt;




&lt;p&gt;More experiments, engineering notes and quantitative research at pkazantsev.com&lt;/p&gt;

</description>
      <category>python</category>
      <category>trading</category>
      <category>datascience</category>
      <category>machinelearning</category>
    </item>
    <item>
      <title>Green Tests, Broken Architecture: Four Models Review TypeScript Diffs</title>
      <dc:creator>Pavel Kazantsev 💎</dc:creator>
      <pubDate>Tue, 06 Oct 2026 10:20:38 +0000</pubDate>
      <link>https://dev.to/pavel_kkkkazantsev/green-tests-broken-architecture-four-models-review-typescript-diffs-16ge</link>
      <guid>https://dev.to/pavel_kkkkazantsev/green-tests-broken-architecture-four-models-review-typescript-diffs-16ge</guid>
      <description>&lt;p&gt;&lt;em&gt;This is a submission for the &lt;a href="https://dev.to/challenges/kaggle-2026-09-23"&gt;Kaggle Benchmarking Challenge&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;In ProgressCut, it is possible to write a change that works, passes a narrow test, and still violates a boundary I care about. A renderer can reach for Electron directly. A recovery record can be overwritten halfway through a write. An exported filename can escape its session folder.&lt;/p&gt;

&lt;p&gt;Those are review problems. I made a small benchmark to see how four models review short TypeScript diffs against explicit architecture rules.&lt;/p&gt;

&lt;p&gt;Three models got exactly &lt;strong&gt;0.949&lt;/strong&gt;. Each failed for a completely different reason.&lt;/p&gt;

&lt;p&gt;The public Kaggle task contains 13 compact synthetic diffs. Each has an expected APPROVE or BLOCK verdict, a violated invariant where applicable, and a requirement to cite evidence from the diff.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F3nmjetlwuru9u3wyvjct.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F3nmjetlwuru9u3wyvjct.png" alt="Architecture-aware code review benchmark" width="800" height="420"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What I Benchmarked
&lt;/h2&gt;

&lt;p&gt;This is a deliberately narrow review exercise:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Can a model reject a functionally plausible change when it violates an explicit repository invariant?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The diffs are synthetic and small. They are based on rules from ProgressCut, a local-first Electron application, but expose no private screenshots, sessions or unreleased code.&lt;/p&gt;

&lt;p&gt;The six rules under test:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;code&gt;engine-no-node&lt;/code&gt;: algorithm code cannot import Node or platform APIs.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;domain-no-runtime-deps&lt;/code&gt;: domain types have no runtime-library dependency.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;renderer-no-electron&lt;/code&gt;: UI reaches native capabilities only through a narrow bridge.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;atomic-recovery-write&lt;/code&gt;: recovery metadata is written to a temporary file, then renamed.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;exact-render-duration&lt;/code&gt;: FFmpeg output is capped at the selected story duration.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;safe-output-path&lt;/code&gt;: an export cannot escape the configured session directory.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Seven changes should be blocked and six should be approved. The approval cases sit close to the violations: a renderer may call a typed bridge, but may not import Electron; &lt;code&gt;resolve()&lt;/code&gt; plus a path-boundary check is acceptable, while &lt;code&gt;join(outputDir, userName)&lt;/code&gt; is not.&lt;/p&gt;

&lt;h2&gt;
  
  
  How the score works
&lt;/h2&gt;

&lt;p&gt;Each case is worth one point, split across three deterministic checks:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;case score = (correct verdict + correct violated rule + grounded evidence) / 3
benchmark score = mean(case scores)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For an approved change, &lt;code&gt;rule_id&lt;/code&gt; must be &lt;code&gt;none&lt;/code&gt;. A reviewer has to recognize when a change satisfies a rule, not merely spot its vocabulary.&lt;/p&gt;

&lt;p&gt;I ran the unchanged task once per model through Kaggle's benchmark runner. Every model received the same rules and diff. The scorer is deterministic; no LLM judges another LLM.&lt;/p&gt;

&lt;h2&gt;
  
  
  Models Tested
&lt;/h2&gt;

&lt;p&gt;I used a small, deliberately mixed set from four providers, including the coding-focused Qwen3 Coder. This is not a claim of market coverage; it is a way to compare different review failure modes under the same prompt and scorer.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Model&lt;/th&gt;
&lt;th&gt;Score&lt;/th&gt;
&lt;th&gt;Server runtime&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Gemini 3.7 Flash&lt;/td&gt;
&lt;td&gt;1.000&lt;/td&gt;
&lt;td&gt;10m 48s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Claude Sonnet 5&lt;/td&gt;
&lt;td&gt;0.949&lt;/td&gt;
&lt;td&gt;32.9s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;GPT-5.4 mini&lt;/td&gt;
&lt;td&gt;0.949&lt;/td&gt;
&lt;td&gt;14.2s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Qwen3 Coder 480B&lt;/td&gt;
&lt;td&gt;0.949&lt;/td&gt;
&lt;td&gt;3m 29s&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Findings
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fkr4jhuzh7veawmmzhipx.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fkr4jhuzh7veawmmzhipx.png" alt="Scorecard and distinct failure modes" width="800" height="447"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I had no baseline for whether 0.95 was good or bad, so the number alone told me little. With 13 cases, one third-point miss accounts for the whole gap. The failures were more informative than the scores.&lt;/p&gt;

&lt;p&gt;The three &lt;code&gt;0.949&lt;/code&gt; scores have the same arithmetic: twelve full points and one third-point case. They do not mean the same thing in practice.&lt;/p&gt;

&lt;h3&gt;
  
  
  Claude
&lt;/h3&gt;

&lt;p&gt;For a direct &lt;code&gt;import { shell } from "electron"&lt;/code&gt; inside the renderer, Claude returned:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"decision"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"BLOCK"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"rule_id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"placeholder"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"evidence"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"placeholder"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The verdict was right, but the response was unusable for automated enforcement. I cannot tell from this run whether the placeholders came from the model or its structured-output path. Either way, an agent pipeline needs schema validation and a retry or fallback path.&lt;/p&gt;

&lt;h3&gt;
  
  
  GPT-5.4 mini
&lt;/h3&gt;

&lt;p&gt;The safe version resolves the destination and verifies a separator-bounded prefix:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;root&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;resolve&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;outputDir&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;destination&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;resolve&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;root&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;requestedName&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;destination&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;startsWith&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;root&lt;/span&gt;&lt;span class="p"&gt;}${&lt;/span&gt;&lt;span class="nx"&gt;sep&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Invalid export path&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;writeFile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;destination&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&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;GPT-5.4 mini blocked it anyway, arguing that the guard was unsafe. The benchmark expects approval: the guard also rejects the directory itself, which is conservative but not an escape route. This is a real review-agent failure mode: treating a stricter policy as a vulnerability.&lt;/p&gt;

&lt;h3&gt;
  
  
  Qwen
&lt;/h3&gt;

&lt;p&gt;Qwen blocked a renderer function that takes a &lt;code&gt;DesktopBridge&lt;/code&gt; and calls &lt;code&gt;bridge.openFile(path)&lt;/code&gt;. Its explanation said the bridge ultimately uses Electron APIs.&lt;/p&gt;

&lt;p&gt;The bridge may eventually call Electron APIs, but the renderer does not receive Electron itself. Judging the rule from the call graph rather than the declared import boundary quietly changes its meaning.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the perfect score does not mean
&lt;/h2&gt;

&lt;p&gt;Gemini's 1.000 is a result on 13 hand-written examples. It does not establish that Gemini is the best code reviewer, will enforce arbitrary policy, or is safe to run without tests and human review.&lt;/p&gt;

&lt;p&gt;The task has obvious limits:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;one repository style and six named invariants;&lt;/li&gt;
&lt;li&gt;synthetic diffs rather than a history of production pull requests;&lt;/li&gt;
&lt;li&gt;no multi-file reasoning, tool use or test execution;&lt;/li&gt;
&lt;li&gt;one run per model, so no variance estimate;&lt;/li&gt;
&lt;li&gt;no analysis of cost or token usage.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This is a seed benchmark. Every failure can be inspected and reproduced.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I would measure next
&lt;/h2&gt;

&lt;p&gt;The next version needs held-out fixtures and harder near-misses: indirect imports, unrelated noisy edits, an atomic write with bad cleanup, and path guards that look safe but accept sibling prefixes. I would also run repeated trials and report schema compliance separately from architecture reasoning.&lt;/p&gt;

&lt;p&gt;I would not reduce an AI review to one confidence score. In a real merge gate, I want separate signals for the policy verdict, the structured response, the cited evidence and the deterministic check.&lt;/p&gt;

&lt;p&gt;If the response is malformed, its policy decision should not be accepted. When a rule has a deterministic check, that check wins. The model reviews; deterministic logic makes the final decision.&lt;/p&gt;

&lt;h2&gt;
  
  
  My Benchmark
&lt;/h2&gt;

&lt;p&gt;The public leaderboard is &lt;a href="https://www.kaggle.com/benchmarks/pavelkazantsev7776/architecture-aware-typescript-code-review/versions/1" rel="noopener noreferrer"&gt;Architecture-Aware TypeScript Code Review on Kaggle&lt;/a&gt;. The underlying &lt;a href="https://www.kaggle.com/benchmarks/tasks/pavelkazantsev7776/safe-typescript-change-review-benchmark/1" rel="noopener noreferrer"&gt;Safe TypeScript Change Review task&lt;/a&gt; and the &lt;a href="https://github.com/icesurf666/progress-cut/tree/main/experiments/kaggle-safe-typescript-patches" rel="noopener noreferrer"&gt;task source and report generator&lt;/a&gt; are public too.&lt;/p&gt;

</description>
      <category>devchallenge</category>
      <category>kagglechallenge</category>
      <category>ai</category>
      <category>machinelearning</category>
    </item>
    <item>
      <title>I Killed My Electron App Twice to Test Crash Recovery</title>
      <dc:creator>Pavel Kazantsev 💎</dc:creator>
      <pubDate>Mon, 05 Oct 2026 18:36:40 +0000</pubDate>
      <link>https://dev.to/pavel_kkkkazantsev/i-killed-my-electron-app-twice-to-test-crash-recovery-3jgk</link>
      <guid>https://dev.to/pavel_kkkkazantsev/i-killed-my-electron-app-twice-to-test-crash-recovery-3jgk</guid>
      <description>&lt;p&gt;"We have crash recovery" is an easy thing to claim. I wanted to know what happens if the process disappears right now.&lt;/p&gt;

&lt;p&gt;The question is not specific to Electron. It applies to a media exporter, downloader, migration runner or AI workflow that writes files locally. Restarting an app is easy. Recovering enough state to finish the user's work is the part worth testing.&lt;/p&gt;

&lt;p&gt;I used an Electron recorder as the example below. The mechanics are simple: persist something, kill the process, restart, find the leftovers, rebuild, then inspect the result.&lt;/p&gt;

&lt;p&gt;For ProgressCut, a local-first desktop app that turns a work session into a short visual story, "right now" has two uncomfortable meanings:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;while screenshots are being captured;&lt;/li&gt;
&lt;li&gt;while FFmpeg is assembling the final MP4.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;In both cases, raw frames can exist while the output is partial or absent.&lt;/p&gt;

&lt;p&gt;I started isolated Electron processes, created real session folders, sent &lt;code&gt;SIGKILL&lt;/code&gt;, then started fresh Electron processes and asked them to find and rebuild what remained. &lt;code&gt;SIGKILL&lt;/code&gt; ends a process immediately, so shutdown handlers cannot quietly clean up the state first.&lt;/p&gt;

&lt;p&gt;The test found a rendering bug too: a story budgeted for 3.00 seconds encoded to 3.46 seconds.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fy4t9rguwzq5iucwz2572.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fy4t9rguwzq5iucwz2572.png" alt="Crash recovery lifecycle" width="800" height="420"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What had to survive
&lt;/h2&gt;

&lt;blockquote&gt;
&lt;p&gt;If there are captured frames, a later app process must be able to offer a rebuild without recording again.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The implementation is a per-session manifest stored under the user's local application-support directory. It contains the output folder, raw-frame folder, target duration, output format and timestamps. It does not contain screenshot data.&lt;/p&gt;

&lt;p&gt;The record has to be per session. A single &lt;code&gt;active-session.json&lt;/code&gt; works until two incomplete sessions exist and the second one overwrites the first.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;session A → recovery/&amp;lt;hash(A)&amp;gt;.json
session B → recovery/&amp;lt;hash(B)&amp;gt;.json
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Every manifest is written as a temporary file and then renamed into place. A successful recovery clears only the manifest for the recovered session.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fgtslrprlohluz8362icf.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fgtslrprlohluz8362icf.png" alt="Per-session recovery lifecycle" width="799" height="433"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What I actually killed
&lt;/h2&gt;

&lt;p&gt;The smoke test uses synthetic PNGs instead of recording a real screen. Everything after that is production code: Electron startup, session orchestration, manifest storage, story selection and FFmpeg.&lt;/p&gt;

&lt;p&gt;The harness runs two scenarios:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Scenario&lt;/th&gt;
&lt;th&gt;Kill point&lt;/th&gt;
&lt;th&gt;What a restart must prove&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Capture interruption&lt;/td&gt;
&lt;td&gt;after the third PNG is persisted&lt;/td&gt;
&lt;td&gt;the three frames are discovered and remain byte-for-byte unchanged&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Encoding interruption&lt;/td&gt;
&lt;td&gt;after the real FFmpeg process starts&lt;/td&gt;
&lt;td&gt;a fresh process finds the session and can render a valid MP4&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The parent launches each Electron app in its own process group and sends &lt;code&gt;SIGKILL&lt;/code&gt; to that group. This matters: killing the Electron parent while leaving FFmpeg behind does not simulate an interruption cleanly.&lt;/p&gt;

&lt;p&gt;After each kill, a fresh Electron process reads local recovery manifests. The harness rebuilds every discovered session, runs &lt;code&gt;ffprobe&lt;/code&gt; (FFmpeg's inspection tool) on the output and verifies:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;H.264 video;&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;yuv420p&lt;/code&gt; pixel format;&lt;/li&gt;
&lt;li&gt;encoded duration within 150 ms of the requested three seconds;&lt;/li&gt;
&lt;li&gt;raw PNG bytes unchanged;&lt;/li&gt;
&lt;li&gt;recovery metadata removed only for the session that completed.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The command is part of the repository:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pnpm &lt;span class="nb"&gt;test&lt;/span&gt;:crash-recovery
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  If you are testing a different app
&lt;/h2&gt;

&lt;p&gt;Replace "frames" with whatever your app leaves behind: uploaded chunks, edited documents, downloaded files, generated reports or queued jobs.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Choose the smallest irreversible user value. In my case, it is a persisted screenshot; in your app it might be a database checkpoint or a downloaded part file.&lt;/li&gt;
&lt;li&gt;Write a recoverable record before expensive work begins. Keep enough information to resume, but avoid storing sensitive content in that record.&lt;/li&gt;
&lt;li&gt;Interrupt the real process at a deterministic checkpoint. Do not mock the thing whose behaviour you are claiming to test.&lt;/li&gt;
&lt;li&gt;Start a fresh process with the same storage. A retry in the same memory space is not recovery.&lt;/li&gt;
&lt;li&gt;Verify the final artefact, then scope cleanup. Check bytes, schema, duration, checksum or domain result, not only that a file exists.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;In pseudocode it looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;runUntil&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;checkpoint:persisted&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nf"&gt;killProcessGroup&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;restartedApp&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;launchWithSameDataDirectory&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;pending&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;restartedApp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;discoverRecoverableWork&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;restartedApp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;resume&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;pending&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;

&lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;inspect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;)).&lt;/span&gt;&lt;span class="nf"&gt;toMatchObject&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;expectedArtefact&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;sourceBytes&lt;/span&gt;&lt;span class="p"&gt;()).&lt;/span&gt;&lt;span class="nf"&gt;toEqual&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;bytesBeforeCrash&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;pendingRecords&lt;/span&gt;&lt;span class="p"&gt;()).&lt;/span&gt;&lt;span class="nf"&gt;toEqual&lt;/span&gt;&lt;span class="p"&gt;([]);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I check cleanup separately because it is easy to get wrong: stale records cause repeated recovery prompts, while broad cleanup can erase another unfinished job.&lt;/p&gt;

&lt;h2&gt;
  
  
  The failure I wasn't looking for
&lt;/h2&gt;

&lt;p&gt;The first recovery run failed on a stricter check than "the file exists."&lt;/p&gt;

&lt;p&gt;The story target was three seconds. &lt;code&gt;ffprobe&lt;/code&gt; measured 3.458333 seconds.&lt;/p&gt;

&lt;p&gt;That was annoying, because the video looked fine. It played, the codec was right, nothing seemed broken. A test that only checked for a file would have passed it, and I would have shipped the bug.&lt;/p&gt;

&lt;p&gt;The renderer uses FFmpeg's concat demuxer because story moments have variable durations. The usual workaround repeats the last file so FFmpeg honours that last still frame. The side effect was extra tail time in the encoded video.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fh8c1856sknj4wxcqpzsl.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fh8c1856sknj4wxcqpzsl.png" alt="The duration regression" width="799" height="373"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I capped the encoded output at the story's calculated duration and kept a regression test that creates weighted moments and measures the resulting file with &lt;code&gt;ffprobe&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;runFfmpeg&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;
  &lt;span class="c1"&gt;// concat input and video filter omitted&lt;/span&gt;
  &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;-movflags&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;+faststart&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;-t&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;story&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;totalDurationMs&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;1000&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;toString&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
  &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;-y&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;outputPath&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;That is the whole reason to inspect the output rather than assert that a file exists. A report can exist with missing rows; a video can play and still have the wrong length.&lt;/p&gt;

&lt;h2&gt;
  
  
  What passed
&lt;/h2&gt;

&lt;p&gt;On 2026-10-05, the smoke ran on macOS 26.3, an arm64 Mac17,2 and Node.js 22.14.0:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;capture interruption  → 3 frames discovered → recovered
encoding interruption → 3 frames discovered → recovered
source PNGs           → unchanged
output                → H.264 / yuv420p / 3.00 s
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I also tested the unsigned packaged arm64 &lt;code&gt;.app&lt;/code&gt; rather than only the development process. The app launched with a temporary profile, loaded Sharp from inside &lt;code&gt;app.asar&lt;/code&gt;, accepted a production IPC re-render request and generated a three-second MP4.&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;CSC_IDENTITY_AUTO_DISCOVERY&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nb"&gt;false &lt;/span&gt;pnpm run pack
pnpm &lt;span class="nb"&gt;test&lt;/span&gt;:packaged
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That catches a different class of failure: code that works in a workspace because it accidentally resolves a native dependency from the developer machine.&lt;/p&gt;

&lt;h2&gt;
  
  
  What this does &lt;em&gt;not&lt;/em&gt; prove
&lt;/h2&gt;

&lt;p&gt;This covers process crashes and nothing more.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;It does not test power loss or filesystem durability beyond atomic rename.&lt;/li&gt;
&lt;li&gt;It does not test a multi-hour recording, sleep/wake, monitor changes or a full disk.&lt;/li&gt;
&lt;li&gt;It does not test Screen Recording permission UX.&lt;/li&gt;
&lt;li&gt;It does not test Developer ID signing, notarization, Gatekeeper or a clean-machine install.&lt;/li&gt;
&lt;li&gt;It does not evaluate whether the story selector makes better summaries. That requires real held-out sessions and blind review.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Why I added this before release
&lt;/h2&gt;

&lt;p&gt;For a local-first tool, the useful data can already be on disk when the UI is gone. In this app, throwing away an unfinished session because FFmpeg died would be a bad default.&lt;/p&gt;

&lt;p&gt;Each run of the harness goes through the same loop:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F3y2owtv5bqgp9gtepu6a.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F3y2owtv5bqgp9gtepu6a.png" alt="Crash recovery verification loop" width="800" height="240"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Next I need a real two-to-five-hour session with resource measurements, sleep/wake and an intentional interruption. This smoke test narrows the gap; it does not replace that run.&lt;/p&gt;

&lt;p&gt;Source: &lt;a href="https://github.com/icesurf666/progress-cut" rel="noopener noreferrer"&gt;ProgressCut on GitHub&lt;/a&gt;, MIT licensed.&lt;/p&gt;

</description>
      <category>electron</category>
      <category>typescript</category>
      <category>testing</category>
      <category>opensource</category>
    </item>
    <item>
      <title>I Recorded 10,847 Frames While Coding. Only 30 Were Worth Keeping.</title>
      <dc:creator>Pavel Kazantsev 💎</dc:creator>
      <pubDate>Fri, 02 Oct 2026 19:38:56 +0000</pubDate>
      <link>https://dev.to/pavel_kkkkazantsev/from-10000-frames-to-30-moments-building-a-visual-story-compressor-h5p</link>
      <guid>https://dev.to/pavel_kkkkazantsev/from-10000-frames-to-30-moments-building-a-visual-story-compressor-h5p</guid>
      <description>&lt;p&gt;I recorded a two-hour coding session and got 10,847 frames.&lt;/p&gt;

&lt;p&gt;The final timelapse needed only 30.&lt;/p&gt;

&lt;p&gt;So I built a pipeline to figure out which 30 were actually worth keeping.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fpkazantsev.com%2Fprogresscut.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fpkazantsev.com%2Fprogresscut.gif" alt="ProgressCut compressing a coding session into a short visual story" width="600" height="338"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Source session: &lt;a href="https://www.youtube.com/watch?v=GznmPACXBlY" rel="noopener noreferrer"&gt;youtube.com/watch?v=GznmPACXBlY&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Most of those 10,847 frames were noise: the cursor blinked, the system clock ticked, while nothing meaningful in the code changed.&lt;/p&gt;

&lt;p&gt;The first step isn't clever. It's just getting rid of those.&lt;/p&gt;

&lt;p&gt;ProgressCut is a local macOS app that turns a coding session into a short visual story. No talking, no editing. You hit stop and it hands you a GIF. Here's how the algorithm actually works.&lt;/p&gt;

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

&lt;p&gt;Each stage removes a different kind of noise:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Capture → dHash dedupe → SSIM novelty → Segments → 30 moments
10,847  →    1,203     →     284      →    48     →     30
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Real numbers from a 2-hour session.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F01n9ug3l3vw7t7xmc04z.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F01n9ug3l3vw7t7xmc04z.png" alt="Five-stage pipeline: 10,847 raw frames → dHash 1,203 → SSIM 284 → segments 48 → story 30 moments" width="800" height="217"&gt;&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Stage 1 — dHash: kill the identical frames
&lt;/h2&gt;

&lt;p&gt;89% gone in one pass. At 2 fps, consecutive screenshots are often nearly identical: the cursor blinks, the clock changes, or a single character appears while the rest of the screen stays untouched.&lt;/p&gt;

&lt;p&gt;dHash resizes each frame to 9×8 pixels, converts to greyscale, then compares each pixel to the one on its right (1 if brighter, 0 otherwise). 8 rows × 8 comparisons = 64 bits, stored as a &lt;code&gt;BigInt&lt;/code&gt;. XOR two hashes and count the differing bits. Distance ≤ 8: duplicate.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fmwqxq5aym67h4oqqq4m8.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fmwqxq5aym67h4oqqq4m8.png" alt="dHash pipeline: source frame to 9x8 greyscale grid to 64-bit comparison bits, Hamming distance decision" width="799" height="281"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Kernighan's bit trick — O(set bits) not O(64)
&lt;/h3&gt;

&lt;p&gt;Counting those differing bits naively loops 64 times. Kernighan's trick loops once per &lt;em&gt;set&lt;/em&gt; bit, so for near-duplicates that's typically 2–6 iterations:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;hammingDistance&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;a&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;DHash&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;b&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;DHash&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;diff&lt;/span&gt;  &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;a&lt;/span&gt; &lt;span class="o"&gt;^&lt;/span&gt; &lt;span class="nx"&gt;b&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;      &lt;span class="c1"&gt;// XOR: 1 where bits differ&lt;/span&gt;
  &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;count&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;while &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;diff&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="nx"&gt;n&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;diff&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;=&lt;/span&gt; &lt;span class="nx"&gt;diff&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="nx"&gt;n&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;    &lt;span class="c1"&gt;// clears the lowest set bit&lt;/span&gt;
    &lt;span class="nx"&gt;count&lt;/span&gt;&lt;span class="o"&gt;++&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;count&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;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Why BigInt?&lt;/strong&gt; JS numbers are IEEE 754 doubles: they top out at 53 safe integer bits. A dHash is 64 bits, so &lt;code&gt;BigInt&lt;/code&gt; is the only way to get exact XOR. The &lt;code&gt;n&lt;/code&gt; suffix (&lt;code&gt;0n&lt;/code&gt;, &lt;code&gt;1n&lt;/code&gt;) is the literal syntax.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;strong&gt;Result: 10,847 → 1,203 frames (−89%)&lt;/strong&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Stage 2 — SSIM: kill the boring frames
&lt;/h2&gt;

&lt;p&gt;Unique isn't the same as interesting. After dHash you still have ~1,200 frames that each look slightly different, but most of that difference is one more character on a line. You need a score that reflects how much the &lt;em&gt;visual state&lt;/em&gt; actually changed, not just whether the pixels moved.&lt;/p&gt;

&lt;p&gt;SSIM (Structural Similarity Index) compares luminance, contrast, and spatial structure between consecutive frames. Score close to 1: nothing happened. Score close to 0: something worth keeping.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tex"&gt;&lt;code&gt;         (2μₓμᵧ + C₁)(2σₓᵧ + C₂)
SSIM = ─────────────────────────────────
       (μₓ² + μᵧ² + C₁)(σₓ² + σᵧ² + C₂)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F9ls8d3mztp7o07eg4ssm.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F9ls8d3mztp7o07eg4ssm.png" alt="Novelty score timeline showing spikes when significant visual changes occur, with selected moments marked as purple dots" width="800" height="217"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Full-frame SSIM would average out a big change in one corner against stagnation everywhere else. Instead the frame is tiled into &lt;strong&gt;16 regions&lt;/strong&gt;, SSIM computed per region, then averaged. A file-save that changes the status bar doesn't steal credit from a tab switch that changed everything.&lt;/p&gt;

&lt;h3&gt;
  
  
  Single-pass variance — one loop, not two
&lt;/h3&gt;

&lt;p&gt;The naive path does two loops: one for the mean, one for the variance. Both collapse into one using &lt;strong&gt;Var(X) = E[X²] − E[X]²&lt;/strong&gt;: accumulate Σx, Σx², Σy, Σy², Σxy in a single pass and derive everything after:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;sumA&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;sumB&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;sumAA&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;sumBB&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;sumAB&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;n&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;x1&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="nx"&gt;x0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;y1&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="nx"&gt;y0&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;y&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nx"&gt;y0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;y&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;y1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;y&lt;/span&gt;&lt;span class="o"&gt;++&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="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;x&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nx"&gt;x0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;x&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;x1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;x&lt;/span&gt;&lt;span class="o"&gt;++&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;pa&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;a&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;pixels&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;y&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="nx"&gt;W&lt;/span&gt;&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="nx"&gt;x&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;pb&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;b&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;pixels&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;y&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="nx"&gt;W&lt;/span&gt;&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="nx"&gt;x&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nx"&gt;sumA&lt;/span&gt;  &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="nx"&gt;pa&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;    &lt;span class="nx"&gt;sumB&lt;/span&gt;  &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="nx"&gt;pb&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nx"&gt;sumAA&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="nx"&gt;pa&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="nx"&gt;pa&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;sumBB&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="nx"&gt;pb&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="nx"&gt;pb&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;sumAB&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="nx"&gt;pa&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="nx"&gt;pb&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;muA&lt;/span&gt;  &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;sumA&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="nx"&gt;n&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  &lt;span class="nx"&gt;muB&lt;/span&gt;  &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;sumB&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="nx"&gt;n&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;varA&lt;/span&gt;  &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;sumAA&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="nx"&gt;n&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;muA&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="nx"&gt;muA&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;   &lt;span class="c1"&gt;// Var(X) = E[X²] − E[X]²&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;varB&lt;/span&gt;  &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;sumBB&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="nx"&gt;n&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;muB&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="nx"&gt;muB&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;covAB&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;sumAB&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="nx"&gt;n&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;muA&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="nx"&gt;muB&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Result: 1,203 → 284 frames (−77%)&lt;/strong&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Stage 3 — Segmentation: budget the moments fairly
&lt;/h2&gt;

&lt;p&gt;Sessions aren't uniformly active. There's 20 minutes of flow where you're actually building something, then 10 minutes reading docs, then another sprint. If you spread the 30-moment budget evenly across the timeline, the reading gaps eat slots that should go to the interesting parts.&lt;/p&gt;

&lt;p&gt;The pipeline splits the frame sequence into activity windows by novelty density. Each window's share of the budget scales with how much happened inside it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;warm-up │ ███████████ active  │ ░ idle │ ████████ active │ ████ wrap-up
 2 mom  │   12 moments        │ 1 mom  │  9 moments      │ 6 mom
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fcmiiwq24bmmi1db9og2z.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fcmiiwq24bmmi1db9og2z.png" alt="Temporal segmentation showing timeline divided into active and idle windows with budget bars below" width="800" height="141"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;A 20-minute sprint might take 8 moments; a 10-minute idle stretch gets 1.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Result: 284 frames → 48 segments → 30 moments&lt;/strong&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  What comes out
&lt;/h2&gt;

&lt;p&gt;30 moments → a GIF or MP4, under 30 seconds, no input from you. Each moment holds for a duration proportional to the activity level of its segment.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Input:  ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░  10,847 frames
Output: █  ██   █████  █  ████  █  ██   ████      30 moments
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fjajme7lkuwux7y4kk97y.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fjajme7lkuwux7y4kk97y.png" alt="Compression ratio: 10,847 grey frames in the top bar, 30 purple moments in the bottom bar" width="797" height="97"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;No frames leave the machine. The app calls no model. Run it twice on the same input and you get the same output. That last part matters more than it sounds: a non-deterministic story compressor is harder to trust and impossible to debug.&lt;/p&gt;




&lt;h2&gt;
  
  
  What's next
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Better novelty scoring with embeddings&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;SSIM can measure structural change, but it has no idea what that change means. Opening a new file and typing a few characters may affect a similar amount of screen area even though one is a much more meaningful transition.&lt;/p&gt;

&lt;p&gt;An embedding-based scorer might separate those cases better by measuring semantic change rather than raw pixel structure. That's the next hypothesis to test.&lt;/p&gt;

&lt;p&gt;The port in &lt;code&gt;packages/engine&lt;/code&gt; is already the swap point: implement a new scorer behind the same interface, run the same 187 tests, and leave the rest of the pipeline untouched.&lt;/p&gt;

&lt;p&gt;But swapping scorers without a way to evaluate the result is just guessing. The feedback loop comes first: let users remove moments they don't think belong in the final story, then use that data to compare scoring methods.&lt;/p&gt;

&lt;p&gt;SSIM stays the default until an embedding-based scorer demonstrates a measurable improvement.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Longer term: adaptive budget&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The 30-moment budget is fixed right now. A focused 20-minute session and a messy two-hour session both get 30 moments.&lt;/p&gt;

&lt;p&gt;The segmentation step already estimates how much activity happened in each window, so the next step is to let the total story length depend on the session itself.&lt;/p&gt;

&lt;p&gt;A short session with lots of meaningful changes might need fewer, faster moments. A long session with large idle stretches should compress harder instead of wasting slots on inactivity.&lt;/p&gt;

&lt;p&gt;If you were building this, would you keep SSIM or move to embeddings?&lt;/p&gt;

&lt;h2&gt;
  
  
  Code
&lt;/h2&gt;

&lt;p&gt;MIT, pnpm monorepo, 187 tests, CI on &lt;code&gt;macos-latest&lt;/code&gt;. The algorithm lives in &lt;code&gt;packages/engine/src/&lt;/code&gt; with no Electron dependency, runs in plain Node. If you want to poke at the frame selection logic without building the whole app, that's the entry point.&lt;/p&gt;

&lt;p&gt;→ &lt;a href="https://github.com/icesurf666/progress-cut" rel="noopener noreferrer"&gt;github.com/icesurf666/progress-cut&lt;/a&gt;&lt;/p&gt;

</description>
      <category>typescript</category>
      <category>opensource</category>
      <category>showdev</category>
      <category>programming</category>
    </item>
    <item>
      <title>Three Crypto Edges. What Survived the Audit?</title>
      <dc:creator>Pavel Kazantsev 💎</dc:creator>
      <pubDate>Tue, 29 Sep 2026 15:13:08 +0000</pubDate>
      <link>https://dev.to/pavel_kkkkazantsev/three-crypto-edges-three-different-killers-1i6o</link>
      <guid>https://dev.to/pavel_kkkkazantsev/three-crypto-edges-three-different-killers-1i6o</guid>
      <description>&lt;p&gt;A research platform should not make it easy to stop at a good-looking number. That applies to negative results too. Transaction-cost checks, held-out evaluation, and parameter sweeps are useful only when their results can be traced back to their runs.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Candidate&lt;/th&gt;
&lt;th&gt;Historical report&lt;/th&gt;
&lt;th&gt;Current audit status&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Price mean reversion&lt;/td&gt;
&lt;td&gt;Gross Sharpe 7–13; rejected after costs&lt;/td&gt;
&lt;td&gt;Historical revision range not independently reproduced here&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Funding carry&lt;/td&gt;
&lt;td&gt;+48.6% development; −0.5% holdout&lt;/td&gt;
&lt;td&gt;Rerun: +45.62% development; +1.31% holdout; discrepancy unresolved&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Daily momentum&lt;/td&gt;
&lt;td&gt;+97% best case; sensitive to lookback&lt;/td&gt;
&lt;td&gt;Matching run/configuration not located; sweep remains unverified&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Edge 1: Mean reversion and the cost problem
&lt;/h2&gt;

&lt;p&gt;The historical summary reports cross-sectional reversal on 15-minute bars with gross Sharpe between 7 and 13, positive in over 80% of out-of-sample windows across 76 test periods. These figures span research revisions; they are not measurements from one controlled run.&lt;/p&gt;

&lt;p&gt;That summary also reports friction at 5–10 times the available edge when rebalancing every bar, and neighboring settings ranging from +45% to −60% net. Those revision-specific ratios and parameter results have not been independently reproduced in this audit. They should not be treated as verified evidence that every implementation must fail.&lt;/p&gt;

&lt;p&gt;The reported decision was to reject the tested implementations because a robust net result had not been established. The engineering question is whether a pre-cost pattern survives a specified execution model, not whether mean reversion is universally untradeable.&lt;/p&gt;

&lt;p&gt;A &lt;a href="https://pkazantsev.com/writing/crypto-backtest-trading-costs/" rel="noopener noreferrer"&gt;separate deep-dive&lt;/a&gt; describes a controlled evaluation. Its metrics should not be mixed with this historical revision summary as if they came from one run.&lt;/p&gt;

&lt;h2&gt;
  
  
  Edge 2: Funding carry and a failed reproduction
&lt;/h2&gt;

&lt;p&gt;The tested carry construction combines long spot with short perpetual exposure and funding payments. Lower turnover reduces transaction costs; it does not eliminate them or other risks.&lt;/p&gt;

&lt;p&gt;The original report gives +48.6% net development return, Sharpe 1.17, and −0.5% net holdout return, Sharpe −0.02. On September 30, I reran the current implementation against the current local cache. Both the original evaluation script and a separate audit runner produced different results:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Metric&lt;/th&gt;
&lt;th&gt;Historical report&lt;/th&gt;
&lt;th&gt;Current rerun&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Development net cumulative return&lt;/td&gt;
&lt;td&gt;+48.6%&lt;/td&gt;
&lt;td&gt;+45.62%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Development Sharpe&lt;/td&gt;
&lt;td&gt;1.17&lt;/td&gt;
&lt;td&gt;1.079&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Holdout net cumulative return&lt;/td&gt;
&lt;td&gt;−0.5%&lt;/td&gt;
&lt;td&gt;+1.31%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Holdout Sharpe&lt;/td&gt;
&lt;td&gt;−0.02&lt;/td&gt;
&lt;td&gt;0.208&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F2ard1hfoiqgo9yxmemq8.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F2ard1hfoiqgo9yxmemq8.png" alt="Historical and current funding-carry holdout returns have different signs" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Historical report: −0.5%. Current rerun: +1.31%. These are different run versions, not a controlled experiment measuring an improvement. The cause of the discrepancy is not established.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;The current configuration uses FundingCarry(384), rebalances every 1344 15-minute bars (14 days), normalizes to fixed gross exposure, and applies modelled fees of 4 bps plus slippage of 2 bps. It selects 45 symbols and scores 76 development windows and 17 holdout windows.&lt;/p&gt;

&lt;p&gt;The 17 complete 21-day holdout windows cover September 1, 2025 through August 24, 2026 (exclusive): 357 days, not the entire configured interval ending September 1. The original saved window list was not found, so historical coverage cannot be independently certified.&lt;/p&gt;

&lt;p&gt;In the current rerun, gross holdout return is +2.14% and net return is +1.31% on the same windows. Costs reduce the result under this model. This comparison does not explain the historical −0.5% return. Development returns are positive in each represented calendar year, including +3.44% in 2022; that does not recover the original annual figures.&lt;/p&gt;

&lt;p&gt;The audit runner fingerprints source code and cached input files before and after execution, saves scenario returns, and independently recalculates cumulative return and Sharpe from those returns. Those checks passed, as did 14 relevant engine tests. They check these calculations, not every modelling assumption or the historical result.&lt;/p&gt;

&lt;p&gt;The current universe filter uses funding-record counts from the cache before date restriction. Applying the threshold using only pre-holdout records yielded the same 45-symbol set in this cache. That does not establish point-in-time eligibility throughout development. No original run manifest or committed HFM code history was available to recover the historical code, universe and dataset. Nor does this audit prove the reserved interval was originally untouched.&lt;/p&gt;

&lt;p&gt;The earlier development null comparison, reported at about +28%, was not reproduced in this audit. It is not used here to infer a market-wide premium or selection skill.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Null diagnostics on the current rerun&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Two structure-preserving null tests were run post-hoc on the 17 holdout windows of the current reproduction result (net +1.31%, Sharpe +0.208). These diagnostics do not apply to the historical result and do not resolve the discrepancy. They ask two separable questions: did the temporal alignment between position decisions and outcomes contribute, and did the specific coin-selection rule add anything beyond random assignment?&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;CircularShiftNull — temporal alignment.&lt;/strong&gt; For each coin &lt;em&gt;i&lt;/em&gt;, a shift τᵢ is drawn uniformly from [0, T). The weight path becomes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;w̃ᵢ(t) = wᵢ((t + τᵢ) mod T)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;What this preserves: per-asset weight distribution, autocorrelation structure, holding period, and per-asset turnover &lt;code&gt;Σₜ |wᵢ(t) − wᵢ(t−1)|&lt;/code&gt;. What it destroys: the alignment between a position decision and the specific return interval it faced. H₀: the observed Sharpe is no better than an arbitrarily time-shifted version of the same weight paths. N = 500 seeds.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;RebalanceSelectionNull — coin selection.&lt;/strong&gt; Rebalance events are detected as:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;T_R = {t : ‖w(t) − w(t−1)‖₁ &amp;gt; ε}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;At each t ∈ T_R, the weight vector is permuted across universe symbols. What this preserves: rebalance schedule, gross exposure &lt;code&gt;Σᵢ|wᵢ|&lt;/code&gt;, number of active positions, weight magnitudes. What it destroys: which symbol receives which weight at each rebalance. N = 500 seeds.&lt;/p&gt;

&lt;p&gt;Both use the Davison-Hinkley finite-sample p-value to avoid the p = 0 artifact from small N:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;p = (1 + #{θ_null ≥ θ_obs}) / (N + 1)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Turnover inflation.&lt;/strong&gt; CircularShiftNull wraps each coin's weight path at the window boundary. When τᵢ &amp;gt; 0, the series at time T − τᵢ jumps discontinuously to the value at time 0, creating large simultaneous &lt;code&gt;|Δw|&lt;/code&gt; spikes at the seam. Mean null turnover reached 1.82× frozen. RebalanceSelectionNull avoids the wrap-around artifact but permutes across the full universe rather than the positive-funding eligible set, changing the effective cost profile. Mean null turnover: 1.34× frozen. In both cases the frozen strategy received a structural cost advantage independent of signal quality. The CircularShift p-value (0.010) reflects a mix of genuine temporal misalignment loss and artificial cost inflation that cannot be cleanly separated. These p-values are reported for transparency, not as evidence.&lt;/p&gt;

&lt;p&gt;The right next step is a funding-matched null: at each rebalance, permute weight assignments only within the positive-funding eligible set. This would hold funding-exposure loading constant and isolate the coin-ranking question directly. It was not implemented because the execution API does not expose the contemporaneous panel signal needed to define the eligible set.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Sharpe uncertainty via block bootstrap.&lt;/strong&gt; The 17 holdout windows are serially dependent: consecutive windows share overlapping formation data, and 21-day non-overlapping hold periods are far too short for serial correlation to decay. The naive standard error &lt;code&gt;1/√17 ≈ 0.24&lt;/code&gt; treats windows as independent and understates uncertainty by a material factor.&lt;/p&gt;

&lt;p&gt;A moving-block bootstrap was run on the 15-minute holdout return series. Block length L = 4032 bars corresponds to approximately 3 full rebalance cycles (~63 days), chosen to span the dominant autocorrelation horizon of the strategy. N = 2000 resamples; annualization factor √(365 × 24 × 4).&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;point estimate:      Sharpe +0.208
95% CI (L = 4032):  [−0.21, +0.44]
p(Sharpe &amp;gt; 0):       78.2%
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The 95% interval spans zero. With 17 non-overlapping windows, wide CIs are the honest result, not a flaw in the method.&lt;/p&gt;

&lt;p&gt;The historical REJECT decision is part of the research record, but a negative holdout is no longer a verified rationale in this article. The candidate remains on hold pending reconciliation. The positive rerun alone does not establish a tradeable edge. Analysis of this already-opened interval is exploratory, not a new out-of-sample confirmation.&lt;/p&gt;

&lt;h2&gt;
  
  
  Edge 3: Momentum and an unverified parameter sweep
&lt;/h2&gt;

&lt;p&gt;The historical summary describes daily cross-sectional momentum with weekly rebalancing and a best net return of approximately +97%, including taker fees. It gives four lookback results: 7 days +33%, 10 days −23%, 14 days +97%, and 21 days +35%.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fxz07ile4ex2ds4j0aijo.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fxz07ile4ex2ds4j0aijo.png" alt="Historically reported momentum returns at four lookbacks, not independently reproduced" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Values transcribed from the frozen narrative report. A matching saved run/configuration was not located in this check. These are not four independently validated results.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;If reproduced, that sweep would raise a parameter-sensitivity concern: the best setting is not surrounded by comparably strong results. It would not, by itself, prove the peak was luck. The historical summary also attributes about half the return to 2021 and reports a negative 2022. Those regime figures remain unverified here; the report did not estimate a formal beta decomposition.&lt;/p&gt;

&lt;p&gt;The reported WEAK verdict is a historical assessment, not a newly verified finding from this audit.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the audit actually established
&lt;/h2&gt;

&lt;p&gt;The current carry calculation is reproducible locally with recorded code and input hashes. Its headline values do not match the historical report. For the mean-reversion revision range and momentum sweep, narrative evidence is available but numerical reproduction remains incomplete.&lt;/p&gt;

&lt;p&gt;That is a less tidy conclusion than three precise diagnoses. It is also the result the evidence supports. A negative backtest deserves the same provenance checks as a profitable one.&lt;/p&gt;

&lt;p&gt;No candidate is being advanced to live trading on the basis of this article. The next step is to recover the historical run artifacts or explicitly retire unsupported historical numbers, not silently substitute new results into the old story.&lt;/p&gt;

&lt;p&gt;The &lt;a href="https://github.com/icesurf666/quant-research-notes/tree/main/studies/round2-three-edges-three-killers" rel="noopener noreferrer"&gt;research package&lt;/a&gt; preserves the original narrative evidence. This is not a complete independent reproduction of the private data pipeline. SHA-256 checks establish file identity, not economic validity. The frozen source retains stronger claims that this corrected article does not endorse.&lt;/p&gt;

&lt;p&gt;Evidence source SHA-256: &lt;code&gt;cf9ce73542af24be3e5c2872864b2b41c6ce864e3b2f07b760e5434a1bdb23ba&lt;/code&gt;&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Research system: HFM · Historical reports and a post-publication rerun · Bybit spot and perpetuals · Python&lt;/em&gt;&lt;/p&gt;

</description>
      <category>algorithms</category>
      <category>python</category>
      <category>datascience</category>
      <category>machinelearning</category>
    </item>
    <item>
      <title>My Football Model Passed Validation. A Five-Check Audit Killed It.</title>
      <dc:creator>Pavel Kazantsev 💎</dc:creator>
      <pubDate>Sun, 27 Sep 2026 07:08:32 +0000</pubDate>
      <link>https://dev.to/pavel_kkkkazantsev/my-football-model-passed-validation-a-check-audit-killed-it-37f4</link>
      <guid>https://dev.to/pavel_kkkkazantsev/my-football-model-passed-validation-a-check-audit-killed-it-37f4</guid>
      <description>&lt;p&gt;My football prediction model passed validation. Val away CLV was +152 bps, confidence interval excluded zero. Then I ran five forensic checks. Four failed, one passed, and the pass made the four failures harder to argue away.&lt;/p&gt;

&lt;h2&gt;
  
  
  The experiment
&lt;/h2&gt;

&lt;p&gt;The hypothesis: EPL bookmakers systematically underprice high-odds away teams. A LightGBM classifier trained on seven seasons of opening-line data would find those games and generate positive closing line value.&lt;/p&gt;

&lt;p&gt;CLV is &lt;code&gt;mean(model_prob - devigged_closing_prob)&lt;/code&gt;. Devigging removes the bookmaker's margin from decimal odds to recover fair probabilities that sum to 1. Positive CLV means your model put higher probability on an outcome than the final Pinnacle price did, after sharp money finished moving it. It is the standard measure of model skill in sports prediction research.&lt;/p&gt;

&lt;p&gt;14 features, all opening-line: devigged Pinnacle probabilities, B365, market consensus, Pinnacle-vs-consensus disagreement, raw decimal odds. No temporal index, no threshold from the full dataset. LightGBM with &lt;code&gt;n_estimators=300, learning_rate=0.03, num_leaves=15&lt;/code&gt;, hyperparameters locked before touching val. Logistic regression as a same-feature baseline. 380 val games (EPL 2022-23). One pre-registered hypothesis: AWAY outcome.&lt;/p&gt;

&lt;p&gt;The surface result:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Split&lt;/th&gt;
&lt;th&gt;Outcome&lt;/th&gt;
&lt;th&gt;CLV&lt;/th&gt;
&lt;th&gt;CI&lt;/th&gt;
&lt;th&gt;Status&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Val 2223&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Away&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;+152 bps&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;[+55, +251]&lt;/td&gt;
&lt;td&gt;CLV_ONLY_BRIER_FAIL&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Val 2223&lt;/td&gt;
&lt;td&gt;Home&lt;/td&gt;
&lt;td&gt;−39 bps&lt;/td&gt;
&lt;td&gt;[−132, +52]&lt;/td&gt;
&lt;td&gt;NULL_RESULT&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Test 2324&lt;/td&gt;
&lt;td&gt;Away&lt;/td&gt;
&lt;td&gt;+11 bps&lt;/td&gt;
&lt;td&gt;[−79, +100]&lt;/td&gt;
&lt;td&gt;NULL_RESULT&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Test 2324&lt;/td&gt;
&lt;td&gt;Home&lt;/td&gt;
&lt;td&gt;+176 bps&lt;/td&gt;
&lt;td&gt;[+95, +263]&lt;/td&gt;
&lt;td&gt;CLV_ONLY_BRIER_FAIL&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Val away CI excludes zero, which is what the hypothesis required. The verdict column reads &lt;code&gt;CLV_ONLY_BRIER_FAIL&lt;/code&gt; rather than &lt;code&gt;EDGE_CANDIDATE&lt;/code&gt; because the protocol requires positive CLV &lt;em&gt;and&lt;/em&gt; Brier improvement over the closing line. Five forensic checks determined why.&lt;/p&gt;

&lt;h2&gt;
  
  
  Check 1: What does the market itself drift?
&lt;/h2&gt;

&lt;p&gt;Any model trained on opening odds will inherit whatever directional drift the market already has between open and close. Measure that drift first, with no model, so you know what the baseline is.&lt;/p&gt;

&lt;p&gt;Val away drift: &lt;strong&gt;+3.9 bps&lt;/strong&gt; (CI: −19.5 to +26.8, crosses zero). No systematic directional movement in 2022-23 for away outcomes.&lt;/p&gt;

&lt;p&gt;The model showed +152 bps on the same split, so +148 bps was model-generated with no structural drift to explain it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Check 2: Brier skill score
&lt;/h2&gt;

&lt;p&gt;A model that systematically overestimates probabilities in the right direction will show positive CLV while being less accurate than the market. Brier score separates the two. To claim predictive value, the model needs a lower Brier score than the closing line on the same games.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fgwdgixxuncaocste2j8v.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fgwdgixxuncaocste2j8v.png" alt="Model Brier score vs closing line across 6 split-outcome combinations. Lower is better." width="800" height="394"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Model Brier vs closing line, all 6 split-outcome combinations. Lower is better. The model is worse on every single one.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;The model lost on Brier in all six combinations. Delta ranged from +0.004 to +0.019, consistently worse than the closing line.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;A model can beat the closing line on CLV while losing to it on Brier. It is overestimating probabilities in the right direction at the wrong magnitude. That is systematic bias.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Check 3: Feature cleanliness
&lt;/h2&gt;

&lt;p&gt;The first version of this experiment had two contaminated features. &lt;code&gt;season_num&lt;/code&gt; was a global index 0-8 derived from the full nine-season dataset, so val season got index 7 and the model extrapolated outside its training range. &lt;code&gt;is_high_odds_away&lt;/code&gt; was a binary flag using a threshold of 0.62 set by a calibration run on all nine seasons, including val and test.&lt;/p&gt;

&lt;p&gt;I removed both and reran from scratch with 14 features: opening odds from Pinnacle, B365, and market consensus; bookmaker disagreement; raw decimal odds. All from pre-match data only. Check 3: PASS. Zero violations.&lt;/p&gt;

&lt;p&gt;Removing the contaminated features dropped val away CLV from +216 bps to +152 bps, so the contamination was inflating the result by about 64 bps. With a clean feature set, the remaining failures have nothing to blame.&lt;/p&gt;

&lt;h2&gt;
  
  
  Check 4: Incremental CLV
&lt;/h2&gt;

&lt;p&gt;Standard CLV is &lt;code&gt;model_prob - close_prob&lt;/code&gt;. To separate the model's contribution from the market's own drift, compute &lt;code&gt;model_prob - open_prob&lt;/code&gt; instead. The closing line cancels algebraically:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;(model - close) - (open - close) = model - open
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F6u4iz8ai65cczb4vbt6p.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F6u4iz8ai65cczb4vbt6p.png" alt="Val away CLV decomposed. Market drifted +3.9 bps. Model added +147.7 bps." width="800" height="449"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Val away CLV decomposed. Market drifted +3.9 bps. Model added +147.7 bps. The model contributes 38x the underlying signal.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;The model pushed away probabilities &lt;strong&gt;147.7 bps above the opening line&lt;/strong&gt; (CI: [+52.5, +244.4]). True market drift was +3.9 bps. The model overshot by 38x.&lt;/p&gt;

&lt;p&gt;On test, the same model produced +11 bps CLV for away (CI: [−79, +100], crosses zero). The model was overestimating away probabilities in both splits. In val that overshoot happened to align with a small positive drift. In test it produced noise.&lt;/p&gt;

&lt;h2&gt;
  
  
  Check 5: Season stability
&lt;/h2&gt;

&lt;p&gt;If removing one training season flips the sign of the result, the result depends on that season, not on a real underlying relationship. Leave-one-out validation removes one training season at a time and re-measures val CLV.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Outcome&lt;/th&gt;
&lt;th&gt;Mean CLV&lt;/th&gt;
&lt;th&gt;Std&lt;/th&gt;
&lt;th&gt;Range&lt;/th&gt;
&lt;th&gt;Sign stable?&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Home&lt;/td&gt;
&lt;td&gt;−39.7 bps&lt;/td&gt;
&lt;td&gt;85.6&lt;/td&gt;
&lt;td&gt;[−200, +114]&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Draw&lt;/td&gt;
&lt;td&gt;−134.2 bps&lt;/td&gt;
&lt;td&gt;44.2&lt;/td&gt;
&lt;td&gt;[−213, −81]&lt;/td&gt;
&lt;td&gt;Yes (consistently negative)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Away&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;+106.3 bps&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;126.6&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;[−151, +314]&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;No (sign flips)&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Away CLV standard deviation: 126.6 bps. Removing season 2020-21 gives +314 bps, removing season 2021-22 gives −151 bps. The val result is entirely a function of which seasons happened to land in the training set.&lt;/p&gt;

&lt;h2&gt;
  
  
  Four failures. One pass. Verdict unchanged.
&lt;/h2&gt;

&lt;p&gt;Fixing the contamination improved the experiment, not the result. The core failures are independent of the bad features.&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;Finding&lt;/th&gt;
&lt;th&gt;Status&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Opening-line baseline&lt;/td&gt;
&lt;td&gt;True drift +3.9 bps; model added 38x more&lt;/td&gt;
&lt;td&gt;FAIL&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Brier skill score&lt;/td&gt;
&lt;td&gt;Model worse than market on all 6 combinations&lt;/td&gt;
&lt;td&gt;FAIL&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Feature cleanliness&lt;/td&gt;
&lt;td&gt;14 features, zero violations, clean rerun&lt;/td&gt;
&lt;td&gt;PASS&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Incremental CLV&lt;/td&gt;
&lt;td&gt;Model overshoots by 148 bps; collapses to noise on test&lt;/td&gt;
&lt;td&gt;FAIL&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Season stability&lt;/td&gt;
&lt;td&gt;Std 126.6 bps, sign flips on leave-one-out&lt;/td&gt;
&lt;td&gt;FAIL&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Logistic regression on the same features: val away CLV +87 bps (CI: [+57, +117]), Brier 0.185 vs closing 0.182, test away −102 bps (CI: [−135, −71]). Same pattern. The calibration failure is not specific to LightGBM.&lt;/p&gt;

&lt;p&gt;All four failing checks trace to the same thing: the model overestimates away probabilities. In 2022-23 that overshoot happened to align with a small and statistically insignificant positive drift. In 2023-24 it produced +11 bps with a CI that crosses zero. REJECT.&lt;/p&gt;

&lt;h2&gt;
  
  
  The contamination fix that changed nothing
&lt;/h2&gt;

&lt;p&gt;Val away CLV dropped from 216 to 152 bps when the contaminated features were removed. The contamination inflated the number. But 152 bps still excludes zero, so one could argue the signal survived.&lt;/p&gt;

&lt;p&gt;Brier closes that argument. With clean features, on both val and test, the model is less accurate than the closing line on all six outcome-split combinations. A model that cannot beat the market's accuracy is overfit, not predictive. The contamination was real. Removing it changed the CLV. The Brier failure is independent and survived the cleanup.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Finding contamination and fixing it is not enough. The clean rerun has to pass all gates. This one does not.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  What a genuine signal would require
&lt;/h2&gt;

&lt;p&gt;To restart this experiment honestly, the protocol needs harder gates. Brier improvement over closing as a required gate, not a diagnostic. Sign-stable LOO results before the test set is touched at all. Multi-league or multi-season test data, because one EPL season (380 games) is too small for confirmation at this effect size. At 150 bps, 380 games gives borderline statistical power and cannot separate a real edge from model miscalibration of the same magnitude. That is a different experiment from what this was.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Research system: HFM · LightGBM · 9 EPL seasons · Walk-forward OOS · Clean features v2&lt;/em&gt;&lt;/p&gt;

</description>
      <category>machinelearning</category>
      <category>python</category>
      <category>datascience</category>
      <category>computerscience</category>
    </item>
    <item>
      <title>My crypto backtest had a sharpe of 13.4. Then I added trading costs</title>
      <dc:creator>Pavel Kazantsev 💎</dc:creator>
      <pubDate>Wed, 23 Sep 2026 19:53:44 +0000</pubDate>
      <link>https://dev.to/pavel_kkkkazantsev/my-crypto-backtest-had-a-sharpe-of-134-then-i-added-trading-costs-5718</link>
      <guid>https://dev.to/pavel_kkkkazantsev/my-crypto-backtest-had-a-sharpe-of-134-then-i-added-trading-costs-5718</guid>
      <description>&lt;p&gt;I was not trying to prove a grand theory about crypto. I was testing simple, falsifiable baselines and looking for one result worth a second experiment.&lt;/p&gt;

&lt;p&gt;Cross-sectional reversal looked almost too clean: Sharpe &lt;code&gt;13.44&lt;/code&gt; before transaction costs, with positive returns in &lt;code&gt;67 / 76&lt;/code&gt; reported out-of-sample windows. Then I charged the same signal for the trading it required. At &lt;code&gt;10 bps&lt;/code&gt; per side, net Sharpe fell to &lt;code&gt;-40.13&lt;/code&gt;, and not one of those 76 windows remained positive.&lt;/p&gt;

&lt;p&gt;That is not a typo. I genuinely did not see it coming — the pre-cost numbers looked clean enough that I spent twenty minutes checking whether I had made an error somewhere before I accepted the result. It is what can happen when a short-lived pattern asks for a new portfolio every 15 minutes.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F7c3rvxpqurwa6rmw4owm.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F7c3rvxpqurwa6rmw4owm.jpg" alt="The HFM cost and funding implementation open during the experiment" width="800" height="1067"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;The less glamorous part of the experiment: execution delay, turnover, fees, slippage, and funding inside the same evaluation path.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What I actually tested
&lt;/h2&gt;

&lt;p&gt;This article covers one intentionally narrow reproduction of H3, a cross-sectional reversal baseline, on cached Bybit USDT perpetual data:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;15-minute bars from January 2021 to the pre-lockbox boundary in September 2025;&lt;/li&gt;
&lt;li&gt;an eight-bar, or two-hour, return signal;&lt;/li&gt;
&lt;li&gt;90-day formation periods followed by 21-day OOS periods;&lt;/li&gt;
&lt;li&gt;76 non-overlapping OOS windows, rolled every 21 days;&lt;/li&gt;
&lt;li&gt;causal volatility targeting at 10% annualized volatility, using a 96-bar trailing estimate and a 3× leverage cap;&lt;/li&gt;
&lt;li&gt;a sealed lockbox beginning September 1, 2025, which was not opened for this article.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A window counts as positive when the sum of its per-bar returns is greater than zero. That is the exact definition used by the evaluator for the window fractions below.&lt;/p&gt;

&lt;p&gt;The current cache contains &lt;code&gt;451&lt;/code&gt; symbol directories. That is not the same thing as a properly reconstructed point-in-time liquid universe. The panel uses the histories available in the cache, but the current reproduction does not apply a historical liquidity screen. Survivorship and availability bias therefore remain possible. This result is a development diagnostic, not evidence of a deployable alpha.&lt;/p&gt;

&lt;h2&gt;
  
  
  The signal was deliberately boring
&lt;/h2&gt;

&lt;p&gt;At each timestamp, H3 measures every available asset's return over the previous eight bars, standardizes those returns across the cross-section, and reverses the sign.&lt;/p&gt;

&lt;p&gt;Recent relative losers receive positive weights. Recent relative winners receive negative weights. The vector is scaled to unit gross exposure before portfolio-level volatility targeting.&lt;/p&gt;

&lt;p&gt;The core signal is short enough to inspect:&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;recent&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;prices&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;select&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="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pl&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;col&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;symbols&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="n"&gt;pl&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;col&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;symbols&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;shift&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;lookback_bars&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="p"&gt;)&lt;/span&gt;
&lt;span class="nb"&gt;long&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;recent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;unpivot&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;index&lt;/span&gt;&lt;span class="o"&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;on&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;symbols&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;variable_name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;symbol&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;value_name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;return&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;drop_nulls&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;return&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;stats&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;long&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;group_by&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="nf"&gt;agg&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;pl&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;col&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;return&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&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;standard_deviation&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;pl&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;col&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;return&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;std&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;signal&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;long&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;stats&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;on&lt;/span&gt;&lt;span class="o"&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="nf"&gt;with_columns&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;signal&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;pl&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;when&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pl&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;col&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;standard_deviation&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;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;then&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pl&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;col&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;return&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;pl&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;col&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;mean&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;pl&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;col&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;standard_deviation&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;otherwise&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;0.0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;gross&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;signal&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;group_by&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="nf"&gt;agg&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;gross_signal&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;pl&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;col&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;signal&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;abs&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="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;normalized&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;signal&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;gross&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;on&lt;/span&gt;&lt;span class="o"&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="nf"&gt;with_columns&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;weight&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;pl&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;when&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pl&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;col&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;gross_signal&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;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;then&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pl&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;col&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;signal&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;pl&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;col&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;gross_signal&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;otherwise&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;0.0&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;The production implementation also handles an empty cross-section and zero variance. No model is fitted here. There is no feature search hidden behind the result.&lt;/p&gt;

&lt;p&gt;A weight computed at time &lt;code&gt;t&lt;/code&gt; becomes live at &lt;code&gt;t+1&lt;/code&gt;. The evaluator enforces that delay centrally:&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;live&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;column&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="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;pl&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Expr&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;pl&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;col&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;column&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;shift&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;fill_null&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;0.0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Without the shift, the backtest would earn the same closing-bar return used to calculate the signal.&lt;/p&gt;

&lt;h2&gt;
  
  
  The number that made me stop
&lt;/h2&gt;

&lt;p&gt;With fees and slippage set to zero, the current run produced the following.&lt;/p&gt;

&lt;p&gt;I am not a professional quant. I run these experiments on my own time, publish the results as I go, and try to document what actually happens rather than what I hoped would happen. When I saw &lt;code&gt;13.44&lt;/code&gt; I let myself get briefly excited. That was a mistake.&lt;/p&gt;

&lt;p&gt;The zero-transaction-cost baseline still includes funding PnL and is not a pure gross-price-return series. Funding belongs to holding a perpetual position rather than transaction execution, so &lt;code&gt;13.44&lt;/code&gt; is the baseline Sharpe before fees and slippage.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Metric&lt;/th&gt;
&lt;th&gt;Zero-transaction-cost baseline&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Annualized Sharpe&lt;/td&gt;
&lt;td&gt;&lt;code&gt;13.44&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Positive OOS windows&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;67 / 76&lt;/code&gt; (&lt;code&gt;88.2%&lt;/code&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Maximum drawdown&lt;/td&gt;
&lt;td&gt;&lt;code&gt;-11.62%&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Summed absolute turnover&lt;/td&gt;
&lt;td&gt;&lt;code&gt;29,451&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;That last row is the warning. A huge pre-cost Sharpe and huge turnover can be two descriptions of the same fragile result.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fozurgzf7d8d1yghpza40.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fozurgzf7d8d1yghpza40.png" alt="The same signal before and after trading costs" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The comparison above is controlled: both bars come from the same code revision, data fingerprint, walk-forward windows, signal weights, and normalization. Only the cost model changes.&lt;/p&gt;

&lt;h2&gt;
  
  
  Making the backtest pay its bill
&lt;/h2&gt;

&lt;p&gt;Gross PnL uses execution-shifted weights. Turnover is the sum of absolute changes in live positions. Fees and slippage are fixed basis-point charges on turnover; funding is aligned separately with the long/short sign.&lt;/p&gt;

&lt;p&gt;The accounting identity is explicit:&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;fees&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;turnover&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;fee_bps&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;10_000&lt;/span&gt;
&lt;span class="n"&gt;slippage&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;turnover&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;slippage_bps&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;10_000&lt;/span&gt;
&lt;span class="n"&gt;net&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;gross&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;fees&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;slippage&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;funding&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is a rejection model, not an execution simulator. It does not model spread variation, order-book depth, queue position, partial fills, market impact, or adverse selection.&lt;/p&gt;

&lt;p&gt;Using &lt;code&gt;6 bps&lt;/code&gt; fees plus &lt;code&gt;4 bps&lt;/code&gt; slippage per side, the same every-bar signal produced:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Metric&lt;/th&gt;
&lt;th&gt;Net result&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Annualized Sharpe&lt;/td&gt;
&lt;td&gt;&lt;code&gt;-40.13&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Positive OOS windows&lt;/td&gt;
&lt;td&gt;&lt;code&gt;0 / 76&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cost / gross alpha&lt;/td&gt;
&lt;td&gt;&lt;code&gt;4.00×&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Total return&lt;/td&gt;
&lt;td&gt;approximately &lt;code&gt;-100%&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;I was hoping the cost model would hurt but leave something survivable. It did not. At this point I considered just not publishing — a Sharpe of &lt;code&gt;-40.13&lt;/code&gt; is not a great headline. But the result is real, the methodology is documented, and a clean failure is more useful to me than a buried one.&lt;/p&gt;

&lt;p&gt;The extreme Sharpe is less mysterious than it looks. A continuously refreshed cross-sectional portfolio creates persistent turnover. A relatively stable negative cost stream, annualized from 15-minute observations, can generate an absurdly negative ratio.&lt;/p&gt;

&lt;p&gt;The useful result is not the theatrical &lt;code&gt;-40.13&lt;/code&gt;. It is that the modeled trading bill was four times the gross PnL.&lt;/p&gt;

&lt;h2&gt;
  
  
  Slowing the strategy down
&lt;/h2&gt;

&lt;p&gt;The obvious response was to hold weights longer. Under a deliberately pessimistic &lt;code&gt;15 bps&lt;/code&gt; per-side scenario, net Sharpe improved as turnover fell:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Rebalance interval&lt;/th&gt;
&lt;th&gt;Net Sharpe&lt;/th&gt;
&lt;th&gt;Positive OOS windows&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Every bar / 15 minutes&lt;/td&gt;
&lt;td&gt;&lt;code&gt;-66.00&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;0 / 76&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Every 16 bars / 4 hours&lt;/td&gt;
&lt;td&gt;&lt;code&gt;-16.24&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;0 / 76&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Every 96 bars / 1 day&lt;/td&gt;
&lt;td&gt;&lt;code&gt;-4.45&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;10 / 76&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fn6w94js6ccj29c475iz8.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fn6w94js6ccj29c475iz8.png" alt="Cost sensitivity as the signal is rebalanced less often" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The friendliest tested corner used daily rebalancing and &lt;code&gt;3 bps&lt;/code&gt; per side. It reached a Sharpe of &lt;code&gt;-0.01&lt;/code&gt;, with &lt;code&gt;39 / 76&lt;/code&gt; positive windows. A &lt;code&gt;7.5 bps&lt;/code&gt; daily scenario produced &lt;code&gt;-1.70&lt;/code&gt;, with &lt;code&gt;27 / 76&lt;/code&gt; positive windows.&lt;/p&gt;

&lt;p&gt;Slower trading removed much of the damage, but it did not reveal a robust net edge. The signal decayed while it waited. And the maker-style scenario is especially optimistic because this vectorized test says nothing about queue position or whether a passive order would actually fill.&lt;/p&gt;

&lt;h2&gt;
  
  
  What failed, and what did not
&lt;/h2&gt;

&lt;p&gt;The experiment does not establish that “crypto mean reverts.” It shows a strong pre-transaction-cost short-horizon reversal pattern inside this particular development sample and cache. It also shows that the naive implementation cannot capture that pattern after even simple modeled friction.&lt;/p&gt;

&lt;p&gt;Those are different conclusions:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Research lead:&lt;/strong&gt; relative two-hour moves contain a repeatable pre-cost pattern in this sample.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Failed strategy:&lt;/strong&gt; resizing the book continuously consumes more than the pattern earns.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Open engineering problem:&lt;/strong&gt; reduce turnover without waiting so long that the signal disappears.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That leaves specific follow-ups: threshold entries, hysteresis bands, cost-aware sizing, historical universe reconstruction, and a fill model for passive execution. None is assumed to work. They are simply better questions than adding more indicators to the same high-turnover baseline.&lt;/p&gt;

&lt;h2&gt;
  
  
  Reproduction boundary
&lt;/h2&gt;

&lt;p&gt;The internal HFM repository reproduces the article slice with two commands once the market-data cache exists:&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;PYTHONPATH&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;src python scripts/reproduce_round1_h3_article.py
&lt;span class="nv"&gt;PYTHONPATH&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;src python scripts/make_article_h3_charts.py
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The first command rebuilds the panel and 76 walk-forward windows, evaluates all seven cost/rebalance scenarios, and writes machine-readable results. It also records the SHA-256 fingerprint of &lt;code&gt;1,442&lt;/code&gt; cached files (&lt;code&gt;880,708,217&lt;/code&gt; bytes), the hashes of every relevant source module, and the Python and Polars versions. The second command creates both figures directly from that result file.&lt;/p&gt;

&lt;p&gt;The &lt;a href="https://github.com/icesurf666/quant-research-notes" rel="noopener noreferrer"&gt;public repository&lt;/a&gt; contains the article, result snapshot, figures, minimal signal/cost code, deterministic tests, and a manifest that fails CI if any frozen artifact drifts. It does not distribute the market-data cache, so a reader can reproduce the mechanics and verify the published evidence package, but cannot independently regenerate the historical metrics without sourcing the data.&lt;/p&gt;

&lt;p&gt;Before treating the numbers as anything stronger than development evidence, keep four limits attached:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The cached-symbol universe is not a point-in-time liquidity universe and may contain survivorship or availability bias.&lt;/li&gt;
&lt;li&gt;Trading costs are fixed-bps assumptions, not observed live fills; maker scenarios omit queue and fill risk.&lt;/li&gt;
&lt;li&gt;The sealed lockbox was not evaluated, and these are backtest results rather than live returns.&lt;/li&gt;
&lt;li&gt;The 76 OOS windows do not overlap, but they are not 76 independent market regimes.&lt;/li&gt;
&lt;/ol&gt;

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

&lt;p&gt;I started the experiment hoping to find an edge. What survived was a better research rule: turnover belongs inside the hypothesis, not in the cleanup after a backtest looks good.&lt;/p&gt;

&lt;p&gt;A pre-cost Sharpe is not a strategy. It is a claim before execution. The strategy begins with what remains after the position delay, turnover, fees, slippage, funding, universe construction, and untouched data have all had their turn.&lt;/p&gt;

&lt;p&gt;In this run, almost nothing remained.&lt;/p&gt;

&lt;p&gt;If you have watched a promising backtest collapse once execution entered the model, what did the damage: turnover, spread, fill probability, market impact, or something less obvious?&lt;/p&gt;

&lt;p&gt;Results file SHA-256: &lt;a href="https://github.com/icesurf666/quant-research-notes/blob/main/studies/round1-cross-sectional-reversal/evidence/results.json" rel="noopener noreferrer"&gt;&lt;code&gt;fd1f506549b8f43ea33029acb3da760dbc88478f12f0122cf980bc725e6e85a9&lt;/code&gt;&lt;/a&gt;&lt;/p&gt;

</description>
      <category>python</category>
      <category>datascience</category>
      <category>quant</category>
      <category>web3</category>
    </item>
  </channel>
</rss>
