<?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: rytjy</title>
    <description>The latest articles on DEV Community by rytjy (@rytjy).</description>
    <link>https://dev.to/rytjy</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%2F4189121%2Fa81098d2-2207-4d74-b752-d6325e9b79ef.png</url>
      <title>DEV Community: rytjy</title>
      <link>https://dev.to/rytjy</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/rytjy"/>
    <language>en</language>
    <item>
      <title>37 public leaderboard strategies, one screen: the arithmetic holds, the concentration does not</title>
      <dc:creator>rytjy</dc:creator>
      <pubDate>Sat, 07 Nov 2026 08:19:14 +0000</pubDate>
      <link>https://dev.to/rytjy/37-public-leaderboard-strategies-one-screen-the-arithmetic-holds-the-concentration-does-not-4l5d</link>
      <guid>https://dev.to/rytjy/37-public-leaderboard-strategies-one-screen-the-arithmetic-holds-the-concentration-does-not-4l5d</guid>
      <description>&lt;p&gt;I parsed every strategy on a public leaderboard this week - 37 ids from Collective2's server-rendered page, monthly tables only, no login - and recomputed each one locally before looking at any ranking.&lt;/p&gt;

&lt;p&gt;What I recomputed: yearly monthly cells compounded against the printed YTD; annualised return as eq^(365/days)-1; the share of total log return carried by the best three months; annual return against reported max drawdown; record length; win rate against the break-even win rate implied by average win and average loss.&lt;/p&gt;

&lt;p&gt;The arithmetic reconciles. Across 36 active strategies, every year's monthly cells compound to the printed YTD within 0.4pp, and the annual figure matches calendar-day annualisation within -1.6 to +0.2pp on the five strategies I checked by hand. That rules out "the platform cannot compute". Any criticism has to be about what the number means, not whether it adds up.&lt;/p&gt;

&lt;p&gt;What varies (36 active):&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;19 carry more than 70% of their total log return in the best three months. One carries 194% - outside those months the record is net negative.&lt;/li&gt;
&lt;li&gt;11 have an annual return above 2.5x the reported max drawdown.&lt;/li&gt;
&lt;li&gt;11 have less than 18 months of history behind an annualised number.&lt;/li&gt;
&lt;li&gt;3 have a win rate under 2 standard errors above break-even.&lt;/li&gt;
&lt;li&gt;14 carry no flag at all. A clean result is also a result.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Two examples, both public:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;152974419: 14 months, +55.6% cumulative, 49.2% annualised, 7.5% max drawdown. One month - 2026-07 at +24.1% - carries 84% of the log return. 469 trades, 91.7% win rate against a 58.3% break-even: the per-trade edge looks real, the annual figure is one month.&lt;/li&gt;
&lt;li&gt;13202557: 22 years, +6,979%, and only 12% of it in the best three months. Concentration is not the issue; a 77% max drawdown is.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The limits, since a screen is not an audit: the leaderboard is a selection - it shows winners, and the strategies that disappeared are not measured; everything is hypothetical per the platform's own disclaimer; no broker statements were seen; the edge test assumes a fixed average win and loss.&lt;/p&gt;

&lt;p&gt;One page with the table and both examples: &lt;a href="https://github.com/rytjy/backtest-audit-kit/raw/main/docs/c2-screen-37.pdf" rel="noopener noreferrer"&gt;https://github.com/rytjy/backtest-audit-kit/raw/main/docs/c2-screen-37.pdf&lt;/a&gt;&lt;br&gt;
Parsed table: &lt;a href="https://github.com/rytjy/backtest-audit-kit/raw/main/docs/c2-screen-37.table.txt" rel="noopener noreferrer"&gt;https://github.com/rytjy/backtest-audit-kit/raw/main/docs/c2-screen-37.table.txt&lt;/a&gt;&lt;br&gt;
Repo: &lt;a href="https://github.com/rytjy/backtest-audit-kit" rel="noopener noreferrer"&gt;https://github.com/rytjy/backtest-audit-kit&lt;/a&gt;&lt;/p&gt;

</description>
      <category>trading</category>
      <category>datascience</category>
      <category>python</category>
    </item>
    <item>
      <title>A vendor claimed +90.2% annualised. Their own monthly table says 34.8% without three months.</title>
      <dc:creator>rytjy</dc:creator>
      <pubDate>Sat, 07 Nov 2026 06:06:03 +0000</pubDate>
      <link>https://dev.to/rytjy/a-vendor-claimed-902-annualised-their-own-monthly-table-says-348-without-three-months-49nh</link>
      <guid>https://dev.to/rytjy/a-vendor-claimed-902-annualised-their-own-monthly-table-says-348-without-three-months-49nh</guid>
      <description>&lt;p&gt;Last week a trading-platform newsletter landed in my inbox with a strategy of the week.&lt;/p&gt;

&lt;p&gt;The headline: &lt;strong&gt;Hypothetical Annual Return (Compounded) +90.2%&lt;/strong&gt;. Somewhere above it, in smaller type: &lt;em&gt;"All results are hypothetical."&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;I do not know whether the strategy works. I do know that a monthly percentage table is enough to check the arithmetic, so I checked it.&lt;/p&gt;

&lt;h2&gt;
  
  
  The table
&lt;/h2&gt;

&lt;p&gt;Two and a bit years of monthly cells, printed right there in the email:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Year&lt;/th&gt;
&lt;th&gt;months (%)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;2024&lt;/td&gt;
&lt;td&gt;-3.8, +8.3, -0.5, -7.9, +5.3, +3.9, +4.5&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2025&lt;/td&gt;
&lt;td&gt;+8.5, +0.1, +0.6, +50.7, +7.6, +4.5, +3.3, +10.9, -, +6.2, +12.4, +10.7&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2026&lt;/td&gt;
&lt;td&gt;+14.1, +23.4, -20.9, +20.3, -6.1, -8.9, -3.9, +2.8, +9.3, +18.5, +2.2, +50.1&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Compound them:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;2024 (7 months): &lt;strong&gt;+9.16%&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;2025 (11 months, September missing): &lt;strong&gt;+180.27%&lt;/strong&gt; — the email prints +180.4%&lt;/li&gt;
&lt;li&gt;2026 (11 months): &lt;strong&gt;+49.9%&lt;/strong&gt; — the email prints +50.1%&lt;/li&gt;
&lt;li&gt;Full span: roughly &lt;strong&gt;+350%&lt;/strong&gt;, which annualises to &lt;strong&gt;~90.3%&lt;/strong&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So the +90.2% is not a typo and not a fabrication. The arithmetic holds. That is the part almost nobody checks, and it passed.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the arithmetic hides
&lt;/h2&gt;

&lt;p&gt;Three questions turn "consistent" into "informative".&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. How much rests on a few months?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The three largest cells are +50.7%, +23.4%, +50.1%. Remove those three months and the annualised figure falls to about &lt;strong&gt;34.8%&lt;/strong&gt;. The strategy is not a smooth 90% engine; it is a handful of good months carrying a decade of averages.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. What is the worst peak-to-trough on monthly closes?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;-21.8%&lt;/strong&gt;. And 2026 contains -20.9%, then -6.1%, -8.9%, -3.9% in a row. Anyone sizing this needs to know that sequence exists before they read the annualised number.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. What exactly is being measured?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The vendor's own disclaimer: &lt;em&gt;"there is no single real-life trading account that looks exactly like the track record shown here."&lt;/em&gt; Fees are estimated, not taken from a broker statement. The track record is a model account.&lt;/p&gt;

&lt;p&gt;None of that is fraud. It is a &lt;strong&gt;hypothetical&lt;/strong&gt; number, clearly labelled as such, in 8-point type.&lt;/p&gt;

&lt;h2&gt;
  
  
  The three-question check
&lt;/h2&gt;

&lt;p&gt;For any seller quoting an annualised return:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Ask for the &lt;strong&gt;monthly cells&lt;/strong&gt;, not the annual figure.&lt;/li&gt;
&lt;li&gt;Compound them yourself. If it does not match, stop and ask why. If it matches, continue.&lt;/li&gt;
&lt;li&gt;Drop the &lt;strong&gt;three best months&lt;/strong&gt; and recompute; compute the worst &lt;strong&gt;monthly&lt;/strong&gt; peak-to-trough.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Two minutes, a calculator, and you learn whether the number is durable or decorative.&lt;/p&gt;

&lt;p&gt;I keep the working notes and a small standard-library toolkit for this kind of check in &lt;a href="https://github.com/rytjy/backtest-audit-kit" rel="noopener noreferrer"&gt;backtest-audit-kit&lt;/a&gt;. The write-up for this case is &lt;a href="https://github.com/rytjy/backtest-audit-kit/blob/main/cases/case15_c2_hypothetical_annualized.md" rel="noopener noreferrer"&gt;case 15&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;If you sell a strategy and your numbers survive these three questions, say so — that is rarer than a 90% headline.&lt;/p&gt;

</description>
      <category>python</category>
      <category>trading</category>
      <category>datascience</category>
    </item>
    <item>
      <title>Fifty Authorizations Is Not an Attack: 30 Blocks of EIP-7702 Data</title>
      <dc:creator>rytjy</dc:creator>
      <pubDate>Fri, 30 Oct 2026 10:08:28 +0000</pubDate>
      <link>https://dev.to/rytjy/fifty-authorizations-is-not-an-attack-30-blocks-of-eip-7702-data-3h54</link>
      <guid>https://dev.to/rytjy/fifty-authorizations-is-not-an-attack-30-blocks-of-eip-7702-data-3h54</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;On 2026-10-30 a major exchange's hot wallets were drained. Public reporting says the attackers never saw a private key — they got the signing infrastructure to sign 50 EIP-7702 authorizations in one transaction. I wrote a decoder to look for that shape on-chain. I found it in the first 30 blocks. It was a product, not an attack.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  The claim I wanted to test
&lt;/h2&gt;

&lt;p&gt;Reports described the pattern as: one transaction, dozens of &lt;code&gt;authorization_list&lt;/code&gt; entries, nonce values in sequence, all landing in a single block. If that shape is the signature of the attack, then a simple rule — &lt;em&gt;more than N authorizations in one transaction = suspicious&lt;/em&gt; — should find it.&lt;/p&gt;

&lt;p&gt;So I built the smallest tool that could check: RLP decoding, keccak256, secp256k1 recovery, no &lt;code&gt;web3&lt;/code&gt; dependency. EIP-7702 authorizations are signed over&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;keccak256(0x05 || rlp([chain_id, address, nonce]))
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Recover the signer, compare it with the transaction's &lt;code&gt;from&lt;/code&gt;, and you can answer the only three questions that matter structurally: &lt;strong&gt;who handed execution rights to which contract, does that contract have code, and how many authorizations are packed into this one transaction.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The sample
&lt;/h2&gt;

&lt;p&gt;Blocks 26,303,375 – 26,303,404 — about six minutes of mainnet time. Every type-4 transaction:&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;Value&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;type-4 transactions&lt;/td&gt;
&lt;td&gt;133 (~4.4 per block)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;authorizations&lt;/td&gt;
&lt;td&gt;319&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;unique signers&lt;/td&gt;
&lt;td&gt;308&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;unique delegates&lt;/td&gt;
&lt;td&gt;104&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;chain_id = 0&lt;/code&gt; (replayable everywhere)&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;0&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;delegate with no code&lt;/td&gt;
&lt;td&gt;1 (pointing at &lt;code&gt;0x0&lt;/code&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;auth signer != tx sender&lt;/td&gt;
&lt;td&gt;38 / 133&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Distribution of authorizations per transaction: 120 transactions carry exactly one. The rest are 2, 3, 4, 5, 6, 8, 21, 30, 32, 34 — and one with &lt;strong&gt;50&lt;/strong&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  The transaction that broke my rule
&lt;/h2&gt;

&lt;p&gt;Block 26,303,378, transaction &lt;code&gt;0x65f116e6…2e23&lt;/code&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;50 authorizations, 50 distinct signers&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;all pointing at the same delegate &lt;code&gt;0xe6b9…43ed&lt;/code&gt; (772 bytes of code)&lt;/li&gt;
&lt;li&gt;submitted by a single sender &lt;code&gt;0x59aa…e16b&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;the same sender filed 34 / 32 / 30 / 21 / 8 / 5-authorization transactions in adjacent blocks&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That is precisely the shape the incident was described with. It is also, almost certainly, a &lt;strong&gt;batch delegation service&lt;/strong&gt; — one operator, steady cadence, every signer handing execution rights to the same deployed contract, and no assets leaving in the same window.&lt;/p&gt;

&lt;p&gt;A rule of "5+ authorizations in one transaction = high severity" would have fired five times in six minutes. All five would have been wrong. &lt;strong&gt;An alarm that is 100% false positives is not an alarm.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What the rule should look like instead
&lt;/h2&gt;

&lt;p&gt;Structure alone is not enough. You need behavior layered on top, ordered by how cheaply it can be verified:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Is the delegate new?&lt;/strong&gt; A first-seen contract plus a batch of authorizations starts to look like an attack. A delegate with months of history does not.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Did assets move in the same window?&lt;/strong&gt; In the reported incident, the authorizations were not the payload — the transfers three minutes later were. Authorization and transfer have to be evaluated together.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Do the signers have any history with the sender?&lt;/strong&gt; 38 of 133 transactions had signer != sender, which is a legitimate relayer pattern. It stops being unremarkable when the signers have never interacted with that sender before.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;I implemented layer 1 only. Layers 2 and 3 need an indexer.&lt;/p&gt;

&lt;h2&gt;
  
  
  Update: I found the attack transaction
&lt;/h2&gt;

&lt;p&gt;After publishing the first version of this post I went back and scanned blocks 26,300,750 – 26,300,800 for every transaction touching Binance 14 (&lt;code&gt;0x28c6…1d60&lt;/code&gt;). It is in there.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;block &lt;strong&gt;26,300,753&lt;/strong&gt; — 2026-10-29 20:20:47 UTC&lt;/li&gt;
&lt;li&gt;tx &lt;strong&gt;&lt;code&gt;0xad99264d…298b&lt;/code&gt;&lt;/strong&gt;, type 4, from &lt;code&gt;0x7c8601…5ca0&lt;/code&gt;, to Binance 14&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;50 authorizations, every one of them signed by Binance 14&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;all delegating to &lt;code&gt;0x63e842b0…6fae&lt;/code&gt; (1,674 bytes of code), &lt;code&gt;chain_id = 1&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;nonces &lt;strong&gt;18,914,824 down to 18,914,775 — contiguous, 50 in a row&lt;/strong&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The receipt is &lt;code&gt;status: 1&lt;/code&gt;, 1,258,562 gas, and &lt;strong&gt;zero logs&lt;/strong&gt;. That transaction moved nothing. It installed the delegation, and the transfer came afterwards.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The discriminator is the nonce, not the count.&lt;/strong&gt; The batch-delegation product had 50 authorizations from 50 &lt;em&gt;different&lt;/em&gt; signers. The attack had 50 authorizations from &lt;strong&gt;one&lt;/strong&gt; signer with &lt;strong&gt;consecutive nonces&lt;/strong&gt; — a stack of pre-signed delegations to replay one after another. You can detect that without an indexer: group authorizations by signer, and flag any signer that appears N times with contiguous nonces. A product doing mass migration never repeats a signer; an attacker building a replay stack always does.&lt;/p&gt;

&lt;p&gt;That rule is now code, not a paragraph: &lt;code&gt;w7702.py&lt;/code&gt; flags any signer appearing three or more times over contiguous nonces. Run against both transactions: the attack fires &lt;code&gt;replay stack: 50 authorizations from 0x28c6… over contiguous nonces 18914775..18914824&lt;/code&gt;; the 50-signer product fires nothing. Eight unit tests, four of them for this rule alone.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;And then I measured how often it fires on ordinary traffic.&lt;/strong&gt; 300 randomly sampled blocks spanning seven days: &lt;strong&gt;1,015 type-4 transactions, 16 replay stacks — about 1.6%.&lt;/strong&gt; Every one of them carried &lt;strong&gt;3 or 5&lt;/strong&gt; authorizations; none carried more than five. The attack carried 50.&lt;/p&gt;

&lt;p&gt;So contiguity alone is a weak signal. A wallet pre-signing its next five delegations looks structurally identical to an operator pre-signing fifty, and the small version is routine: one sender in that sample filed three separate 5-nonce stacks back to back, which is exactly what an operational hot wallet does when it tops up its delegation budget.&lt;/p&gt;

&lt;p&gt;The separating dimension in this data is &lt;strong&gt;scale&lt;/strong&gt;, not shape. That is a worse answer than the one I wrote an hour ago, and it is the one the numbers support.&lt;/p&gt;

&lt;p&gt;The delegate itself is purpose-built. Disassembling its 1,674 bytes yields exactly &lt;strong&gt;one&lt;/strong&gt; embedded address — &lt;code&gt;0x7c8601…5ca0&lt;/code&gt;, the operator who sent the transaction. The destination is hardcoded into the contract, not supplied as an argument. A batch-delegation product is generic; this one already knows where the money goes.&lt;/p&gt;

&lt;p&gt;One more thing the scan showed: across blocks 26,300,750 – 26,300,821 (about fourteen minutes after the delegation landed), &lt;strong&gt;no transaction calls into Binance 14 at all&lt;/strong&gt; — only the hot wallet's own outbound traffic. So whatever moved the reported 167,464 ETH did not do it by calling the delegated account directly in that window.&lt;/p&gt;

&lt;p&gt;Binance 14 returns &lt;code&gt;0x&lt;/code&gt; today — no code, delegation cleared.&lt;/p&gt;

&lt;h2&gt;
  
  
  My own tool was wrong first
&lt;/h2&gt;

&lt;p&gt;The unit tests failed on the first run: my RLP encoder wrote integer &lt;code&gt;0&lt;/code&gt; as &lt;code&gt;0x00&lt;/code&gt;. RLP encodes zero as the &lt;strong&gt;empty string &lt;code&gt;0x80&lt;/code&gt;&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;The consequence is not cosmetic — a &lt;code&gt;nonce = 0&lt;/code&gt; authorization hashes differently, and recovery returns a &lt;strong&gt;fake signer&lt;/strong&gt;. Nonce zero is exactly what a first-time authorization from a fresh account looks like, so the bug would have produced wrong answers on the transactions most worth inspecting.&lt;/p&gt;

&lt;p&gt;Fixed, with a regression test. The same test round produced a real validation: in &lt;code&gt;0x73af…756f&lt;/code&gt;, the authorization signer is not the transaction sender, and the tool flags &lt;code&gt;no authorization belongs to the transaction sender&lt;/code&gt;. That is a relayer submitting on someone's behalf — legal, and exactly the kind of "valid on-chain, unclear intent" case a human should look at.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I did not verify
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;The figures — &lt;code&gt;$5.8B&lt;/code&gt; of outflows, &lt;code&gt;$3B&lt;/code&gt; frozen, &lt;code&gt;167,464 ETH&lt;/code&gt; in the first transfer — are still from public reporting. I confirmed the &lt;em&gt;delegation transaction&lt;/em&gt;; I did not reconstruct the transfers themselves, and I have not traced the delegate contract's internal calls.&lt;/li&gt;
&lt;li&gt;The 30-block sample above and the incident block are different windows; the earlier numbers (133 type-4 transactions) do not include the attack.&lt;/li&gt;
&lt;li&gt;30 blocks is roughly six minutes. "4.4 type-4 transactions per block" does not extrapolate to a day.&lt;/li&gt;
&lt;li&gt;I did not audit any delegate bytecode. The tool emits structural warnings; deciding whether a delegate is malicious is still a human judgement.&lt;/li&gt;
&lt;li&gt;Zero &lt;code&gt;chain_id = 0&lt;/code&gt; authorizations means zero &lt;em&gt;in this window&lt;/em&gt;, not zero on mainnet.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Why I am publishing the negative result
&lt;/h2&gt;

&lt;p&gt;The interesting output of six minutes of scanning is not a detection — it is a &lt;strong&gt;disproof of a detection rule I was about to write&lt;/strong&gt;. Shape-based rules are cheap to state and expensive to be wrong about in public. The data said no, so the rule changed.&lt;/p&gt;

&lt;p&gt;Code and full numbers: &lt;code&gt;security/w7702/&lt;/code&gt; — decoder, incremental scanner, five tests, and the raw report with an explicit scope statement.&lt;/p&gt;

</description>
      <category>ethereum</category>
      <category>security</category>
      <category>crypto</category>
      <category>blockchain</category>
    </item>
    <item>
      <title>I tried to reproduce four Collective2 Calmar ratios. None of them matched.</title>
      <dc:creator>rytjy</dc:creator>
      <pubDate>Wed, 28 Oct 2026 05:03:52 +0000</pubDate>
      <link>https://dev.to/rytjy/i-tried-to-reproduce-four-collective2-calmar-ratios-none-of-them-matched-3ek9</link>
      <guid>https://dev.to/rytjy/i-tried-to-reproduce-four-collective2-calmar-ratios-none-of-them-matched-3ek9</guid>
      <description>&lt;p&gt;I spent an evening recomputing the headline statistics on four Collective2 strategy pages. The profit factors mostly hold up. The Calmar ratios do not — none of the four.&lt;/p&gt;

&lt;p&gt;This is not a fraud hunt. It is a reproducibility check, the same one I would run on any backtest: take the numbers a page publishes, recompute the derived metrics from them, and see whether they agree.&lt;/p&gt;

&lt;h2&gt;
  
  
  The two definitions I used
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Calmar ratio&lt;/strong&gt; = annualized return ÷ maximum drawdown&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Profit factor&lt;/strong&gt; = (win % × average win) ÷ ((1 − win %) × average loss)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Both are standard. If a page shows all five inputs, its Calmar and profit factor are arithmetic, not opinion.&lt;/p&gt;

&lt;h2&gt;
  
  
  Results
&lt;/h2&gt;

&lt;p&gt;Four pages had complete summary blocks on 2026-10-27 (twelve candidates were listed, nine fetched; five showed all-zero stats and were excluded as inactive).&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Strategy&lt;/th&gt;
&lt;th&gt;Annual return&lt;/th&gt;
&lt;th&gt;Max DD&lt;/th&gt;
&lt;th&gt;Calmar (page)&lt;/th&gt;
&lt;th&gt;Calmar (recomputed)&lt;/th&gt;
&lt;th&gt;page ÷ recomputed&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;131145247&lt;/td&gt;
&lt;td&gt;32.4%&lt;/td&gt;
&lt;td&gt;14.1%&lt;/td&gt;
&lt;td&gt;3.52&lt;/td&gt;
&lt;td&gt;2.30&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;1.53×&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;132169961&lt;/td&gt;
&lt;td&gt;22.3%&lt;/td&gt;
&lt;td&gt;31.8%&lt;/td&gt;
&lt;td&gt;0.95&lt;/td&gt;
&lt;td&gt;0.70&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;1.35×&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;146455121&lt;/td&gt;
&lt;td&gt;56.9%&lt;/td&gt;
&lt;td&gt;29.7%&lt;/td&gt;
&lt;td&gt;2.76&lt;/td&gt;
&lt;td&gt;1.92&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;1.44×&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;146649042&lt;/td&gt;
&lt;td&gt;22.8%&lt;/td&gt;
&lt;td&gt;13.4%&lt;/td&gt;
&lt;td&gt;2.06&lt;/td&gt;
&lt;td&gt;1.70&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;1.21×&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Profit factor on the same pages:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Strategy&lt;/th&gt;
&lt;th&gt;PF (page)&lt;/th&gt;
&lt;th&gt;PF (recomputed)&lt;/th&gt;
&lt;th&gt;Difference&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;131145247&lt;/td&gt;
&lt;td&gt;1.50&lt;/td&gt;
&lt;td&gt;1.46&lt;/td&gt;
&lt;td&gt;+2.8%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;132169961&lt;/td&gt;
&lt;td&gt;2.70&lt;/td&gt;
&lt;td&gt;2.35&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;+14.8%&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;146455121&lt;/td&gt;
&lt;td&gt;3.10&lt;/td&gt;
&lt;td&gt;3.03&lt;/td&gt;
&lt;td&gt;+2.4%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;146649042&lt;/td&gt;
&lt;td&gt;1.70&lt;/td&gt;
&lt;td&gt;1.69&lt;/td&gt;
&lt;td&gt;+0.9%&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  What I take from it
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;1. The summary blocks are mostly coherent.&lt;/strong&gt; Three of four profit factors land within 3% — rounding level. That is what a well-formed summary looks like. It also means the pages are not sloppy in general, which makes the Calmar result more interesting, not less.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. The Calmar overshoot is not a constant.&lt;/strong&gt; 1.21× to 1.53×. If the platform used one different definition consistently, you would see a roughly fixed multiplier. You do not. So it is not "Calmar but annualized monthly" or "Calmar but with a rolling drawdown" — or if it is, the effect varies by strategy in a way the page never tells you.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. One page fails both checks.&lt;/strong&gt; 132169961's profit factor is 14.8% above what its own win rate and average win/loss imply. Either one input is stale, or the profit factor is computed over a different trade set than the other fields. Same page, same summary block.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4. The practical problem is comparability, not honesty.&lt;/strong&gt; If two strategies both show Calmar 2.0, nothing on the page tells you they were computed the same way. Ranking subscriptions by that number is ranking by a quantity whose definition you cannot see. The same applies to any platform metric you cannot recompute.&lt;/p&gt;

&lt;h2&gt;
  
  
  What this does not say
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;It does not say any seller is lying. The likeliest explanation is a platform-side definition difference — the profit-factor check mostly passing is evidence for that.&lt;/li&gt;
&lt;li&gt;I had no trade-level data and no equity curve. So I cannot say which field is wrong, and I cannot recompute max drawdown independently.&lt;/li&gt;
&lt;li&gt;I did not determine what Collective2's Calmar actually uses. That needs the equity series, which these pages do not expose publicly.&lt;/li&gt;
&lt;li&gt;Nothing here is a claim about future performance.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Reproduce it
&lt;/h2&gt;

&lt;p&gt;The checker is four rows of published fields and a handful of divisions:&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;ROWS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="c1"&gt;# id, ann_ret_pct, max_dd_pct, pf_page, win_pct, avg_win, avg_loss, calmar_page
&lt;/span&gt;    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;131145247&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;32.4&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;14.1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;1.50&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;35.6&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;903.0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;342.0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;3.52&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;132169961&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;22.3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;31.8&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;2.70&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;58.0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;315.0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;185.0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;0.95&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
&lt;span class="p"&gt;]&lt;/span&gt;

&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;sid&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ann&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;dd&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;pf_page&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;win&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;aw&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;al&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;cal_page&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;ROWS&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;cal_calc&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ann&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="n"&gt;dd&lt;/span&gt;
    &lt;span class="n"&gt;w&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;win&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mf"&gt;100.0&lt;/span&gt;
    &lt;span class="n"&gt;pf_calc&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;w&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;aw&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="mi"&gt;1&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;w&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;al&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sid&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;cal_page&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="n"&gt;cal_calc&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;pf_page&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="n"&gt;pf_calc&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Full script and tables: &lt;a href="https://github.com/rytjy/backtest-audit-kit/blob/main/audits/2026-10-28-collective2-calmar.md" rel="noopener noreferrer"&gt;backtest-audit-kit/audits&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why I published this
&lt;/h2&gt;

&lt;p&gt;If you are paying a monthly fee for a strategy, the headline ratio is part of what you are buying. It costs one afternoon to check whether that ratio can be recomputed from the page that advertises it. If it cannot, ask the seller which definition produced it — and treat "whatever the platform shows" as an answer.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Scope: verified — the arithmetic relationship between six summary fields on four public pages on 2026-10-27. Not verified — trade-level accuracy, drawdown computation, equity curve, platform metric definitions, future returns. No strategy seller was contacted.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>trading</category>
      <category>python</category>
      <category>datascience</category>
      <category>data</category>
    </item>
    <item>
      <title>I built a ghost detector for backtest edges. It was 99% confident and 80% right.</title>
      <dc:creator>rytjy</dc:creator>
      <pubDate>Tue, 27 Oct 2026 10:08:31 +0000</pubDate>
      <link>https://dev.to/rytjy/i-built-a-ghost-detector-for-backtest-edges-it-was-99-confident-and-80-right-2jl0</link>
      <guid>https://dev.to/rytjy/i-built-a-ghost-detector-for-backtest-edges-it-was-99-confident-and-80-right-2jl0</guid>
      <description>&lt;p&gt;&lt;em&gt;This is a submission for the &lt;a href="https://dev.to/challenges/hacktoberfest-week4-2026-10-26"&gt;Hacktoberfest Open-Source AI Challenge Week 4: Ghost in the Machine&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Every strategy in my archive looked alive once. A few still do. The rest are ghosts: equity curves that fell apart when the data changed, parameters that were tuned on the same bars they were judged on, an edge that existed only inside one sample.&lt;/p&gt;

&lt;p&gt;On Día de los Muertos you set a table for the ones who are gone and say their names. This post is that table for my dead configs — a small ofrenda built out of code — and the detector is the thing that tells me, before I spend money, which of the living are already ghosts.&lt;/p&gt;

&lt;p&gt;The clearest case is my best-looking config: an in-sample Sharpe of &lt;strong&gt;0.981&lt;/strong&gt;, the highest in the set. The detector's verdict on it: &lt;strong&gt;0.101&lt;/strong&gt;. And the first time I ran the tool on my own archive, all 29 strategies scored exactly &lt;strong&gt;0.000&lt;/strong&gt; — a fact about the tool, not about the strategies.&lt;/p&gt;

&lt;p&gt;I wanted a detector that answers one question — &lt;em&gt;given only what you could have known in-sample, how likely is this edge to survive?&lt;/em&gt; — and, more importantly, a detector honest about its own confidence. The second part turned out to be the hard one.&lt;/p&gt;

&lt;p&gt;And it is overconfident exactly where confidence matters: when it says &lt;em&gt;almost certainly real&lt;/em&gt; it is right about &lt;strong&gt;80%&lt;/strong&gt; of the time, not 99%. That gap is the reason this post exists.&lt;/p&gt;

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

&lt;p&gt;&lt;strong&gt;ghostscore&lt;/strong&gt; is a small CLI. You give it a strategy's in-sample statistics; it returns a probability that the edge survives out of sample. It is trained entirely on synthetic markets where the answer is known, then applied to real configs from my archive.&lt;/p&gt;

&lt;p&gt;It's for anyone who already has a strategy and a backtest and wants to know whether the edge is real before risking money on it.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ python3 -m ghostscore.cli real --n 900 --seed 7
spearman(ghost_score, OOS Sharpe) = 0.512   p = 0.0045   n = 29
top-5 mean OOS Sharpe 0.482 · bottom-5 mean OOS Sharpe 0.220
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;On 29 real configs — MACD variants, Donchian, Bollinger, RSI, time-series momentum, AO, each long/short with and without an EMA200 gate — the ranking correlates with realised out-of-sample Sharpe at 0.51. Not a crystal ball. Enough to be useful as a filter.&lt;/p&gt;

&lt;p&gt;The most instructive row is the one that looks best in-sample:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;config&lt;/th&gt;
&lt;th&gt;IS Sharpe&lt;/th&gt;
&lt;th&gt;OOS Sharpe&lt;/th&gt;
&lt;th&gt;ghost score&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Donchian (20)&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;0.981&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;0.455&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;0.101&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;MACD 12/26/5 + gate&lt;/td&gt;
&lt;td&gt;0.475&lt;/td&gt;
&lt;td&gt;0.496&lt;/td&gt;
&lt;td&gt;0.865&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;MACD 19/39/9&lt;/td&gt;
&lt;td&gt;0.631&lt;/td&gt;
&lt;td&gt;0.709&lt;/td&gt;
&lt;td&gt;0.536&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Donchian (20) has the highest in-sample Sharpe of the entire set and a survival probability of 0.101. That is the ghost the tool is for.&lt;/p&gt;

&lt;p&gt;Two things are true about that 0.512, and only one of them is flattering. The 29 real configs never entered training — the model only ever saw synthetic markets, so this is a genuine out-of-distribution check rather than a fit. But I did use those 29 rows to diagnose the two bugs described below, which means they are not a pristine holdout either. The honest reading is "one archive, one sample", not "validated".&lt;/p&gt;

&lt;h3&gt;
  
  
  How I use it
&lt;/h3&gt;

&lt;p&gt;As a triage band, not a probability: below 0.2 I do not spend money on the config; 0.2–0.6 I paper-trade; above 0.6 I look at the cost model before anything else. The bands come from these 29 rows and will move with a second archive.&lt;/p&gt;

&lt;h2&gt;
  
  
  Demo
&lt;/h2&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%2Frwbv220zobc5is2cj055.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%2Frwbv220zobc5is2cj055.gif" alt="ghostscore demo" width="720" height="442"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;A real terminal recording, not a mock-up. This clip runs with &lt;code&gt;--n 300&lt;/code&gt; to keep it short, so its holdout line (AUC 0.860, ECE 0.178) differs slightly from the numbers in this post, which come from &lt;code&gt;--n 900&lt;/code&gt;. Regenerate either with &lt;code&gt;scripts/record_demo.py&lt;/code&gt; or &lt;code&gt;ghostscore.cli real&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Same run as text, for anyone who cannot play the clip:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ python3 -m ghostscore.cli real --n 900 --seed 7
synthetic holdout: AUC 0.850  Brier 0.180  ECE 0.141  base 0.400  backend sklearn-hgb
   family            config  is_sharpe  oos_sharpe  ghost_score
     MACD     12/26/5 +gate      0.475       0.496        0.865
       AO           (5, 34)      0.282       0.608        0.823
  RSI-rev      (2, 10, 90)      0.206       0.475        0.793
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



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

&lt;p&gt;&lt;a href="https://github.com/rytjy/ghostscore" rel="noopener noreferrer"&gt;github.com/rytjy/ghostscore&lt;/a&gt; — MIT, Python, no API keys, CPU only. The full run takes &lt;strong&gt;~12 s on one CPU core&lt;/strong&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git clone https://github.com/rytjy/ghostscore &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;cd &lt;/span&gt;ghostscore
python3 &lt;span class="nt"&gt;-m&lt;/span&gt; ghostscore.cli run  &lt;span class="nt"&gt;--n&lt;/span&gt; 1500 &lt;span class="nt"&gt;--seed&lt;/span&gt; 7   &lt;span class="c"&gt;# synthetic only&lt;/span&gt;
python3 &lt;span class="nt"&gt;-m&lt;/span&gt; ghostscore.cli real &lt;span class="nt"&gt;--n&lt;/span&gt; 900  &lt;span class="nt"&gt;--seed&lt;/span&gt; 7   &lt;span class="c"&gt;# score real configs&lt;/span&gt;
python3 scripts/plot_calibration.py results/holdout.json
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  How I Built It
&lt;/h2&gt;

&lt;h3&gt;
  
  
  A generator where the truth is known
&lt;/h3&gt;

&lt;p&gt;Three market regimes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;noise&lt;/code&gt; — iid returns, nothing to exploit but costs;&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;trend&lt;/code&gt; — positively autocorrelated returns, momentum pays;&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;revert&lt;/code&gt; — negatively autocorrelated, momentum bleeds.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Returns are Student-t (fat tails) with occasional negative jumps, because Gaussian innovations put every real config outside the training support — more on that below. Half the strategy population uses an EMA200 gate, matching the archive and producing the long flat stretches that real PnL series actually contain.&lt;/p&gt;

&lt;p&gt;A strategy is &lt;code&gt;sign(trailing k-bar return)&lt;/code&gt;, filled at the close, costing 1/2/5 bps per edge. In-sample length is sampled from 900–1,900 bars so the model sees the length of the real configs (~1,094).&lt;/p&gt;

&lt;h3&gt;
  
  
  My first labels were pure noise (AUC 0.489)
&lt;/h3&gt;

&lt;p&gt;This was my first failure. Using a single out-of-sample window as the label gave &lt;strong&gt;AUC 0.489&lt;/strong&gt; — chance. A 480-bar OOS Sharpe has a standard error near 1.0; the label was mostly noise. The correlation between in-sample and out-of-sample Sharpe was &lt;strong&gt;−0.02&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;The fix: average OOS Sharpe over &lt;strong&gt;three independent continuation paths&lt;/strong&gt; and strengthen the regimes. Same data, same features, back to 0.85.&lt;/p&gt;

&lt;h3&gt;
  
  
  Features that survive scale changes
&lt;/h3&gt;

&lt;p&gt;Eleven in-sample features, all scale-free: Sharpe, skew, kurtosis, hit rate, turnover, max drawdown, profit factor, t-stat, lag-1 autocorrelation, Sharpe standard error, bar count. Absolute mean and volatility are deliberately excluded: synthetic markets run at 1% daily vol, crypto runs at 3–5%, and a tree model reads that difference as a regime.&lt;/p&gt;

&lt;h3&gt;
  
  
  One report, one named backend
&lt;/h3&gt;

&lt;p&gt;TabPFN is the intended classifier — a tabular foundation model, few-shot, CPU, no training loop. A gradient-boosting fallback keeps the demo runnable. Every report prints &lt;code&gt;backend&lt;/code&gt;, so a number can never be attributed to the wrong model.&lt;/p&gt;

&lt;h3&gt;
  
  
  The honest part: 99% confident, 80% right
&lt;/h3&gt;

&lt;p&gt;Synthetic holdout, n=270:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;AUC 0.850 · Brier 0.180 · ECE 0.141 · base rate 0.400
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;predicted bin&lt;/th&gt;
&lt;th&gt;n&lt;/th&gt;
&lt;th&gt;predicted&lt;/th&gt;
&lt;th&gt;observed&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;0.0–0.1&lt;/td&gt;
&lt;td&gt;110&lt;/td&gt;
&lt;td&gt;0.013&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;0.136&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;0.9–1.0&lt;/td&gt;
&lt;td&gt;71&lt;/td&gt;
&lt;td&gt;0.988&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;0.803&lt;/strong&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%2F28qs0e3rkxe3q05y46rw.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%2F28qs0e3rkxe3q05y46rw.png" alt="Reliability diagram: predicted probability vs observed frequency, 10 fixed-width bins" width="800" height="800"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;When it says "almost certainly real" it is right 80% of the time, not 99%. When it says "almost certainly a ghost" it is right 14% of the time, not 1%. Ranking and calibration are different abilities; this tool is good at the first and overconfident at the second. That gap is the finding.&lt;/p&gt;

&lt;h3&gt;
  
  
  The second failure: every real score was 0.000
&lt;/h3&gt;

&lt;p&gt;The first application to the archive returned &lt;strong&gt;0.000 for all 29 configs&lt;/strong&gt;. Two causes:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;code&gt;max_dd&lt;/code&gt; divided by a peak that can sit near zero, producing values like &lt;strong&gt;−4.8e11&lt;/strong&gt;;&lt;/li&gt;
&lt;li&gt;Gaussian innovations (kurtosis ≈ 3) against real crypto kurtosis of &lt;strong&gt;10–240&lt;/strong&gt;.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Fixes: multiplicative drawdown bounded to [−1, 0], Student-t innovations with negative jumps, and the EMA200 gate in half the synthetic population. After that the scores spread from 0.003 to 0.865 and the rank correlation appeared.&lt;/p&gt;

&lt;p&gt;I kept both failures in the repository history. A detector that was chance, then zero, then useful is a more honest artefact than one that arrived useful.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Open Matters Here
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;The model is not a black box.&lt;/strong&gt; The classifier is an open-source gradient-boosting implementation running on this machine, and every report prints &lt;code&gt;backend=sklearn-hgb&lt;/code&gt; so a number can never be attributed to the wrong model. I wanted TabPFN — an open-weights tabular foundation model — but its install failed here (repeated read timeouts from the package index). Rather than claim a model I did not run, I shipped the fallback and printed its name.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;You can see the failures.&lt;/strong&gt; A closed endpoint would hand you an AUC and an invoice. It would not show you a calibration table where the top bin says 0.988 and delivers 0.803, and it would not have kept the two earlier versions of this tool that scored chance (AUC 0.489) and then zero (all 29 configs at 0.000). Both are still in the repository history.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The regimes are auditable.&lt;/strong&gt; Every label comes from code you can read, not from a vendor's "AI score". You can disagree with my generator and rerun the experiment — that is the whole difference between an audit and a rating.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The data never leaves.&lt;/strong&gt; Strategies are the one thing in this field people do not upload. Local, CPU only, no API key, no account.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Spirit of It
&lt;/h2&gt;

&lt;p&gt;I keep a folder of dead configs. Some were killed by costs, some by a regime that ended, one by a drawdown I did not see because my accounting was wrong. This project is an ofrenda for them — not a memorial, a post-mortem. The detector's job is to say, before the money is spent, which of the living are already ghosts.&lt;/p&gt;

&lt;p&gt;The Donchian row is why I trust it more than its AUC. It had the best in-sample Sharpe I have ever produced, and the tool looked at it and said: &lt;em&gt;probably a ghost&lt;/em&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Limits, Stated Up Front
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;29 real configs, one archive.&lt;/strong&gt; Everything about real markets here rests on 29 rows from my own strategy folder — six families (MACD, Donchian, Bollinger, RSI, time-series momentum, AO), long/short, with and without an EMA200 gate. It is not a benchmark.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The label is Sharpe.&lt;/strong&gt; "Survives" is defined as out-of-sample Sharpe staying positive. A tool trained on that will miss an edge that shows up as convexity or as fat tails instead.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The regimes are mine.&lt;/strong&gt; Noise, trend and revert, with my autocorrelation and jump settings. If real markets have a fourth shape, the generator never taught it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Calibration is the weak end.&lt;/strong&gt; At the two extremes the tool is off by about 12 and 19 points. Use it to rank, not to size positions.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Costs are 1/2/5 bps per edge.&lt;/strong&gt; No funding, no borrow, no slippage beyond that, no capacity limit.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The test I owe this tool: run it on a second archive I did not build. If the rank correlation there comes in under 0.3, the detector is the ghost and this post is wrong.&lt;/p&gt;

&lt;h2&gt;
  
  
  Prize Categories
&lt;/h2&gt;

&lt;p&gt;None claimed. The TabPFN path is implemented in the repository, but its install failed on this machine, so every number here comes from the sklearn fallback and every report prints backend=sklearn-hgb.&lt;/p&gt;

&lt;h2&gt;
  
  
  About the Model, Honestly
&lt;/h2&gt;

&lt;p&gt;No partner category is claimed. The TabPFN path is implemented in the repository, but its install failed on this machine (repeated read timeouts from the package index), and every number in this post comes from the gradient-boosting fallback — the reports print &lt;code&gt;backend=sklearn-hgb&lt;/code&gt; for that reason. Claiming a partner category on a backend I did not run would be exactly the kind of in-sample story this tool is built to catch.&lt;/p&gt;

&lt;h2&gt;
  
  
  Build Log
&lt;/h2&gt;

&lt;h2&gt;
  
  
  Six commits, all inside the challenge window (first 2026-10-27 05:08Z, last 2026-10-27 10:04Z): &lt;a href="https://github.com/rytjy/ghostscore/commits/main" rel="noopener noreferrer"&gt;commit history&lt;/a&gt;. The two failures in this post are in that history, not rewritten. A DevRelay session is not included because the tool does not support this environment; this log replaces it.
&lt;/h2&gt;

&lt;p&gt;&lt;em&gt;All numbers are reproducible from the repository: &lt;code&gt;python3 -m ghostscore.cli real --n 900 --seed 7&lt;/code&gt; and &lt;code&gt;python3 scripts/plot_calibration.py results/holdout.json&lt;/code&gt;. Written with an AI assistant; every figure comes from code in the repo.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>devchallenge</category>
      <category>hf26challenge</category>
      <category>python</category>
      <category>machinelearning</category>
    </item>
    <item>
      <title>Same code, same question, three layouts: the verdict moved, the code did not</title>
      <dc:creator>rytjy</dc:creator>
      <pubDate>Mon, 26 Oct 2026 10:56:22 +0000</pubDate>
      <link>https://dev.to/rytjy/same-code-same-question-three-layouts-the-verdict-moved-the-code-did-not-573l</link>
      <guid>https://dev.to/rytjy/same-code-same-question-three-layouts-the-verdict-moved-the-code-did-not-573l</guid>
      <description>&lt;p&gt;I built a small harness to answer one narrow question: when a local model answers&lt;br&gt;
"is there a lookahead bug in this snippet?", how much of that answer is about the code?&lt;/p&gt;

&lt;p&gt;Short answer: on a 2B model, not much. Move the question above the code and the&lt;br&gt;
verdict flips - deterministically, on both runs.&lt;/p&gt;

&lt;h2&gt;
  
  
  Setup
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;6 snippets from real backtest-review work: 3 with a genuine defect (lookahead,
omitted fees, grid-search maximum reported as strategy performance), 3 that
match a suspicious pattern but are correct.&lt;/li&gt;
&lt;li&gt;3 prompt layouts, identical content otherwise:

&lt;ul&gt;
&lt;li&gt;E = evidence first: code, then question&lt;/li&gt;
&lt;li&gt;Q = question first: question, then code&lt;/li&gt;
&lt;li&gt;H = evidence first plus "if the code does not contain enough information,
answer UNSURE. Do not guess."&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;2 runs per cell, temperature 0.2, 16-token cap, CPU only, llama.cpp b11804.&lt;/li&gt;
&lt;li&gt;Two models: gemma-2-2b-it Q4_K_M and qwen2.5-coder-1.5b-instruct Q4_K_M.&lt;/li&gt;
&lt;li&gt;72 calls total.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Ground truth is my own reading of each snippet, recorded before the runs.&lt;/p&gt;

&lt;h2&gt;
  
  
  Result 1: the 1.5B model answers YES to everything
&lt;/h2&gt;

&lt;p&gt;qwen2.5-coder-1.5b returned YES on 36 of 36 calls, under all three layouts.&lt;/p&gt;

&lt;p&gt;Exactly half the items are positive, so an always-YES model scores 50%. It scored&lt;br&gt;
50.0%. Zero abstentions, zero flips: perfectly stable and perfectly uninformative.&lt;br&gt;
Stability and usefulness are different properties, and a fixed-answer model gets a&lt;br&gt;
perfect score on the stability one.&lt;/p&gt;

&lt;h2&gt;
  
  
  Result 2: the 2B model lands inside the trivial baseline
&lt;/h2&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;accuracy&lt;/th&gt;
&lt;th&gt;always-YES baseline&lt;/th&gt;
&lt;th&gt;YES rate&lt;/th&gt;
&lt;th&gt;abstentions&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;gemma-2-2b-it&lt;/td&gt;
&lt;td&gt;19/36 = 52.8%&lt;/td&gt;
&lt;td&gt;18/36 = 50.0%&lt;/td&gt;
&lt;td&gt;75.0%&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;qwen2.5-coder-1.5b&lt;/td&gt;
&lt;td&gt;18/36 = 50.0%&lt;/td&gt;
&lt;td&gt;18/36 = 50.0%&lt;/td&gt;
&lt;td&gt;100.0%&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;One correct answer above the always-YES baseline, on 36 calls. That is not a&lt;br&gt;
capability, it is rounding.&lt;/p&gt;

&lt;p&gt;The UNSURE instruction changed nothing: 0 abstentions across 72 calls. Both models&lt;br&gt;
answered with full confidence every single time.&lt;/p&gt;

&lt;h2&gt;
  
  
  Result 3: 4 of 6 items change verdict when only the layout changes
&lt;/h2&gt;

&lt;p&gt;Per item, in order E | Q | H, one character per run:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;item&lt;/th&gt;
&lt;th&gt;truth&lt;/th&gt;
&lt;th&gt;E&lt;/th&gt;
&lt;th&gt;Q&lt;/th&gt;
&lt;th&gt;H&lt;/th&gt;
&lt;th&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;lookahead&lt;/td&gt;
&lt;td&gt;defect&lt;/td&gt;
&lt;td&gt;YY&lt;/td&gt;
&lt;td&gt;NN&lt;/td&gt;
&lt;td&gt;YY&lt;/td&gt;
&lt;td&gt;layout-sensitive&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;fee_missing&lt;/td&gt;
&lt;td&gt;defect&lt;/td&gt;
&lt;td&gt;YY&lt;/td&gt;
&lt;td&gt;NN&lt;/td&gt;
&lt;td&gt;YY&lt;/td&gt;
&lt;td&gt;layout-sensitive&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;grid_select&lt;/td&gt;
&lt;td&gt;defect&lt;/td&gt;
&lt;td&gt;YY&lt;/td&gt;
&lt;td&gt;YY&lt;/td&gt;
&lt;td&gt;YY&lt;/td&gt;
&lt;td&gt;stable&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;fee_named_other&lt;/td&gt;
&lt;td&gt;clean&lt;/td&gt;
&lt;td&gt;NY&lt;/td&gt;
&lt;td&gt;NN&lt;/td&gt;
&lt;td&gt;YY&lt;/td&gt;
&lt;td&gt;layout-sensitive&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;rolling_ok&lt;/td&gt;
&lt;td&gt;clean&lt;/td&gt;
&lt;td&gt;YY&lt;/td&gt;
&lt;td&gt;YY&lt;/td&gt;
&lt;td&gt;NN&lt;/td&gt;
&lt;td&gt;layout-sensitive&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;label_shift&lt;/td&gt;
&lt;td&gt;clean&lt;/td&gt;
&lt;td&gt;YY&lt;/td&gt;
&lt;td&gt;YY&lt;/td&gt;
&lt;td&gt;YY&lt;/td&gt;
&lt;td&gt;stable&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Two of these are not noise. lookahead and fee_missing were answered YES twice with&lt;br&gt;
the code first, and NO twice with the question first. Same model, same text, same&lt;br&gt;
temperature - only the order changed, and the flip reproduced.&lt;/p&gt;

&lt;p&gt;Adding the UNSURE hint moved rolling_ok from YES, YES to NO, NO.&lt;/p&gt;

&lt;p&gt;Only one cell in the whole grid is plain run-to-run noise: fee_named_other under E,&lt;br&gt;
which gave NO then YES.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I take from it
&lt;/h2&gt;

&lt;p&gt;If a verdict changes when you move the question above the code, that verdict is not&lt;br&gt;
a measurement of the code. It is a measurement of the prompt.&lt;/p&gt;

&lt;p&gt;Concretely: one ask per finding is not enough for anything automated. Either ask the&lt;br&gt;
same item under several layouts and report the disagreement as a confidence signal,&lt;br&gt;
or keep the model as a pointer and let a human read the snippet.&lt;/p&gt;

&lt;p&gt;Also: consistency is not evidence. The most consistent model here never looked at&lt;br&gt;
the code.&lt;/p&gt;

&lt;h2&gt;
  
  
  A bug I found in my own harness
&lt;/h2&gt;

&lt;p&gt;The first version of this bench printed YES for all 36 gemma calls. Not because the&lt;br&gt;
model agreed with itself, but because the parser never read the model.&lt;/p&gt;

&lt;p&gt;This llama.cpp build still echoes the prompt next to the answer&lt;br&gt;
(--no-display-prompt notwithstanding). The prompt contains the sentence "Answer with&lt;br&gt;
exactly one word: YES, NO, or UNSURE", and my parser took the first YES/NO/UNSURE&lt;br&gt;
token in stdout - which is that sentence, not the model output.&lt;/p&gt;

&lt;p&gt;After the fix, 9 of 36 cells changed, every one of them from YES to NO: each was a NO&lt;br&gt;
the model had actually produced and the parser had overwritten. Rows from the buggy&lt;br&gt;
parser are kept in runs/*.buggy1026 and are counted nowhere above.&lt;/p&gt;

&lt;p&gt;Fix: keep full stdout, cut at the last Answer: marker, parse from there. Everything&lt;br&gt;
above comes from the fixed parser, and the harness now stores complete output so any&lt;br&gt;
row can be re-checked.&lt;/p&gt;

&lt;p&gt;The lesson is not "check your parser". It is that a harness with this bug reports a&lt;br&gt;
perfectly stable, perfectly wrong model, and nothing in the output looks broken.&lt;/p&gt;

&lt;h2&gt;
  
  
  Limits
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;6 items. A smoke test, not a benchmark.&lt;/li&gt;
&lt;li&gt;Ground truth is my own reading, not outside adjudication.&lt;/li&gt;
&lt;li&gt;Two small 4-bit models. Nothing here says anything about 7B+ models.&lt;/li&gt;
&lt;li&gt;No seed control, so run-to-run flips include sampling noise.&lt;/li&gt;
&lt;li&gt;One machine, CPU only.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Reproduce
&lt;/h2&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;python3 -m flipbench.runner --model gemma --runs 2 --out runs/gemma.jsonl
python3 -m flipbench.runner --model qwen  --runs 2 --out runs/qwen.jsonl
python3 -m flipbench.analyze runs/gemma.jsonl runs/qwen.jsonl
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;Roughly 8s per call for gemma, 6s for qwen, about 4 minutes per model here.&lt;/p&gt;

&lt;p&gt;Code: &lt;a href="https://github.com/rytjy/flipbench" rel="noopener noreferrer"&gt;https://github.com/rytjy/flipbench&lt;/a&gt;&lt;/p&gt;

</description>
      <category>llm</category>
      <category>machinelearning</category>
      <category>testing</category>
      <category>python</category>
    </item>
    <item>
      <title>A 1.5B model is a triage signal, not a verdict</title>
      <dc:creator>rytjy</dc:creator>
      <pubDate>Sat, 24 Oct 2026 06:03:08 +0000</pubDate>
      <link>https://dev.to/rytjy/a-15b-model-is-a-triage-signal-not-a-verdict-2j63</link>
      <guid>https://dev.to/rytjy/a-15b-model-is-a-triage-signal-not-a-verdict-2j63</guid>
      <description>&lt;p&gt;I run an auditor that reads trading-strategy repos while I sleep. A 1.5B model is part of it, and for a long time I described that part wrong.&lt;/p&gt;

&lt;p&gt;The tool runs five deterministic checks over the source, then hands each finding to a local model with exactly six lines of context and one yes/no question: &lt;em&gt;is this actually true here?&lt;/em&gt; Nothing leaves the machine. No API key, no upload, no account. The model is Qwen2.5-Coder-1.5B-Instruct Q4_K_M, loaded through llama.cpp, on a laptop CPU.&lt;/p&gt;

&lt;p&gt;The interesting part is not that it works. It is where it stops working, and what I changed once I measured that.&lt;/p&gt;

&lt;h2&gt;
  
  
  The 48-second constraint
&lt;/h2&gt;

&lt;p&gt;Four model checks on the bundled sample take about 48 to 50 seconds on this laptop. That is roughly twelve seconds per question. The deterministic pass, &lt;code&gt;--no-llm&lt;/code&gt;, takes under a second.&lt;/p&gt;

&lt;p&gt;Twelve seconds per question is the fact that shaped everything else. You cannot put that inside a loop over every file in a repository. You cannot put it behind a save button. What you can do is run it at night, or while you make coffee, and then read a report that is already sorted.&lt;/p&gt;

&lt;p&gt;Latency also removes a whole class of bad ideas for free. I stopped considering "ask the model to look around and tell me what seems off" not because it is philosophically wrong but because it costs twelve seconds and returns a paragraph, and I do not want a paragraph at that price. I want a pointer.&lt;/p&gt;

&lt;h2&gt;
  
  
  One question, six lines, three answers
&lt;/h2&gt;

&lt;p&gt;The model never sees the repository. It sees the finding's evidence line plus five lines around it, and it can answer YES, NO, or UNSURE. It is never asked to name a file, never asked for a line number, never asked for a summary.&lt;/p&gt;

&lt;p&gt;That last restriction came from a failure. In an early build I asked it to summarise the file. The output was fluent, specific, and wrong about which function did what — the kind of answer that is expensive to read because you have to verify every clause. A checker that points at line 9 and a model that confirms or denies a claim about line 9 is a pipeline. A model that writes prose about code is a different product, and not one a 1.5B can deliver.&lt;/p&gt;

&lt;p&gt;So the division is blunt:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Locations come from code, never from the model.&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The model answers one narrow question per finding&lt;/strong&gt;, and "UNSURE" is a real answer, not a failure.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The report prints the evidence line verbatim&lt;/strong&gt;, so the reason to read a finding is never the model's prose.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The instability I could not engineer away
&lt;/h2&gt;

&lt;p&gt;Here is the measurement that changed my design.&lt;/p&gt;

&lt;p&gt;I asked Gemma 2 2B IT Q4_K_M (temperature 0.2) the same yes/no question about the same code, three times, in isolated contexts. The answers were &lt;strong&gt;NO, NO, YES&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Same weights, same prompt, same evidence line. Two of three said the cost bug was not there. It was there; the checker had already printed the ledger line with no fee term in it.&lt;/p&gt;

&lt;p&gt;I also run both models and print their answers side by side, as a confidence signal. Two runs of the same audit gave &lt;strong&gt;3 of 4&lt;/strong&gt; agreement and then &lt;strong&gt;4 of 4&lt;/strong&gt;. It would be tidy to call that sampling variance, but it is not: the code changed between the runs — I moved the context window to the evidence line — so the difference is confounded. I would rather write that down than round it into a nicer story.&lt;/p&gt;

&lt;p&gt;Both runs are committed in the repository, and so are the three re-asks. That is the only claim I am willing to make: at 2B, on this task, &lt;strong&gt;the verdict is not a stable measurement&lt;/strong&gt;. It is a hint about where to look, from a reader who has read six lines.&lt;/p&gt;

&lt;p&gt;Once you accept that, the design stops fighting the model.&lt;/p&gt;

&lt;h2&gt;
  
  
  Five rules that follow
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;The checker owns the finding.&lt;/strong&gt; Every &lt;code&gt;path:line&lt;/code&gt; in the report comes from code that matched a pattern. The model cannot create a finding, move one, or delete one.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;One question per finding.&lt;/strong&gt; Narrow scope keeps a 1.5B inside the task. Broad scope turns it into a confident narrator.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The model is allowed to say nothing.&lt;/strong&gt; "UNSURE" and "nothing to add" are valid outputs, and the report still finishes without them.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Agreement is a hint, not a vote.&lt;/strong&gt; Two models agreeing means neither disputed the checker. It does not mean the finding is real, and the report never prints a confidence score I cannot compute.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Absence checks print what they searched.&lt;/strong&gt; "No fee term in this file" is a weaker statement than "no match for fee, commission, slippage, or spread across 214 lines". For negative findings the evidence is the search, so the search has to be in the output.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The blind spot is written in fixed text, not generated: the tool does not run the strategy, does not see your data, and cannot tell you whether the edge is real. All it can do is point at the places where a backtest usually lies and ask a small model whether the pointer is correct.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I have not measured yet
&lt;/h2&gt;

&lt;p&gt;The honest gap is the threshold. Right now a finding either fires or it does not, and the model's answer is read by a human. If I ever wanted to automate the decision — skip the ones the model denies, say — I would need two numbers I do not have:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;the margin around the cutoff&lt;/strong&gt;, meaning the distribution of scores for true findings versus near-misses, not three anecdotes;&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;the flip rate under prompt shift&lt;/strong&gt;, meaning the same question with a quoted line added or the field order changed.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Both are cheap to measure and both are missing from this post. That is the difference between a demo and something I would let run unattended on a repository I care about. I would rather say that here than discover it in production.&lt;/p&gt;

&lt;h2&gt;
  
  
  When to skip the model entirely
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;--no-llm&lt;/code&gt; runs the deterministic checks only, instantly, and it is the right mode more often than you would think:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;when you need the same output twice, byte for byte;&lt;/li&gt;
&lt;li&gt;when the repository is small enough that reading the report is faster than waiting;&lt;/li&gt;
&lt;li&gt;when you are about to argue with someone about a number and cannot afford a witness who changes its mind.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The model is worth its twelve seconds when the finding is ambiguous and you want a second reader at 3am. It is not worth it when you already know what to look at.&lt;/p&gt;

&lt;h2&gt;
  
  
  Try it
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pip &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-e&lt;/span&gt; &lt;span class="nb"&gt;.&lt;/span&gt;
slothaudit audit samples/broken_lookahead            &lt;span class="c"&gt;# ~50s, four model checks&lt;/span&gt;
slothaudit audit samples/broken_lookahead &lt;span class="nt"&gt;--compare&lt;/span&gt;   &lt;span class="c"&gt;# both models, one report&lt;/span&gt;
slothaudit audit samples/broken_lookahead &lt;span class="nt"&gt;--no-llm&lt;/span&gt;    &lt;span class="c"&gt;# deterministic only, instant&lt;/span&gt;
python3 &lt;span class="nt"&gt;-m&lt;/span&gt; slothaudit audit samples/broken_lookahead  &lt;span class="c"&gt;# works without install&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A clean repository gets a deliberately un-reassuring answer: &lt;em&gt;no v0 check fired; that means this run had nothing to point at, not that the strategy survives an audit.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Code: &lt;a href="https://github.com/rytjy/slothaudit" rel="noopener noreferrer"&gt;github.com/rytjy/slothaudit&lt;/a&gt;&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Disclosure: I used an AI assistant to help draft and edit this post. The measurements, the code, and the command output are mine, and the output above is pasted verbatim from runs on this machine.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>llm</category>
      <category>ai</category>
      <category>python</category>
      <category>opensource</category>
    </item>
    <item>
      <title>slothaudit: a night-shift auditor for your backtest, on a 1.5B model in your own laptop</title>
      <dc:creator>rytjy</dc:creator>
      <pubDate>Thu, 22 Oct 2026 10:35:36 +0000</pubDate>
      <link>https://dev.to/rytjy/slothaudit-a-night-shift-auditor-for-your-backtest-on-a-15b-model-in-your-own-laptop-59pi</link>
      <guid>https://dev.to/rytjy/slothaudit-a-night-shift-auditor-for-your-backtest-on-a-15b-model-in-your-own-laptop-59pi</guid>
      <description>&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%2Fzejifnob9yyjl2xz1y4i.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%2Fzejifnob9yyjl2xz1y4i.png" alt=" " width="800" height="336"&gt;&lt;/a&gt;&lt;em&gt;This is a submission for the &lt;a href="https://dev.to/challenges/hacktoberfest-week3-2026-10-19"&gt;Hacktoberfest Open-Source AI Challenge Week 3: Slow and Steady&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

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

&lt;p&gt;&lt;strong&gt;slothaudit&lt;/strong&gt; is a command-line auditor for trading-strategy repos that runs entirely offline. Point it at a folder; it runs five deterministic source checks, then a local 1.5B open-weight model reads the code each finding points at and answers one narrow question about it: &lt;em&gt;is this finding actually true here?&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;You start it before you go to bed. In the morning you read six lines.&lt;/p&gt;

&lt;p&gt;Why a night shift and not a chat window: the bugs that kill a backtest are boring and repetitive — a fill taken on the signal bar, a ledger that never mentions fees, one market event counted as eleven samples, a drawdown that resets its own peak. You find them when you already suspect them, which is exactly when you don't need help. So the boring pass runs while you're asleep, at sloth speed, on your own CPU.&lt;/p&gt;

&lt;h2&gt;
  
  
  Demo
&lt;/h2&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%2Fpkcakwuhrtpfg939chwt.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%2Fpkcakwuhrtpfg939chwt.gif" alt="slothaudit auditing the broken sample: each finding prints the source line it matched" width="760" height="507"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Rendered from a real run's output on 2026-10-23 — CPU only, network interface idle.&lt;/em&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ slothaudit audit samples/broken_lookahead
# slothaudit report — samples/broken_lookahead
Generated 2026-10-23 12:30, offline.

## Morning summary

FIX FIRST: samples/broken_lookahead/strategy.py:10 — fill price read on the signal bar
ALSO: samples/broken_lookahead/strategy.py:12 — no fee or slippage term in the ledger
ALSO: samples/broken_lookahead/strategy.py:8 — entries without a cooldown

## Findings

### CO-01 [high] no fee or slippage term in the ledger
samples/broken_lookahead/strategy.py:12
This file records PnL but never mentions fee, commission, slippage or spread. Searched 24 lines: 0 matches for fee|commission|slippage|spread|cost.
Evidence:
    samples/broken_lookahead/strategy.py:12  pnl = (exit_ - entry) * 100
Model check: **YES**
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A clean repo gets the other answer, and it is deliberately not reassuring:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ slothaudit audit samples/clean_strategy
No v0 check fired. That is not the same as clean: it means this run had
nothing to point at, not that the strategy survives an audit.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Both runs: &lt;strong&gt;under a minute&lt;/strong&gt; on a laptop CPU, no network.&lt;/p&gt;

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

&lt;p&gt;&lt;a href="https://github.com/rytjy/slothaudit" rel="noopener noreferrer"&gt;github.com/rytjy/slothaudit&lt;/a&gt; — Python standard library only, no dependencies beyond the local model binary.&lt;/p&gt;

&lt;p&gt;The split of labour is the point:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Locations come from code, never from the model.&lt;/strong&gt; The report prints &lt;code&gt;path:line&lt;/code&gt; from the checker. The model is never asked to name a file or a line.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Every finding prints the line it matched.&lt;/strong&gt; The checker already knows the exact source line, so the report shows it under &lt;code&gt;Evidence:&lt;/code&gt;. For an absence check (no fee term, no cooldown) it prints the ledger line and the number of lines searched. The model's prose is never the reason to open a file.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The model answers one question per finding&lt;/strong&gt;, with the surrounding six lines as context, and must start its reply with &lt;code&gt;yes&lt;/code&gt;, &lt;code&gt;no&lt;/code&gt; or &lt;code&gt;unsure&lt;/code&gt;. That is a task a 1.5B model can do. Asking it to "write a summary" produced confident nonsense — so I stopped asking.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Every report ends with the blind spot&lt;/strong&gt;, in fixed text: this tool does not run the strategy, does not see your data, and cannot tell you the edge is real.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  How I Built It
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Prior work, credited.&lt;/strong&gt; The five checks are distilled from my own earlier kit, &lt;a href="https://github.com/rytjy/backtest-audit-kit" rel="noopener noreferrer"&gt;backtest-audit-kit&lt;/a&gt;, which documents the failing cases they came from. slothaudit is a new project written this week during the challenge; the checks were rewritten as source-level heuristics and the model layer is new.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Models:&lt;/strong&gt; two open-weight GGUFs, both served by &lt;strong&gt;llama.cpp&lt;/strong&gt; build b11804 as a subprocess — &lt;code&gt;llama-cli&lt;/code&gt; in and out, one process per audit, nothing left running: Qwen2.5-Coder-1.5B-Instruct (1.1 GB) and Gemma 2 2B IT Q4_K_M (1.6 GB). &lt;code&gt;--model qwen&lt;/code&gt; or &lt;code&gt;--model gemma&lt;/code&gt; picks one; &lt;code&gt;--compare&lt;/code&gt; asks both.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Checks:&lt;/strong&gt; five regex/AST-level heuristics distilled from real audit cases: same-bar fills, missing cost terms, entries without a cooldown, peak resets in drawdown, and &lt;code&gt;where(x &amp;gt; 0, 0)&lt;/code&gt; turning NaN into zero.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Everything else:&lt;/strong&gt; Python 3.12 standard library. No key, no account, no telemetry, no upload.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Four decisions came from being honest about what a small local model can do:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Few-shot beat instruction.&lt;/strong&gt; "Reply with VERDICT: YES | NO | UNSURE" made it echo the template back verbatim. A worked example about &lt;em&gt;different&lt;/em&gt; code fixed it.&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;When the model has nothing to add, the tool says so.&lt;/strong&gt; An empty summary is rendered as "deterministic findings only", not as a confident paragraph.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;An unstable verdict is a finding, not noise.&lt;/strong&gt; Asked the same yes/no question about the same code three times, the 2B answered NO, NO, YES (temperature 0.2). That is why every finding now prints the source line the check matched: the model's answer is a second opinion, the line is the evidence — and when the two disagree, the line is what you act on.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;The slow path is the design.&lt;/strong&gt; Four checks through the 1.5B took about 48 seconds on this laptop's CPU. That number is why the tool is built to run overnight and print six lines in the morning, instead of pretending to be a chat window.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  The Lazy Payoff
&lt;/h2&gt;

&lt;p&gt;An audit of the broken sample takes &lt;strong&gt;about a minute&lt;/strong&gt; and produces four findings, one of which — the missing cost term — is the kind of thing I would otherwise notice three weeks later, after wondering why a strategy that "works" loses money.&lt;/p&gt;

&lt;p&gt;The honest comparison is not a minute vs 20 minutes of expert review. It is a minute vs the zero minutes I actually spend looking, because after the fifth project nobody re-reads the fill logic. That is the effort it saves: the pass that never happened.&lt;/p&gt;

&lt;p&gt;I ran it on both sample repos this week; the clean one fired nothing, which is the result that makes the other one trustworthy.&lt;/p&gt;

&lt;h2&gt;
  
  
  Prize Categories
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Best Use of Gemma.&lt;/strong&gt; slothaudit asks the same yes/no question of two open-weight models on the same laptop — Qwen2.5-Coder-1.5B-Instruct and Gemma 2 2B IT — both through llama.cpp, with no API key and no network. &lt;code&gt;--compare&lt;/code&gt; prints both answers for each finding and marks whether they agreed:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Model check: qwen YES · gemma YES
Agreement: yes
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;That is a confidence signal, not proof: agreement means neither model disputed the checker's reading of those six lines; disagreement points you at the line to read yourself. Neither model is asked where the bug is, and neither writes the report — they only answer the one question the deterministic checker cannot answer alone.&lt;/p&gt;

&lt;p&gt;On the bundled broken sample, two runs of the same audit gave &lt;strong&gt;3 of 4&lt;/strong&gt; and then &lt;strong&gt;4 of 4&lt;/strong&gt; agreement. The code changed between them, but asking Gemma the cost question again on its own returned NO in both variants — at 2B the verdict is not stable enough to be a metric. So the report leads with the checker's evidence line and treats agreement as a hint. Both runs are committed: &lt;a href="https://github.com/rytjy/slothaudit/blob/main/docs/compare_report_1022.md" rel="noopener noreferrer"&gt;2026-10-22&lt;/a&gt; and &lt;a href="https://github.com/rytjy/slothaudit/blob/main/docs/compare_report.md" rel="noopener noreferrer"&gt;2026-10-23&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Asked on its own on the same code, Gemma returned:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ slothaudit audit samples/broken_lookahead --model gemma
### LA-01 [high] fill price read on the signal bar
Model check: gemma **YES** — the code fills on the bar that produced the signal.

### CO-01 [high] no fee or slippage term in the ledger
Model check: gemma **YES**

### SI-01 [medium] entries without a cooldown
Model check: gemma **YES**

### MD-01 [medium] peak reset inside the drawdown loop
Model check: gemma **YES** — this code calculates a running peak by adding the current pnl to the previous peak.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Same code, same question, different run: this one says YES on the cost finding, while the 10-22 run and one isolated re-ask said NO. That flip is why the report prints the evidence line instead of a confidence score.&lt;/p&gt;

&lt;p&gt;Full report: &lt;a href="https://github.com/rytjy/slothaudit/blob/main/docs/gemma_only.md" rel="noopener noreferrer"&gt;docs/gemma_only.md&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Open Innovation Matters
&lt;/h2&gt;

&lt;p&gt;Three concrete reasons, not slogans:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;The code never leaves the machine.&lt;/strong&gt; A trading-strategy repo is a person's edge, and its data includes fills and account sizes. Uploading that to a hosted API to get a lint report is a bad trade. Here the audit runs on the same laptop the repo lives on — I tested it with the network interface down.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;It costs nothing per run.&lt;/strong&gt; A nightly audit is 20 model calls a day. On a hosted API that is a meter running; locally it is the laptop you already own, asleep at 3 a.m.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The model is swappable.&lt;/strong&gt; The interface is one function — &lt;code&gt;chat(prompt, model)&lt;/code&gt; — with llama.cpp as the local path and an Ollama HTTP server as the fallback. Qwen 1.5B and Gemma 2B are the same three lines of wiring. When a better small model appears, I drop in a GGUF. No re-platforming, no migration, no vendor.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The 1.5B is not a placeholder for a bigger model I wish I could afford. It is a deliberate choice: the checks find the problem, the model only has to answer "is this true here?" — and a small model that answers a narrow question honestly beats a large one that writes a beautiful paragraph I cannot verify.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Built during Week 3. The repo includes the two sample strategies used above, and the audit output is reproducible byte-for-byte on any machine with the same GGUF.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;AI disclosure:&lt;/strong&gt; the code and the draft of this post were produced with an AI coding assistant (OpenClaw), working from my direction. Every number here — the timings, the verdicts, the two-model comparison — comes from runs committed in the repo, and the demo GIF is rendered from real output, not staged.&lt;/p&gt;

</description>
      <category>devchallenge</category>
      <category>hf26challenge</category>
    </item>
    <item>
      <title>Four ways your backtest lies — and a dependency-free Python kit that catches them</title>
      <dc:creator>rytjy</dc:creator>
      <pubDate>Fri, 16 Oct 2026 15:03:28 +0000</pubDate>
      <link>https://dev.to/rytjy/four-ways-your-backtest-lies-and-a-dependency-free-python-kit-that-catches-them-3p8n</link>
      <guid>https://dev.to/rytjy/four-ways-your-backtest-lies-and-a-dependency-free-python-kit-that-catches-them-3p8n</guid>
      <description>&lt;p&gt;My strategy looked fine in the backtest and lost money live.\n\nIf you've done this for a while, you already know the suspects. What surprised me is how &lt;em&gt;mechanical&lt;/em&gt; they are — each one is a specific, checkable defect, not a vague "the market changed":\n\n- &lt;strong&gt;Look-ahead&lt;/strong&gt; — signals silently shifted onto future bars.\n- &lt;strong&gt;A ledger that doesn't balance&lt;/strong&gt; — cash/position conservation breaks, and the PnL is fiction.\n- &lt;strong&gt;Fees and funding counted zero times (or twice)&lt;/strong&gt; — the cost model quietly drops out.\n- &lt;strong&gt;Exit logic that scans the whole window&lt;/strong&gt; instead of the current bar.\n\nI packaged those four checks into a small, dependency-free Python toolkit (no numpy, no pandas):\n\n&lt;br&gt;
&lt;br&gt;
&lt;code&gt;\npip install backtest-audit-kit\n&lt;/code&gt;&lt;br&gt;
&lt;br&gt;
\n\n- a look-ahead detector\n- ledger invariants (cash/position conservation)\n- a fee / funding double-count detector\n- mutation testing, to find blind spots in your own test suite\n\n*&lt;em&gt;A worked example.&lt;/em&gt;* &lt;code&gt;cases/&lt;/code&gt; ships a real BTC backtest whose cost model was zero — reported PnL came out at roughly &lt;strong&gt;5.7x the principal&lt;/strong&gt;. But the structural problem was worse than the bug: the take-profit needed &lt;strong&gt;+0.08%&lt;/strong&gt; while a round-trip taker fee is &lt;strong&gt;0.10%&lt;/strong&gt;, so every winning trade net-loses &lt;em&gt;by construction&lt;/em&gt;. No amount of parameter tuning fixes that.\n\nRepo: &lt;a href="https://github.com/rytjy/backtest-audit-kit%5Cn%5Cn**What" rel="noopener noreferrer"&gt;https://github.com/rytjy/backtest-audit-kit\n\n**What&lt;/a&gt; check would you add?** I'm collecting the ones I haven't automated yet.&lt;/p&gt;

</description>
      <category>python</category>
      <category>trading</category>
      <category>showdev</category>
      <category>opensource</category>
    </item>
  </channel>
</rss>
