<?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: Arhan Canli</title>
    <description>The latest articles on DEV Community by Arhan Canli (@arhancanli).</description>
    <link>https://dev.to/arhancanli</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%2F4145298%2F97d83989-e99d-4dd8-a814-d88a50412e96.png</url>
      <title>DEV Community: Arhan Canli</title>
      <link>https://dev.to/arhancanli</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/arhancanli"/>
    <language>en</language>
    <item>
      <title>1,285 strategy sleeves, frozen before the test: the year-by-year table is the warning</title>
      <dc:creator>Arhan Canli</dc:creator>
      <pubDate>Fri, 16 Oct 2026 10:59:46 +0000</pubDate>
      <link>https://dev.to/arhancanli/1285-strategy-sleeves-frozen-before-the-test-the-year-by-year-table-is-the-warning-2np4</link>
      <guid>https://dev.to/arhancanli/1285-strategy-sleeves-frozen-before-the-test-the-year-by-year-table-is-the-warning-2np4</guid>
      <description>&lt;p&gt;Most backtest write-ups report one number for the whole test window. I want to show what happens when you split the same test by year.&lt;/p&gt;

&lt;h2&gt;
  
  
  The protocol
&lt;/h2&gt;

&lt;p&gt;We built 1,285 small strategies ("sleeves") in two blocks:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;1,026 stock sleeves:&lt;/strong&gt; 27 signals, each run inside every industry with at least 10 names, long the best-ranked and short the worst.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;259 crypto sleeves:&lt;/strong&gt; cash-and-carry per coin, long spot and short the same coin's perpetual, switched on while funding pays.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Everything was chosen on &lt;em&gt;search years&lt;/em&gt; (stocks 1998 to 2017, crypto 2020 to 2022). The selection was written to a file, &lt;code&gt;frozen_selection.json&lt;/code&gt;, and its SHA-256 (&lt;code&gt;8d1803de7e04d3ee...&lt;/code&gt;) was pushed before the test ran. Then the frozen list was tested once on years the search never saw.&lt;/p&gt;

&lt;h2&gt;
  
  
  The headline
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Block&lt;/th&gt;
&lt;th&gt;Test years&lt;/th&gt;
&lt;th&gt;Sharpe after costs&lt;/th&gt;
&lt;th&gt;Before costs&lt;/th&gt;
&lt;th&gt;Worst drawdown&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Stocks, 1,026 sleeves&lt;/td&gt;
&lt;td&gt;2018-01 to 2026-06&lt;/td&gt;
&lt;td&gt;0.73 (t 2.12)&lt;/td&gt;
&lt;td&gt;0.85&lt;/td&gt;
&lt;td&gt;-14.8%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Crypto, 259 coins&lt;/td&gt;
&lt;td&gt;2023-01 to 2026-09&lt;/td&gt;
&lt;td&gt;4.19 (t 8.03)&lt;/td&gt;
&lt;td&gt;8.03&lt;/td&gt;
&lt;td&gt;-6.1%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Both at equal risk&lt;/td&gt;
&lt;td&gt;2023-01 to 2026-06&lt;/td&gt;
&lt;td&gt;3.42 (t 6.35)&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;-17.2%&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The two blocks' correlation was 0.02. The stock block earned about half its search-year Sharpe (1.43), which is the usual decay. Read alone, the last row is the kind of number that gets posted.&lt;/p&gt;

&lt;h2&gt;
  
  
  The same test, by year
&lt;/h2&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;Stocks&lt;/th&gt;
&lt;th&gt;Crypto&lt;/th&gt;
&lt;th&gt;Both&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;2023&lt;/td&gt;
&lt;td&gt;1.54&lt;/td&gt;
&lt;td&gt;7.12&lt;/td&gt;
&lt;td&gt;4.59&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2024&lt;/td&gt;
&lt;td&gt;2.13&lt;/td&gt;
&lt;td&gt;11.99&lt;/td&gt;
&lt;td&gt;8.05&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2025&lt;/td&gt;
&lt;td&gt;0.31&lt;/td&gt;
&lt;td&gt;0.42&lt;/td&gt;
&lt;td&gt;0.41&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2026 to date&lt;/td&gt;
&lt;td&gt;-0.05&lt;/td&gt;
&lt;td&gt;-3.01&lt;/td&gt;
&lt;td&gt;-2.05&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Since January 2025 the combined book's Sharpe is &lt;strong&gt;-0.44&lt;/strong&gt; (t -0.53, 366 days). The strong overall figure comes from 2023 and 2024.&lt;/p&gt;

&lt;p&gt;The crypto side shows why. The average daily funding collected while a position was on was 0.0449% in 2024, 0.0095% in 2025 and about zero in 2026 (-0.0003%). The carry lives on that income. With it gone, switching coins on and off around zero funding costs more than it earns: before costs the 2026 crypto Sharpe is +0.98, after costs -3.01.&lt;/p&gt;

&lt;h2&gt;
  
  
  Two other things the search taught
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Costs do not diversify.&lt;/strong&gt; Every sleeve's costs add up while its noise cancels. Holding overlapping portfolios and trading through a no-trade band cut the stock book's costs to about 1.2% a year (0.0118 in the result file).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;One bug, published.&lt;/strong&gt; The first test run left out the frozen no-trade band (a runner bug). Its result (stocks 0.52, combined 3.16) is kept in the repo as &lt;code&gt;test_result_run1_without_frozen_band.json&lt;/code&gt;. The corrected run applies the band the frozen file already named; nothing in the frozen file changed.&lt;/li&gt;
&lt;/ul&gt;

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

&lt;p&gt;No sleeve is admitted to a live book. The forward record is the only clean test left, and the yearly table says the edge may be thinner now than the window average. A freeze and a single test protect you from fooling yourself during the search; they do not make the market keep paying.&lt;/p&gt;

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

&lt;p&gt;The protocol, the frozen file and both result files are in the &lt;code&gt;research/breadth-lab&lt;/code&gt; branch of github.com/arhancanli/alphac. The validation tools (deflated Sharpe, placebo tests) live in github.com/arhancanli/canlicapital, and &lt;code&gt;canli-mcp&lt;/code&gt; is a free MCP server (MIT, no API key, runs locally): &lt;code&gt;npx -y canli-mcp&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;How do you report a test window whose last two years disagree with the first two: the average, the split, or both?&lt;/p&gt;

&lt;p&gt;If this was useful to you, a star on the canlicapital repo is appreciated, and so is sending this to someone who'd use it.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Written with AI assistance. Every number is from &lt;code&gt;test_result.json&lt;/code&gt; and &lt;code&gt;RESULTS.md&lt;/code&gt; in research/breadth_lab on the alphac repo (saved in drafts/data on 2026-10-16).&lt;/em&gt;&lt;/p&gt;

</description>
      <category>algotrading</category>
      <category>finance</category>
      <category>python</category>
      <category>statistics</category>
    </item>
    <item>
      <title>Central banks' own policy rates against the BIS's copy: 9 of 9, and one convention to know</title>
      <dc:creator>Arhan Canli</dc:creator>
      <pubDate>Fri, 16 Oct 2026 04:58:10 +0000</pubDate>
      <link>https://dev.to/arhancanli/central-banks-own-policy-rates-against-the-biss-copy-9-of-9-and-one-convention-to-know-21jj</link>
      <guid>https://dev.to/arhancanli/central-banks-own-policy-rates-against-the-biss-copy-9-of-9-and-one-convention-to-know-21jj</guid>
      <description>&lt;p&gt;If you need policy rates for several countries, the Bank for International Settlements is the obvious place: its &lt;code&gt;WS_CBPOL&lt;/code&gt; dataset has about forty central banks' policy rates as daily series, behind one free API. But it is a copy. I wanted to know whether it matches what the central banks publish themselves before using it in a tool.&lt;/p&gt;

&lt;h2&gt;
  
  
  The check
&lt;/h2&gt;

&lt;p&gt;On three quarter-ends of 2026 I compared the BIS series with each central bank's own source:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;the &lt;strong&gt;ECB's&lt;/strong&gt; deposit facility rate, from the ECB's data portal;&lt;/li&gt;
&lt;li&gt;the &lt;strong&gt;Bank of England's&lt;/strong&gt; Bank Rate, from its statistical database;&lt;/li&gt;
&lt;li&gt;the &lt;strong&gt;Fed's&lt;/strong&gt; target range for the federal funds rate, from the New York Fed's daily data.&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Date&lt;/th&gt;
&lt;th&gt;ECB deposit rate&lt;/th&gt;
&lt;th&gt;BIS euro area&lt;/th&gt;
&lt;th&gt;Bank Rate&lt;/th&gt;
&lt;th&gt;BIS UK&lt;/th&gt;
&lt;th&gt;Fed target range&lt;/th&gt;
&lt;th&gt;BIS US&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;31 Mar 2026&lt;/td&gt;
&lt;td&gt;2.00&lt;/td&gt;
&lt;td&gt;2.00&lt;/td&gt;
&lt;td&gt;3.75&lt;/td&gt;
&lt;td&gt;3.75&lt;/td&gt;
&lt;td&gt;3.50 to 3.75&lt;/td&gt;
&lt;td&gt;3.625&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;30 Jun 2026&lt;/td&gt;
&lt;td&gt;2.25&lt;/td&gt;
&lt;td&gt;2.25&lt;/td&gt;
&lt;td&gt;3.75&lt;/td&gt;
&lt;td&gt;3.75&lt;/td&gt;
&lt;td&gt;3.50 to 3.75&lt;/td&gt;
&lt;td&gt;3.625&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;30 Sep 2026&lt;/td&gt;
&lt;td&gt;2.50&lt;/td&gt;
&lt;td&gt;2.50&lt;/td&gt;
&lt;td&gt;3.75&lt;/td&gt;
&lt;td&gt;3.75&lt;/td&gt;
&lt;td&gt;3.75 to 4.00&lt;/td&gt;
&lt;td&gt;3.875&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;All nine agreed.&lt;/p&gt;

&lt;h2&gt;
  
  
  The convention: a range becomes its midpoint
&lt;/h2&gt;

&lt;p&gt;The Fed doesn't set one rate, it sets a range. The BIS series has one number a day, and for the US that number is the &lt;strong&gt;midpoint&lt;/strong&gt; of the range: 3.625 for 3.50 to 3.75. Compare it with the upper bound, as many headlines quote the Fed, and it looks 12.5 basis points wrong. The check above counts the BIS figure as agreeing when it sits inside the New York Fed's range, which it does on every date.&lt;/p&gt;

&lt;h2&gt;
  
  
  Two smaller things
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;The ECB publishes change dates, not a daily series.&lt;/strong&gt; Its policy-rate series has one row per decision: the date the new rate took effect. To get the rate on a given day you need the last change on or before it, including changes from before your window (the deposit rate in force on 1 January 2026 was set on 11 June 2025). The tool returns the rate in effect on the first day you ask for, then each change.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The BIS can lag the announcement by a few days.&lt;/strong&gt; For a decision made this week, read the central bank's own series first.&lt;/p&gt;

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

&lt;p&gt;&lt;code&gt;reference_rates&lt;/code&gt; in canli-mcp, a free MCP server (MIT, no API key), reads all of these: SOFR, the effective fed funds rate with the Fed's range, and other New York Fed rates; the euro short-term rate and the ECB's three policy rates; SONIA and Bank Rate; and the BIS series for about forty central banks.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx &lt;span class="nt"&gt;-y&lt;/span&gt; canli-mcp
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two rates in one &lt;code&gt;run_tool&lt;/code&gt; call:&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="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"calls"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"reference_rates"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"arguments"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"rate"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"ecb_deposit"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"start"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2026-01-01"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"reference_rates"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"arguments"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"rate"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"policy:JP"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The comparison script is &lt;code&gt;mcp-markets/bench/global/crosscheck.mjs&lt;/code&gt;, with its saved results:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://github.com/arhancanli/canlicapital" rel="noopener noreferrer"&gt;arhancanli/canlicapital&lt;/a&gt;:&lt;/strong&gt; every MCP server, the check and its results.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://github.com/arhancanli/alphac" rel="noopener noreferrer"&gt;arhancanli/alphac&lt;/a&gt;:&lt;/strong&gt; the strategy research engine.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If this was useful to you, a star on &lt;a href="https://github.com/arhancanli/canlicapital" rel="noopener noreferrer"&gt;the canlicapital repo&lt;/a&gt; helps a small project get found.&lt;/p&gt;

&lt;p&gt;Which central bank's data have you found hardest to get at? I'd like to add it.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Written with AI assistance. The figures are from the check's saved results of 15 October 2026.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>finance</category>
      <category>economics</category>
      <category>opendata</category>
      <category>api</category>
    </item>
    <item>
      <title>Europe's tagged annual reports against the same companies' SEC filings: 12 of 12</title>
      <dc:creator>Arhan Canli</dc:creator>
      <pubDate>Thu, 15 Oct 2026 15:03:53 +0000</pubDate>
      <link>https://dev.to/arhancanli/europes-tagged-annual-reports-against-the-same-companies-sec-filings-12-of-12-4ih6</link>
      <guid>https://dev.to/arhancanli/europes-tagged-annual-reports-against-the-same-companies-sec-filings-12-of-12-4ih6</guid>
      <description>&lt;p&gt;Companies listed on EU and EEA regulated markets file their annual reports in ESEF, the European Single Electronic Format, and UK-listed companies follow the UK's version of the same rule: an XHTML report with the figures tagged in XBRL. XBRL International collects the filings at &lt;a href="https://filings.xbrl.org" rel="noopener noreferrer"&gt;filings.xbrl.org&lt;/a&gt; and serves each one as xBRL-JSON. That makes about 7,400 European filers' tagged figures free to read, with no API key.&lt;/p&gt;

&lt;p&gt;I added a tool for them to canli-mcp (&lt;code&gt;european_filings&lt;/code&gt;) and wanted an independent check that it reads the right facts. Some European companies also file an annual report with the SEC on Form 20-F, tagged in the same IFRS taxonomy. That is a second filing, to a different regulator, of the same figures, so the two should agree.&lt;/p&gt;

&lt;h2&gt;
  
  
  The check
&lt;/h2&gt;

&lt;p&gt;Three companies, two years, two measures each. The ESEF side comes from &lt;code&gt;european_filings&lt;/code&gt;; the SEC side comes straight from SEC's &lt;code&gt;companyfacts&lt;/code&gt; API (the &lt;code&gt;ifrs-full&lt;/code&gt; facts on 20-F filings), which the tool never reads.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Company&lt;/th&gt;
&lt;th&gt;Year&lt;/th&gt;
&lt;th&gt;Revenue (ESEF = 20-F)&lt;/th&gt;
&lt;th&gt;Profit for the year (ESEF = 20-F)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;TotalEnergies&lt;/td&gt;
&lt;td&gt;2024&lt;/td&gt;
&lt;td&gt;USD 214,550,000,000&lt;/td&gt;
&lt;td&gt;USD 16,031,000,000&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;TotalEnergies&lt;/td&gt;
&lt;td&gt;2025&lt;/td&gt;
&lt;td&gt;USD 201,196,000,000&lt;/td&gt;
&lt;td&gt;USD 13,357,000,000&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Novo Nordisk&lt;/td&gt;
&lt;td&gt;2024&lt;/td&gt;
&lt;td&gt;DKK 290,403,000,000&lt;/td&gt;
&lt;td&gt;DKK 100,988,000,000&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Novo Nordisk&lt;/td&gt;
&lt;td&gt;2025&lt;/td&gt;
&lt;td&gt;DKK 309,064,000,000&lt;/td&gt;
&lt;td&gt;DKK 102,434,000,000&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Shell&lt;/td&gt;
&lt;td&gt;2024&lt;/td&gt;
&lt;td&gt;USD 284,312,000,000&lt;/td&gt;
&lt;td&gt;USD 16,521,000,000&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Shell&lt;/td&gt;
&lt;td&gt;2025&lt;/td&gt;
&lt;td&gt;USD 266,886,000,000&lt;/td&gt;
&lt;td&gt;USD 18,119,000,000&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;All 12 matched to the unit. "Profit for the year" is IFRS &lt;code&gt;ProfitLoss&lt;/code&gt;, which includes non-controlling interests, so it is not the same line as "income attributable to shareholders".&lt;/p&gt;

&lt;h2&gt;
  
  
  Three traps on the way
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;1. Periods end at midnight of the next day.&lt;/strong&gt; In xBRL-JSON a duration is written like &lt;code&gt;2025-01-01T00:00:00/2026-01-01T00:00:00&lt;/code&gt;. That is calendar 2025, not a period that reaches into 2026. A balance-sheet instant written &lt;code&gt;2026-01-01T00:00:00&lt;/code&gt; is the position at the end of 31 December 2025. Read the dates naively and every year-end balance lands in the wrong year.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. "Revenue" has more than one tag.&lt;/strong&gt; Novo Nordisk and Shell tag revenue as &lt;code&gt;ifrs-full:Revenue&lt;/code&gt;. TotalEnergies uses &lt;code&gt;ifrs-full:RevenueFromContractsWithCustomers&lt;/code&gt;, and so does ASML. A tool that reads one concept quietly reports "no revenue" for half the companies. It needs a list per measure, and it should say which concept each figure came from.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Names match more than one filer.&lt;/strong&gt; "Novo Nordisk" also matches Novo Nordisk Fonden, the foundation that controls the company and files its own report. Legal forms ("A/S", "N.V.", "SE", "plc") have to be ignored, and the company has to rank above near-namesakes, or the first answer is the wrong entity. The tool lists the other matches, so a mismatch is visible.&lt;/p&gt;

&lt;h2&gt;
  
  
  What this does and doesn't show
&lt;/h2&gt;

&lt;p&gt;It shows that the tool reads the totals the companies tagged, on the right periods and in their own currencies. It does not show that ESEF data is clean in general: these are three large, careful filers, and only two measures in two years. Smaller filers are another matter, and segment figures aren't read from these reports at all.&lt;/p&gt;

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

&lt;p&gt;canli-mcp is a free MCP server (MIT, no API key):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx &lt;span class="nt"&gt;-y&lt;/span&gt; canli-mcp
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then ask for a European company's annual report, or call &lt;code&gt;run_tool&lt;/code&gt; with:&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="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"european_filings"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"arguments"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"company"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Novo Nordisk"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"year"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;2025&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It returns the report's period, links to the report and its viewer, and the tagged totals with their concept, period, value, unit and precision. &lt;code&gt;words&lt;/code&gt; finds any other tagged figure, such as &lt;code&gt;"words": "net system sales"&lt;/code&gt; for ASML.&lt;/p&gt;

&lt;p&gt;The cross-check script is &lt;code&gt;mcp-markets/bench/global/crosscheck.mjs&lt;/code&gt;, with its results in &lt;code&gt;results-2026-10-15.json&lt;/code&gt; next to it:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://github.com/arhancanli/canlicapital" rel="noopener noreferrer"&gt;arhancanli/canlicapital&lt;/a&gt;:&lt;/strong&gt; every MCP server, the check and its results.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://github.com/arhancanli/alphac" rel="noopener noreferrer"&gt;arhancanli/alphac&lt;/a&gt;:&lt;/strong&gt; the strategy research engine.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If this was useful to you, a star on &lt;a href="https://github.com/arhancanli/canlicapital" rel="noopener noreferrer"&gt;the canlicapital repo&lt;/a&gt; helps a small project get found.&lt;/p&gt;

&lt;p&gt;Have you used ESEF data? I'd like to hear where it disagreed with the PDF report.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Written with AI assistance. The figures are from the check's saved results of 15 October 2026.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>finance</category>
      <category>xbrl</category>
      <category>opendata</category>
      <category>python</category>
    </item>
    <item>
      <title>Polymarket and Kalshi priced the Fed's October decision 4 points apart</title>
      <dc:creator>Arhan Canli</dc:creator>
      <pubDate>Thu, 15 Oct 2026 10:00:46 +0000</pubDate>
      <link>https://dev.to/arhancanli/polymarket-and-kalshi-priced-the-feds-october-decision-4-points-apart-3cln</link>
      <guid>https://dev.to/arhancanli/polymarket-and-kalshi-priced-the-feds-october-decision-4-points-apart-3cln</guid>
      <description>&lt;p&gt;Prediction markets usually get quoted as one number: "markets see a 75% chance the Fed holds". There are two big US venues for Fed decisions, Polymarket and Kalshi, and they don't always say the same thing.&lt;/p&gt;

&lt;p&gt;At 07:46 UTC on 15 October 2026 I pulled every open market on the Fed's next decisions from both venues' public APIs, with the &lt;code&gt;prediction_markets&lt;/code&gt; tool in canli-mcp. The raw output is saved with that timestamp, so every number below can be checked against it.&lt;/p&gt;

&lt;h2&gt;
  
  
  The October meeting (27 and 28 October)
&lt;/h2&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;Polymarket&lt;/th&gt;
&lt;th&gt;Kalshi&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;No change&lt;/td&gt;
&lt;td&gt;75.5%&lt;/td&gt;
&lt;td&gt;71.5%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Hike 25 bp&lt;/td&gt;
&lt;td&gt;24.5%&lt;/td&gt;
&lt;td&gt;27.5%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Anything else (cuts, bigger hikes)&lt;/td&gt;
&lt;td&gt;under 1% each&lt;/td&gt;
&lt;td&gt;under 1% each&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;"Probability" here is the middle of the best bid and the best ask, so it is a price, not a forecast. The quotes behind it: Polymarket "No change" 0.75 bid / 0.76 ask and "25 bps increase" 0.24 / 0.25; Kalshi "Fed maintains rate" 0.71 / 0.72 and "Hike 25bps" 0.26 / 0.29.&lt;/p&gt;

&lt;h2&gt;
  
  
  Three things I noticed
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;1. Four points apart on the same question.&lt;/strong&gt; Both books are deep. Polymarket's October event has traded about $40.5 million, and Kalshi's about 11.1 million contracts (each pays $1 if its outcome happens). The spreads are one to three cents, so the gap is bigger than the spread. The two venues differ in fees, in who is allowed to trade on them, and in their exact resolution rules, and any of those can keep prices apart. If you quote "the market", say which one.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Kalshi disagrees with itself a little.&lt;/strong&gt; Kalshi also runs "Fed funds rate after October meeting?" as a ladder of thresholds. "Above 4.00%" traded at 0.22 bid / 0.25 ask (23.5%). The target range is 3.75% to 4.00% today, so getting above 4.00% needs a hike, and the hike outcomes in the other market add up to 28% (27.5% for 25 bp plus 0.5% for more). The two quotes don't even overlap: the best ask for "Above 4.00%" (0.25) is below the best bid for "Hike 25bps" (0.26). The ladder is a much thinner market, about 0.37 million contracts traded against 11.1 million for the main October one, and I haven't checked the two contracts' exact settlement rules, so there may be a reason. If you trade these, I'd like to know what it is.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Further out, thinner books.&lt;/strong&gt; Polymarket's December market has traded about $3.5 million and January's about $0.5 million. At those sizes a single trade can move the price, so read the volume before reading the percentage.&lt;/p&gt;

&lt;h2&gt;
  
  
  Pull it yourself
&lt;/h2&gt;

&lt;p&gt;canli-mcp is a free MCP server (MIT, no API key). In any MCP client:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx &lt;span class="nt"&gt;-y&lt;/span&gt; canli-mcp
&lt;span class="c"&gt;# Claude Code&lt;/span&gt;
claude mcp add canli &lt;span class="nt"&gt;--&lt;/span&gt; npx &lt;span class="nt"&gt;-y&lt;/span&gt; canli-mcp
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then ask for "prediction markets on the fed decision", or call the tool directly through &lt;code&gt;run_tool&lt;/code&gt;:&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="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"prediction_markets"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"arguments"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"query"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"fed decision"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"limit"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each row gives the venue, event, market, outcome, probability, bid, ask, last trade, volume and closing date. Polymarket's events also come with their resolution rules, which is where differences like the one in point 2 usually start.&lt;/p&gt;

&lt;p&gt;These are traders' prices before fees and the spread, not forecasts and not investment advice, and access to each venue depends on where you live.&lt;/p&gt;

&lt;p&gt;The code is on GitHub:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://github.com/arhancanli/canlicapital" rel="noopener noreferrer"&gt;arhancanli/canlicapital&lt;/a&gt;:&lt;/strong&gt; every MCP server, including &lt;code&gt;prediction_markets&lt;/code&gt; in &lt;code&gt;mcp-markets/src/predictions.mjs&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://github.com/arhancanli/alphac" rel="noopener noreferrer"&gt;arhancanli/alphac&lt;/a&gt;:&lt;/strong&gt; the strategy research engine.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If this was useful to you, a star on &lt;a href="https://github.com/arhancanli/canlicapital" rel="noopener noreferrer"&gt;the canlicapital repo&lt;/a&gt; helps a small project get found.&lt;/p&gt;

&lt;p&gt;Do you watch both venues, or trust one? I'm curious which one people treat as the reference price.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Written with AI assistance. Every number is from the tool output saved at 07:46 UTC on 15 October 2026.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>finance</category>
      <category>economics</category>
      <category>datascience</category>
      <category>mcp</category>
    </item>
    <item>
      <title>The same SEC filing has two acceptance times, depending on which EDGAR file you read</title>
      <dc:creator>Arhan Canli</dc:creator>
      <pubDate>Mon, 12 Oct 2026 05:00:23 +0000</pubDate>
      <link>https://dev.to/arhancanli/the-same-sec-filing-has-two-acceptance-times-depending-on-which-edgar-file-you-read-371</link>
      <guid>https://dev.to/arhancanli/the-same-sec-filing-has-two-acceptance-times-depending-on-which-edgar-file-you-read-371</guid>
      <description>&lt;p&gt;If you pull filings from SEC's submissions API (&lt;code&gt;data.sec.gov/submissions/CIK##########.json&lt;/code&gt;), each one comes with an &lt;code&gt;acceptanceDateTime&lt;/code&gt; like &lt;code&gt;2026-07-14T14:30:38.000Z&lt;/code&gt;. The &lt;code&gt;Z&lt;/code&gt; says UTC. For a large share of companies that is not true, and which companies depends on the file you read, not on the filing.&lt;/p&gt;

&lt;p&gt;That matters whenever the time of day matters: whether results came out before the open or after the close, which trading day an insider purchase belongs to, or an event study's day 0.&lt;/p&gt;

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

&lt;p&gt;On 2026-10-11 I took 25 companies' submissions files: the 12 newest filings in each, plus 6 from December to February to cover standard time. That is 450 filings across 34 form types, 98 filing agents and three years.&lt;/p&gt;

&lt;p&gt;For each one I compared &lt;code&gt;acceptanceDateTime&lt;/code&gt; with the "Accepted" time printed on the filing's index page (&lt;code&gt;.../{accession}-index.htm&lt;/code&gt;). That page is in US Eastern time and matches the filing's own SGML header.&lt;/p&gt;

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

&lt;p&gt;&lt;strong&gt;Every file was either right for all its filings, or off by exactly one Eastern offset for all of them:&lt;/strong&gt; +4 hours for filings accepted in daylight time, +5 hours in standard time. No other offset appeared, whatever the form, filing agent or time of day.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Right&lt;/th&gt;
&lt;th&gt;Off by one Eastern offset&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;MSFT, GOOGL, XOM, PFE, KO, TSLA, NFLX, INTC, GE, F, O, SPY&lt;/td&gt;
&lt;td&gt;AAPL, NVDA, AMZN, META, JPM, BAC, WMT, BRK, TSM, GME, PLTR, MRNA, Vanguard&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;So JPMorgan's results 8-K, accepted at 06:30 Eastern (10:30 UTC), shows up as 14:30Z: after the open instead of before it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The same filing reads differently in two files.&lt;/strong&gt; A Form 4 sits in both the company's file and the insider's own file. Apple's director Form 4s from 26 February:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Accession&lt;/th&gt;
&lt;th&gt;Apple's file&lt;/th&gt;
&lt;th&gt;Director's file&lt;/th&gt;
&lt;th&gt;Index page (Eastern)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;0001059235-26-000004&lt;/td&gt;
&lt;td&gt;2026-02-27T04:34:19Z&lt;/td&gt;
&lt;td&gt;2026-02-26T23:34:19Z&lt;/td&gt;
&lt;td&gt;2026-02-26 18:34:19&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;0001216519-26-000004&lt;/td&gt;
&lt;td&gt;2026-02-27T04:33:49Z&lt;/td&gt;
&lt;td&gt;2026-02-26T23:33:49Z&lt;/td&gt;
&lt;td&gt;2026-02-26 18:33:49&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The director's files are right; Apple's file is five hours late (February is standard time). You can check it yourself with the standard library:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;re&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;urllib&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;
&lt;span class="n"&gt;UA&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;User-Agent&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Your Name you@example.com&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;  &lt;span class="c1"&gt;# SEC asks for a contact
&lt;/span&gt;&lt;span class="n"&gt;get&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;lambda&lt;/span&gt; &lt;span class="n"&gt;u&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;urllib&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;urlopen&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;urllib&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Request&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;u&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;UA&lt;/span&gt;&lt;span class="p"&gt;)).&lt;/span&gt;&lt;span class="nf"&gt;read&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;decode&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="n"&gt;acc&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;0001059235-26-000004&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;cik&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;320193&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1059235&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;  &lt;span class="c1"&gt;# Apple's file, then the director's
&lt;/span&gt;    &lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;loads&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://data.sec.gov/submissions/CIK&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;cik&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;010&lt;/span&gt;&lt;span class="n"&gt;d&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;.json&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;))[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;filings&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;recent&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cik&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;acceptanceDateTime&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;accessionNumber&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;index&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;acc&lt;/span&gt;&lt;span class="p"&gt;)])&lt;/span&gt;
&lt;span class="n"&gt;html&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://www.sec.gov/Archives/edgar/data/320193/&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;acc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;-&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;''&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;/&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;acc&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;-index.htm&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;index:&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;re&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;search&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;r&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;Accepted&amp;lt;/div&amp;gt;\s*&amp;lt;div class=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;info&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;&amp;gt;([^&amp;lt;]+)&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;html&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;group&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;These are the values on 11 October. The files change, which is the next point.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The split follows each file's newest filing.&lt;/strong&gt; Every file whose newest filing was accepted on or before 2 October was right. Every file whose newest filing was accepted on or after 5 October was off, including its filings from February. My guess is that files rebuilt since early October are written with the extra offset. I can't see rebuild times (the API sends no Last-Modified header), so that is a hypothesis, not a finding.&lt;/p&gt;

&lt;p&gt;I wasn't the first to notice. An edgartools issue (&lt;a href="https://github.com/dgunning/edgartools/issues/1501" rel="noopener noreferrer"&gt;dgunning/edgartools#1501&lt;/a&gt;) reported Apple four hours late on 8 October, and on 7 October some files that gave Eastern time labelled as UTC. I added the data above there.&lt;/p&gt;

&lt;h2&gt;
  
  
  What to do about it
&lt;/h2&gt;

&lt;p&gt;Read the time from the index page or the SGML header when it matters. That costs one request per filing.&lt;/p&gt;

&lt;p&gt;Because a file is uniform, one request per file is enough. Measure the file's error on one filing, then correct the rest:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;datetime&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;datetime&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;timedelta&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;zoneinfo&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;ZoneInfo&lt;/span&gt;

&lt;span class="n"&gt;ET&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;ZoneInfo&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;America/New_York&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;file_shift&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;api_iso&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;accepted_et&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;A submissions file&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;s error in Eastern offsets (-1, 0 or 1), from one filing&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;s index-page time.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="n"&gt;api&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;datetime&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fromisoformat&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;api_iso&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Z&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;+00:00&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="n"&gt;true&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;datetime&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;strptime&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;accepted_et&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;%Y-%m-%d %H:%M:%S&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;tzinfo&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;ET&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;k&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;api&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;true&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;true&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;utcoffset&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;  &lt;span class="c1"&gt;# the offset is 4 h (EDT) or 5 h (EST)
&lt;/span&gt;    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;k&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;ValueError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;unexpected shift &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;k&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;int&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;k&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;corrected&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;api_iso&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;k&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;The true acceptance time of any filing in a file whose shift is k.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="n"&gt;api&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;datetime&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fromisoformat&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;api_iso&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Z&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;+00:00&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;k&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;return&lt;/span&gt; &lt;span class="n"&gt;api&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;h&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;t&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;api&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;k&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nf"&gt;timedelta&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;hours&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;h&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;astimezone&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ET&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;utcoffset&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="nf"&gt;timedelta&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;hours&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;h&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;t&lt;/span&gt;

&lt;span class="n"&gt;k&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;file_shift&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;2026-02-27T04:34:19.000Z&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;2026-02-26 18:34:19&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="c1"&gt;# Apple's file: 1
&lt;/span&gt;&lt;span class="nf"&gt;corrected&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;2026-07-14T14:30:38.000Z&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;k&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="c1"&gt;# a 06:30 ET filing: 10:30:38 UTC
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Calibrate on two filings from different seasons, and read every filing from its own index page if they disagree. Recalibrate whenever you re-download the file, because a rebuild can change it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where it bit me
&lt;/h2&gt;

&lt;p&gt;canli-mcp's &lt;code&gt;earnings_calendar&lt;/code&gt; already read times from the index pages. Its &lt;code&gt;event_study&lt;/code&gt; did not: it took &lt;code&gt;acceptanceDateTime&lt;/code&gt; to decide whether a filing came before or after the 16:00 close.&lt;/p&gt;

&lt;p&gt;With today's files the damage is small. Only afternoon filings move to the next day: 1 of 31 JPMorgan 8-Ks since 2025, and none of 55 NVIDIA insider sales. A file that gives Eastern time labelled UTC would be worse: every results release after the close would land on its own day. &lt;code&gt;event_study&lt;/code&gt; now calibrates each file this way, with two extra requests, and says in its result which it did.&lt;/p&gt;

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

&lt;p&gt;canli-mcp is a free, MIT-licensed MCP server for finance: 306 tools behind three, reading SEC filings, insider trades, 13F and 13D/G holdings, options, FOMC and macro data straight from the sources, with no API key for almost everything.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx &lt;span class="nt"&gt;-y&lt;/span&gt; canli-mcp

&lt;span class="c"&gt;# Claude Code&lt;/span&gt;
claude mcp add canli &lt;span class="nt"&gt;--&lt;/span&gt; npx &lt;span class="nt"&gt;-y&lt;/span&gt; canli-mcp
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The code is on GitHub:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://github.com/arhancanli/canlicapital" rel="noopener noreferrer"&gt;arhancanli/canlicapital&lt;/a&gt;:&lt;/strong&gt; every MCP server, including the fix (&lt;code&gt;acceptanceTimes&lt;/code&gt; in &lt;code&gt;mcp-markets/src/sources.mjs&lt;/code&gt;), the benchmarks and the site.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://github.com/arhancanli/alphac" rel="noopener noreferrer"&gt;arhancanli/alphac&lt;/a&gt;:&lt;/strong&gt; the strategy research engine.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you have seen other EDGAR fields that don't mean what they say, I'd like to hear about them.&lt;/p&gt;

</description>
      <category>python</category>
      <category>finance</category>
      <category>datascience</category>
      <category>api</category>
    </item>
    <item>
      <title>"silver price" returned US CPI: plain-word lookups that answer the wrong question</title>
      <dc:creator>Arhan Canli</dc:creator>
      <pubDate>Sun, 11 Oct 2026 15:00:22 +0000</pubDate>
      <link>https://dev.to/arhancanli/silver-price-returned-us-cpi-plain-word-lookups-that-answer-the-wrong-question-l16</link>
      <guid>https://dev.to/arhancanli/silver-price-returned-us-cpi-plain-word-lookups-that-answer-the-wrong-question-l16</guid>
      <description>&lt;p&gt;Agents rarely pass the exact ID a data source wants. They pass words: "10 year treasury yield", "core inflation", "silver price". So most tools that wrap a data source have a small resolver that turns words into an ID. Mine had a bug that never raised an error.&lt;/p&gt;

&lt;h2&gt;
  
  
  The bug
&lt;/h2&gt;

&lt;p&gt;In canli-markets-mcp 0.1.0 (now one of the packs in canli-mcp), &lt;code&gt;economic_series&lt;/code&gt; took a FRED series ID or plain words. The plain-word path scored each of 47 common series by the share of the request's words found in its description, and took the best one if at least half matched:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;words&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toLowerCase&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;match&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="se"&gt;[&lt;/span&gt;&lt;span class="sr"&gt;a-z0-9&amp;amp;&lt;/span&gt;&lt;span class="se"&gt;]&lt;/span&gt;&lt;span class="sr"&gt;+/g&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;scored&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;COMMON_SERIES&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;(([&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;text&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;words&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;w&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;text&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;split&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt; &lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;includes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;w&lt;/span&gt;&lt;span class="p"&gt;)).&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="nx"&gt;words&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sort&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;b&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;b&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="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="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;
&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;scored&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;scored&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="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mf"&gt;0.5&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;NotFound&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="cm"&gt;/* ... */&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;scored&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="mi"&gt;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;A two-word request needs only one shared word to reach half. CPI's description is "cpi consumer price index inflation all items", so "price" alone was enough. Here is the 0.1.0 resolver next to the current one, on the same requests:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Request&lt;/th&gt;
&lt;th&gt;0.1.0 returned&lt;/th&gt;
&lt;th&gt;Now&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;silver price&lt;/td&gt;
&lt;td&gt;CPIAUCSL (US CPI)&lt;/td&gt;
&lt;td&gt;refused, pointing to &lt;code&gt;price_history&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;copper price&lt;/td&gt;
&lt;td&gt;CPIAUCSL&lt;/td&gt;
&lt;td&gt;refused&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;bitcoin price&lt;/td&gt;
&lt;td&gt;CPIAUCSL&lt;/td&gt;
&lt;td&gt;refused&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;japan inflation&lt;/td&gt;
&lt;td&gt;CPIAUCSL&lt;/td&gt;
&lt;td&gt;refused, pointing to &lt;code&gt;world_economy&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;uk unemployment rate&lt;/td&gt;
&lt;td&gt;UNRATE (US unemployment)&lt;/td&gt;
&lt;td&gt;refused&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;wage growth&lt;/td&gt;
&lt;td&gt;A191RL1Q225SBEA (real GDP growth)&lt;/td&gt;
&lt;td&gt;CES0500000003 (average hourly earnings)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;house prices&lt;/td&gt;
&lt;td&gt;refused&lt;/td&gt;
&lt;td&gt;CSUSHPINSA (Case-Shiller)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;inflation&lt;/td&gt;
&lt;td&gt;CPIAUCSL&lt;/td&gt;
&lt;td&gt;CPIAUCSL&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;10 year treasury yield&lt;/td&gt;
&lt;td&gt;DGS10&lt;/td&gt;
&lt;td&gt;DGS10&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Every wrong answer came back as a real series with real numbers. The result did carry the series title, so the mistake was there for anyone who read it. Nothing made an agent read it.&lt;/p&gt;

&lt;p&gt;The pattern is the same in every row. The words that decide the question ("silver", "japan", "uk") are the ones that matched nothing. The words that did match ("price", "inflation", "rate") say what kind of number you want, not which one.&lt;/p&gt;

&lt;h2&gt;
  
  
  The rule that replaced it
&lt;/h2&gt;

&lt;p&gt;One function now serves three resolvers: FRED series, World Bank indicators and OECD outlook measures. The two newer ones had the same flaw before they were released: "wage growth" gave GDP growth, "house prices" gave CPI, and the OECD "exchange rate" gave the unemployment rate.&lt;/p&gt;

&lt;p&gt;Each word of a request falls into one of three groups:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Filler&lt;/strong&gt; is ignored: "what is", "show me", "since", "this year", and years such as 2020.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Kind words&lt;/strong&gt; say what sort of number: rate, growth, index, level, daily, monthly. They count toward the best match but are never required.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Specific words&lt;/strong&gt; are everything else, and every one of them must appear in the entry.
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&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;matchWords&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;entries&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;textOf&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;vocab&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;asked&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;split&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;w&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;vocab&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;filler&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;has&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;w&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="sr"&gt;/^&lt;/span&gt;&lt;span class="se"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;1&lt;/span&gt;&lt;span class="se"&gt;[&lt;/span&gt;&lt;span class="sr"&gt;89&lt;/span&gt;&lt;span class="se"&gt;]&lt;/span&gt;&lt;span class="sr"&gt;|20&lt;/span&gt;&lt;span class="se"&gt;)\d\d&lt;/span&gt;&lt;span class="sr"&gt;$/&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;test&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;w&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="sr"&gt;/^&lt;/span&gt;&lt;span class="se"&gt;[&lt;/span&gt;&lt;span class="sr"&gt;a-z&lt;/span&gt;&lt;span class="se"&gt;]&lt;/span&gt;&lt;span class="sr"&gt;$/&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;test&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;w&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;specific&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;asked&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;w&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;vocab&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;kind&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;has&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;w&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;specific&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;null&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;best&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;most&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;for &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;entry&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;entries&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;has&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;split&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;textOf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;entry&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;specific&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;every&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;w&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;has&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;has&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;w&lt;/span&gt;&lt;span class="p"&gt;)))&lt;/span&gt; &lt;span class="k"&gt;continue&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;shared&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;asked&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;w&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;has&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;has&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;w&lt;/span&gt;&lt;span class="p"&gt;)).&lt;/span&gt;&lt;span class="nx"&gt;length&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="nx"&gt;shared&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;most&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;best&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;entry&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;most&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;shared&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="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;best&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;&lt;code&gt;split&lt;/code&gt; lowercases, keeps letters, digits and &lt;code&gt;&amp;amp;&lt;/code&gt;, and folds plurals ("wages" becomes "wage"). Ties go to list order, so the more common series sits first. Each resolver adds its own words: the FRED series are all US series, so "us" and "fred" are filler there, and "interest" is a kind word.&lt;/p&gt;

&lt;p&gt;When nothing qualifies, the tool says so and says where to look:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;No common series matches "silver price". These are US series: for another country's economy use world_economy or economic_outlook, and for the price of a commodity, currency, crypto or stock use price_history ("gold" is COMEX gold futures). Or pass a FRED series id (find it at fred.stlouisfed.org).&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;An agent that gets this refusal can call &lt;code&gt;price_history&lt;/code&gt; next. An agent that gets CPI has nothing telling it to.&lt;/p&gt;

&lt;h2&gt;
  
  
  Test the refusals first
&lt;/h2&gt;

&lt;p&gt;The old tests checked that good requests found their series, and they all passed. None of them asked whether a request for something we don't have gets refused. The test file now starts with exactly that:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nf"&gt;test&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;economic_series: a request naming something no series is refuses, with where to look instead&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="o"&gt;=&amp;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;const&lt;/span&gt; &lt;span class="nx"&gt;q&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;silver price&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;copper price&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;bitcoin price&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;gold price&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;japan inflation&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;uk unemployment rate&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;euro area gdp&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;stock prices&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;interest rates&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;assert&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;throws&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;resolveSeries&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;q&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
      &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;e&lt;/span&gt; &lt;span class="k"&gt;instanceof&lt;/span&gt; &lt;span class="nx"&gt;NotFound&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="sr"&gt;/price_history/&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;test&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;message&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="sr"&gt;/world_economy/&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;test&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;message&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="nx"&gt;q&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;After it come 42 plain requests that must each resolve to one exact series (with filler, plurals and IDs inside the words), and the same two kinds of test for the World Bank and OECD resolvers.&lt;/p&gt;

&lt;p&gt;"interest rates" is refused on purpose. There are dozens of interest rates, and picking one would be the same bug in a nicer coat.&lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;A resolver from words to IDs is a search engine that returns one result. It needs a way to return none.&lt;/li&gt;
&lt;li&gt;Score on the words that tell entries apart, not on the share of words that match. Generic words are the most likely to match and the least likely to matter.&lt;/li&gt;
&lt;li&gt;Write the refusal tests before the positive ones. The positive ones pass on day one.&lt;/li&gt;
&lt;/ul&gt;

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

&lt;p&gt;canli-mcp is free and MIT-licensed, runs on your machine, and needs no API key for this (FRED, World Bank and OECD data are public):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx &lt;span class="nt"&gt;-y&lt;/span&gt; canli-mcp

&lt;span class="c"&gt;# Claude Code&lt;/span&gt;
claude mcp add canli &lt;span class="nt"&gt;--&lt;/span&gt; npx &lt;span class="nt"&gt;-y&lt;/span&gt; canli-mcp
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The code is on GitHub:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://github.com/arhancanli/canlicapital" rel="noopener noreferrer"&gt;arhancanli/canlicapital&lt;/a&gt;:&lt;/strong&gt; every MCP server, including &lt;code&gt;mcp-markets/src/words.mjs&lt;/code&gt; and &lt;code&gt;test/words.test.mjs&lt;/code&gt;, the benchmarks and the site.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://github.com/arhancanli/alphac" rel="noopener noreferrer"&gt;arhancanli/alphac&lt;/a&gt;:&lt;/strong&gt; the strategy research engine.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you find a request it resolves wrong, open an issue. That is exactly the bug I want to hear about.&lt;/p&gt;

</description>
      <category>mcp</category>
      <category>ai</category>
      <category>javascript</category>
      <category>testing</category>
    </item>
    <item>
      <title>[Boost]</title>
      <dc:creator>Arhan Canli</dc:creator>
      <pubDate>Sun, 11 Oct 2026 11:08:35 +0000</pubDate>
      <link>https://dev.to/arhancanli/-2e9</link>
      <guid>https://dev.to/arhancanli/-2e9</guid>
      <description>&lt;div class="ltag__link--embedded"&gt;
  &lt;div class="crayons-story "&gt;
  &lt;a href="https://dev.to/arhancanli/canli-mcp-306-free-finance-tools-for-claude-and-any-mcp-client-measured-against-openbb-1jpm" class="crayons-story__hidden-navigation-link"&gt;canli-mcp: 306 free finance tools for Claude and any MCP client, measured against OpenBB, EdgarTools and Yahoo&lt;/a&gt;


  &lt;div class="crayons-story__body crayons-story__body-full_post"&gt;
    &lt;div class="crayons-story__top"&gt;
      &lt;div class="crayons-story__meta"&gt;
        &lt;div class="crayons-story__author-pic"&gt;

          &lt;a href="/arhancanli" class="crayons-avatar  crayons-avatar--l  "&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%2Fuser%2Fprofile_image%2F4145298%2F97d83989-e99d-4dd8-a814-d88a50412e96.png" alt="arhancanli profile" class="crayons-avatar__image"&gt;
          &lt;/a&gt;
        &lt;/div&gt;
        &lt;div&gt;
          &lt;div&gt;
            &lt;a href="/arhancanli" class="crayons-story__secondary fw-medium m:hidden"&gt;
              Arhan Canli
            &lt;/a&gt;
            &lt;div class="profile-preview-card relative mb-4 s:mb-0 fw-medium hidden m:inline-block"&gt;
              
                Arhan Canli
                
                
              
              &lt;div id="story-author-preview-content-4832287" class="profile-preview-card__content crayons-dropdown branded-7 p-4 pt-0"&gt;
                &lt;div class="gap-4 grid"&gt;
                  &lt;div class="-mt-4"&gt;
                    &lt;a href="/arhancanli" class="flex"&gt;
                      &lt;span class="crayons-avatar crayons-avatar--xl mr-2 shrink-0"&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%2Fuser%2Fprofile_image%2F4145298%2F97d83989-e99d-4dd8-a814-d88a50412e96.png" class="crayons-avatar__image" alt=""&gt;
                      &lt;/span&gt;
                      &lt;span class="crayons-link crayons-subtitle-2 mt-5"&gt;Arhan Canli&lt;/span&gt;
                    &lt;/a&gt;
                  &lt;/div&gt;
                  &lt;div class="print-hidden"&gt;
                    
                      Follow
                    
                  &lt;/div&gt;
                  &lt;div class="author-preview-metadata-container"&gt;&lt;/div&gt;
                &lt;/div&gt;
              &lt;/div&gt;
            &lt;/div&gt;

          &lt;/div&gt;
          &lt;a href="https://dev.to/arhancanli/canli-mcp-306-free-finance-tools-for-claude-and-any-mcp-client-measured-against-openbb-1jpm" class="crayons-story__tertiary fs-xs"&gt;&lt;time&gt;Oct 11&lt;/time&gt;&lt;span class="time-ago-indicator-initial-placeholder"&gt;&lt;/span&gt;&lt;/a&gt;
        &lt;/div&gt;
      &lt;/div&gt;

    &lt;/div&gt;

    &lt;div class="crayons-story__indention"&gt;
      &lt;h2 class="crayons-story__title crayons-story__title-full_post"&gt;
        &lt;a href="https://dev.to/arhancanli/canli-mcp-306-free-finance-tools-for-claude-and-any-mcp-client-measured-against-openbb-1jpm" id="article-link-4832287"&gt;
          canli-mcp: 306 free finance tools for Claude and any MCP client, measured against OpenBB, EdgarTools and Yahoo
        &lt;/a&gt;
      &lt;/h2&gt;
        &lt;div class="crayons-story__tags"&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/mcp"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;mcp&lt;/a&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/ai"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;ai&lt;/a&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/opensource"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;opensource&lt;/a&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/finance"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;finance&lt;/a&gt;
        &lt;/div&gt;
      &lt;div class="crayons-story__bottom"&gt;
        &lt;div class="crayons-story__details"&gt;
          &lt;a href="https://dev.to/arhancanli/canli-mcp-306-free-finance-tools-for-claude-and-any-mcp-client-measured-against-openbb-1jpm" class="crayons-btn crayons-btn--s crayons-btn--ghost crayons-btn--icon-left"&gt;
            &lt;div class="multiple_reactions_aggregate"&gt;
              &lt;span class="multiple_reactions_icons_container"&gt;
                  &lt;span class="crayons_icon_container"&gt;
                    &lt;img src="https://assets.dev.to/assets/exploding-head-daceb38d627e6ae9b730f36a1e390fca556a4289d5a41abb2c35068ad3e2c4b5.svg" width="18" height="18"&gt;
                  &lt;/span&gt;
                  &lt;span class="crayons_icon_container"&gt;
                    &lt;img src="https://assets.dev.to/assets/multi-unicorn-b44d6f8c23cdd00964192bedc38af3e82463978aa611b4365bd33a0f1f4f3e97.svg" width="18" height="18"&gt;
                  &lt;/span&gt;
                  &lt;span class="crayons_icon_container"&gt;
                    &lt;img src="https://assets.dev.to/assets/sparkle-heart-5f9bee3767e18deb1bb725290cb151c25234768a0e9a2bd39370c382d02920cf.svg" width="18" height="18"&gt;
                  &lt;/span&gt;
              &lt;/span&gt;
              &lt;span class="aggregate_reactions_counter"&gt;9&lt;span class="hidden s:inline"&gt;&amp;nbsp;reactions&lt;/span&gt;&lt;/span&gt;
            &lt;/div&gt;
          &lt;/a&gt;
            &lt;a href="https://dev.to/arhancanli/canli-mcp-306-free-finance-tools-for-claude-and-any-mcp-client-measured-against-openbb-1jpm#comments" class="crayons-btn crayons-btn--s crayons-btn--ghost crayons-btn--icon-left flex items-center"&gt;
              

              11&lt;span class="hidden s:inline"&gt;&amp;nbsp;comments&lt;/span&gt;
            &lt;/a&gt;
        &lt;/div&gt;
        &lt;div class="crayons-story__save"&gt;
          &lt;small class="crayons-story__tertiary fs-xs mr-2"&gt;
            4 min read
          &lt;/small&gt;
        &lt;/div&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  &lt;/div&gt;
&lt;/div&gt;

&lt;/div&gt;


</description>
      <category>ai</category>
      <category>claude</category>
      <category>mcp</category>
      <category>tools</category>
    </item>
    <item>
      <title>canli-mcp: 306 free finance tools for Claude and any MCP client, measured against OpenBB, EdgarTools and Yahoo</title>
      <dc:creator>Arhan Canli</dc:creator>
      <pubDate>Sun, 11 Oct 2026 10:24:58 +0000</pubDate>
      <link>https://dev.to/arhancanli/canli-mcp-306-free-finance-tools-for-claude-and-any-mcp-client-measured-against-openbb-1jpm</link>
      <guid>https://dev.to/arhancanli/canli-mcp-306-free-finance-tools-for-claude-and-any-mcp-client-measured-against-openbb-1jpm</guid>
      <description>&lt;p&gt;I've been building &lt;strong&gt;canli-mcp&lt;/strong&gt;: one MCP server that puts 306 finance tools behind three. It is free, MIT-licensed, needs no API key for almost everything, and runs on your own machine.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx &lt;span class="nt"&gt;-y&lt;/span&gt; canli-mcp

&lt;span class="c"&gt;# Claude Code&lt;/span&gt;
claude mcp add canli &lt;span class="nt"&gt;--&lt;/span&gt; npx &lt;span class="nt"&gt;-y&lt;/span&gt; canli-mcp
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For Claude Desktop, Cursor or any other MCP client: &lt;code&gt;{ "mcpServers": { "canli": { "command": "npx", "args": ["-y", "canli-mcp"] } } }&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  What it does
&lt;/h2&gt;

&lt;p&gt;The model sees three tools: &lt;code&gt;find_tool&lt;/code&gt; searches all 306 by what you ask for, &lt;code&gt;describe_tool&lt;/code&gt; reads one tool's arguments, and &lt;code&gt;run_tool&lt;/code&gt; runs any tool, or up to 25 in one round trip. Behind them are seven packs:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Markets (34 tools), read from the sources themselves:&lt;/strong&gt;

&lt;ul&gt;
&lt;li&gt;SEC filings and their sections, and full-text search across every filing;&lt;/li&gt;
&lt;li&gt;ownership: insider trades (Form 4), 13F holdings, 5% holders (13D/13G), fund and ETF portfolios (N-PORT);&lt;/li&gt;
&lt;li&gt;trading: short interest and off-exchange volume (FINRA), fails to deliver, futures positioning (CFTC);&lt;/li&gt;
&lt;li&gt;company detail: executive pay against performance, revenue by segment;&lt;/li&gt;
&lt;li&gt;options: chains with IV and greeks, volatility surfaces and the VIX futures curve (Cboe's delayed data);&lt;/li&gt;
&lt;li&gt;the Fed and macro: decisions, votes and dot plots, the US economic calendar, Treasury yields and auctions, the federal debt, FRED, World Bank and OECD data;&lt;/li&gt;
&lt;li&gt;prices for stocks, currencies, futures, indices and crypto.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Quant (235 tools):&lt;/strong&gt; performance and risk, options and exotics, fixed income, portfolios, econometrics, indicators, and a library of 399 strategy sleeves. They are checked against QuantLib, statsmodels, arch, TA-Lib, scipy and pandas in 461 reference cases.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Backtest validation (17 tools):&lt;/strong&gt; deflated Sharpe, the probability of backtest overfitting, data-snooping tests, leakage checks and placebo tests, all run locally.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Also:&lt;/strong&gt; SEC fundamentals as first reported (point in time), factor backtests that refuse lookahead, Alpaca paper trading behind pre-trade checks, and an open research record.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Data results carry their source URLs, and market data carries hashes of the exact bytes received, so a figure can be checked later.&lt;/p&gt;

&lt;h2&gt;
  
  
  How it compares
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;canli-mcp&lt;/th&gt;
&lt;th&gt;OpenBB&lt;/th&gt;
&lt;th&gt;EdgarTools&lt;/th&gt;
&lt;th&gt;Yahoo Finance MCP&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Finance question types with a keyless tool (of 53)&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;49&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;39&lt;/td&gt;
&lt;td&gt;11&lt;/td&gt;
&lt;td&gt;13&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Tool definitions sent with every request&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;859 tokens&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;432,001 (2,171 in its discovery mode)&lt;/td&gt;
&lt;td&gt;3,801&lt;/td&gt;
&lt;td&gt;2,205&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The coverage map checks each server's own tool list (&lt;code&gt;bench/rivals/COVERAGE.md&lt;/code&gt; in the repo). Having a tool doesn't mean answering correctly, so there is also a head-to-head:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Setup:&lt;/strong&gt; the same model (gpt-5.4-mini), the same prompt and the same limits for every arm; only the MCP server differs.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Result:&lt;/strong&gt; on the questions every server finished, canli-mcp answered 22 of 28 runs correctly (79%). The best rival setup, EdgarTools plus Yahoo, answered 15 of 28 (54%).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Caveats:&lt;/strong&gt; we wrote the 24 questions, in areas our servers were built for. It is one small model with two runs per question. A held-out set is still to run.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The full report, with every answer, is &lt;code&gt;bench/rivals/REPORT.md&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Context is the other cost. Installed separately, the seven Canli servers send 34,508 tokens of tool definitions and instructions with every request. canli-mcp sends 1,109, measured with &lt;code&gt;bench/context.py&lt;/code&gt;. The 859 in the table counts tools in the shape OpenAI-style clients send them, the same way for every server.&lt;/p&gt;

&lt;h2&gt;
  
  
  Finding the right tool
&lt;/h2&gt;

&lt;p&gt;With 306 tools, search is the product. I wrote 48 everyday requests after the last change to the search and never tuned on them: "buy spy in my alpaca paper account", "cot report for euro futures", "executive salaries at microsoft". &lt;code&gt;find_tool&lt;/code&gt; puts the right tool first for 46 of them. The previous release managed 25.&lt;/p&gt;

&lt;p&gt;The biggest single fix was embarrassing: the stemmer turned "rates" into "rat", so "mortgage rates" never matched anything that said "rate". Prices, shares and trades broke the same way.&lt;/p&gt;

&lt;h2&gt;
  
  
  Wrong answers we caught before you did
&lt;/h2&gt;

&lt;p&gt;The most useful work this week was hunting answers that were wrong without warning:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;"silver price" returned US CPI.&lt;/strong&gt; The plain-word lookup for economic series took any series sharing half the words, and "price" is in "consumer price index". "japan inflation" returned US inflation the same way. Now every specific word must match, and a request it can't answer is refused, with where to look instead.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;"facebook" resolved to an unrelated "Denny Hill/Facebook LLC"&lt;/strong&gt; from EDGAR's entity search, and "jp morgan" to "JP Morgan AG". Names now go to the listed company first, so Google is Alphabet and Facebook is Meta. Fund managers go to the filer of their 13Fs.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Hostile inputs.&lt;/strong&gt; Every tool was called with hostile arguments: "constructor", empty and 5,000-character strings, edge numbers, 20,000-item arrays. Of the 2,980 calls that passed validation, a few showed bugs: &lt;code&gt;find_tool&lt;/code&gt; crashed on the word "constructor", and some tools returned NaN where they should refuse. All are fixed, and the sweep (&lt;code&gt;bench/robustness.mjs&lt;/code&gt;) runs before every release now.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Privacy
&lt;/h2&gt;

&lt;p&gt;Everything runs on your machine:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Where it connects:&lt;/strong&gt; only the public sources (SEC, Treasury, FRED, FINRA, CFTC, the Fed, BLS, BEA, Census, OECD, World Bank, Cboe, and Yahoo's public chart data for prices).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;What it keeps:&lt;/strong&gt; it sends nothing to us and logs nothing.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Offline:&lt;/strong&gt; &lt;code&gt;CANLI_OFFLINE=1&lt;/code&gt; keeps only the packs that use no network.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Provenance:&lt;/strong&gt; packages are published from GitHub Actions with npm provenance.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Not investment advice. Data comes as the publishers release it, Cboe's quotes are delayed, and Yahoo's chart data is unofficial.&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;npx &lt;span class="nt"&gt;-y&lt;/span&gt; canli-mcp
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then ask your assistant things like:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;"who owns more than 5% of coca cola"&lt;/li&gt;
&lt;li&gt;"did the fed raise rates and who dissented"&lt;/li&gt;
&lt;li&gt;"short interest in GME"&lt;/li&gt;
&lt;li&gt;"is my backtest overfit"&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Everything is on GitHub:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://github.com/arhancanli/canlicapital" rel="noopener noreferrer"&gt;arhancanli/canlicapital&lt;/a&gt;:&lt;/strong&gt; every MCP server, the benchmarks and the site.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://github.com/arhancanli/alphac" rel="noopener noreferrer"&gt;arhancanli/alphac&lt;/a&gt;:&lt;/strong&gt; the strategy research engine.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If something is missing or wrong, open an issue. I would much rather hear about a wrong answer than ship it quietly.&lt;/p&gt;

&lt;p&gt;Coming in the next release: earnings dates from companies' own 8-K filings, press-release headlines as news, and ready-made workflows as MCP prompts.&lt;/p&gt;

</description>
      <category>mcp</category>
      <category>ai</category>
      <category>opensource</category>
      <category>finance</category>
    </item>
    <item>
      <title>[Boost]</title>
      <dc:creator>Arhan Canli</dc:creator>
      <pubDate>Sat, 10 Oct 2026 19:23:24 +0000</pubDate>
      <link>https://dev.to/arhancanli/-1edk</link>
      <guid>https://dev.to/arhancanli/-1edk</guid>
      <description>&lt;div class="ltag__link--embedded"&gt;
  &lt;div class="crayons-story "&gt;
  &lt;a href="https://dev.to/arhancanli/my-agent-kept-dropping-numbers-when-it-copied-data-into-a-tool-call-a-file-path-fixed-it-46gf" class="crayons-story__hidden-navigation-link"&gt;My agent kept dropping numbers when it copied data into a tool call. A file path fixed it.&lt;/a&gt;


  &lt;div class="crayons-story__body crayons-story__body-full_post"&gt;
    &lt;div class="crayons-story__top"&gt;
      &lt;div class="crayons-story__meta"&gt;
        &lt;div class="crayons-story__author-pic"&gt;

          &lt;a href="/arhancanli" class="crayons-avatar  crayons-avatar--l  "&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%2Fuser%2Fprofile_image%2F4145298%2F97d83989-e99d-4dd8-a814-d88a50412e96.png" alt="arhancanli profile" class="crayons-avatar__image"&gt;
          &lt;/a&gt;
        &lt;/div&gt;
        &lt;div&gt;
          &lt;div&gt;
            &lt;a href="/arhancanli" class="crayons-story__secondary fw-medium m:hidden"&gt;
              Arhan Canli
            &lt;/a&gt;
            &lt;div class="profile-preview-card relative mb-4 s:mb-0 fw-medium hidden m:inline-block"&gt;
              
                Arhan Canli
                
                
              
              &lt;div id="story-author-preview-content-4813766" class="profile-preview-card__content crayons-dropdown branded-7 p-4 pt-0"&gt;
                &lt;div class="gap-4 grid"&gt;
                  &lt;div class="-mt-4"&gt;
                    &lt;a href="/arhancanli" class="flex"&gt;
                      &lt;span class="crayons-avatar crayons-avatar--xl mr-2 shrink-0"&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%2Fuser%2Fprofile_image%2F4145298%2F97d83989-e99d-4dd8-a814-d88a50412e96.png" class="crayons-avatar__image" alt=""&gt;
                      &lt;/span&gt;
                      &lt;span class="crayons-link crayons-subtitle-2 mt-5"&gt;Arhan Canli&lt;/span&gt;
                    &lt;/a&gt;
                  &lt;/div&gt;
                  &lt;div class="print-hidden"&gt;
                    
                      Follow
                    
                  &lt;/div&gt;
                  &lt;div class="author-preview-metadata-container"&gt;&lt;/div&gt;
                &lt;/div&gt;
              &lt;/div&gt;
            &lt;/div&gt;

          &lt;/div&gt;
          &lt;a href="https://dev.to/arhancanli/my-agent-kept-dropping-numbers-when-it-copied-data-into-a-tool-call-a-file-path-fixed-it-46gf" class="crayons-story__tertiary fs-xs"&gt;&lt;time&gt;Oct 7&lt;/time&gt;&lt;span class="time-ago-indicator-initial-placeholder"&gt;&lt;/span&gt;&lt;/a&gt;
        &lt;/div&gt;
      &lt;/div&gt;

    &lt;/div&gt;

    &lt;div class="crayons-story__indention"&gt;
      &lt;h2 class="crayons-story__title crayons-story__title-full_post"&gt;
        &lt;a href="https://dev.to/arhancanli/my-agent-kept-dropping-numbers-when-it-copied-data-into-a-tool-call-a-file-path-fixed-it-46gf" id="article-link-4813766"&gt;
          My agent kept dropping numbers when it copied data into a tool call. A file path fixed it.
        &lt;/a&gt;
      &lt;/h2&gt;
        &lt;div class="crayons-story__tags"&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/mcp"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;mcp&lt;/a&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/ai"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;ai&lt;/a&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/agents"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;agents&lt;/a&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/llm"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;llm&lt;/a&gt;
        &lt;/div&gt;
      &lt;div class="crayons-story__bottom"&gt;
        &lt;div class="crayons-story__details"&gt;
          &lt;a href="https://dev.to/arhancanli/my-agent-kept-dropping-numbers-when-it-copied-data-into-a-tool-call-a-file-path-fixed-it-46gf" class="crayons-btn crayons-btn--s crayons-btn--ghost crayons-btn--icon-left"&gt;
            &lt;div class="multiple_reactions_aggregate"&gt;
              &lt;span class="multiple_reactions_icons_container"&gt;
                  &lt;span class="crayons_icon_container"&gt;
                    &lt;img src="https://assets.dev.to/assets/exploding-head-daceb38d627e6ae9b730f36a1e390fca556a4289d5a41abb2c35068ad3e2c4b5.svg" width="18" height="18"&gt;
                  &lt;/span&gt;
                  &lt;span class="crayons_icon_container"&gt;
                    &lt;img src="https://assets.dev.to/assets/multi-unicorn-b44d6f8c23cdd00964192bedc38af3e82463978aa611b4365bd33a0f1f4f3e97.svg" width="18" height="18"&gt;
                  &lt;/span&gt;
                  &lt;span class="crayons_icon_container"&gt;
                    &lt;img src="https://assets.dev.to/assets/sparkle-heart-5f9bee3767e18deb1bb725290cb151c25234768a0e9a2bd39370c382d02920cf.svg" width="18" height="18"&gt;
                  &lt;/span&gt;
              &lt;/span&gt;
              &lt;span class="aggregate_reactions_counter"&gt;8&lt;span class="hidden s:inline"&gt;&amp;nbsp;reactions&lt;/span&gt;&lt;/span&gt;
            &lt;/div&gt;
          &lt;/a&gt;
            &lt;a href="https://dev.to/arhancanli/my-agent-kept-dropping-numbers-when-it-copied-data-into-a-tool-call-a-file-path-fixed-it-46gf#comments" class="crayons-btn crayons-btn--s crayons-btn--ghost crayons-btn--icon-left flex items-center"&gt;
              

              22&lt;span class="hidden s:inline"&gt;&amp;nbsp;comments&lt;/span&gt;
            &lt;/a&gt;
        &lt;/div&gt;
        &lt;div class="crayons-story__save"&gt;
          &lt;small class="crayons-story__tertiary fs-xs mr-2"&gt;
            3 min read
          &lt;/small&gt;
        &lt;/div&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  &lt;/div&gt;
&lt;/div&gt;

&lt;/div&gt;


</description>
      <category>agents</category>
      <category>ai</category>
      <category>debugging</category>
    </item>
    <item>
      <title>I ran 399 trading strategies on the same prices. Whether the winner was luck depended on the question.</title>
      <dc:creator>Arhan Canli</dc:creator>
      <pubDate>Thu, 08 Oct 2026 14:14:58 +0000</pubDate>
      <link>https://dev.to/arhancanli/i-ran-399-trading-strategies-on-the-same-prices-whether-the-winner-was-luck-depended-on-the-1agl</link>
      <guid>https://dev.to/arhancanli/i-ran-399-trading-strategies-on-the-same-prices-whether-the-winner-was-luck-depended-on-the-1agl</guid>
      <description>&lt;p&gt;I ran 399 trading strategies on the same price history and asked one question: is the winner skill or luck?&lt;/p&gt;

&lt;p&gt;The answer depended almost entirely on which test I used. The tests did not disagree because some were wrong. They were answering different questions, and only one of them was the question I meant.&lt;/p&gt;

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

&lt;p&gt;The 399 strategies are textbook ideas, each with its usual parameters on a plain grid:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;moving-average crossovers;&lt;/li&gt;
&lt;li&gt;time-series momentum with and without volatility targeting;&lt;/li&gt;
&lt;li&gt;channel breakouts and MACD;&lt;/li&gt;
&lt;li&gt;z-score and RSI reversion;&lt;/li&gt;
&lt;li&gt;cross-sectional momentum and dual momentum;&lt;/li&gt;
&lt;li&gt;risk parity, minimum variance and low volatility;&lt;/li&gt;
&lt;li&gt;short-term reversal, trend-filtered allocation, 52-week-high momentum and pairs.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;None was picked because it backtested well. Every one is a fixed rule.&lt;/p&gt;

&lt;p&gt;I ran them on two synthetic datasets of five assets and 1,000 days. Synthetic data lets me know the truth:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Trend:&lt;/strong&gt; asset 0 has planted trends. Its drift flips between +0.25% and -0.25% a day every 125 days, so trend-following has a real edge on it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No edge:&lt;/strong&gt; the same kind of market with no planted structure. Nothing should beat simply holding the assets.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Every strategy is scored on the same 495-day window, after the longest warm-up (504 days). Trades cost 5 basis points.&lt;/p&gt;

&lt;h2&gt;
  
  
  What each test said
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;Trend data&lt;/th&gt;
&lt;th&gt;No-edge data&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Best strategy's Sharpe&lt;/td&gt;
&lt;td&gt;4.07&lt;/td&gt;
&lt;td&gt;1.95&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Sharpe the best of 399 would show by luck alone&lt;/td&gt;
&lt;td&gt;6.21&lt;/td&gt;
&lt;td&gt;2.34&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Deflated Sharpe probability, counting all 399 tries&lt;/td&gt;
&lt;td&gt;0.001&lt;/td&gt;
&lt;td&gt;0.29&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Effective number of independent strategies&lt;/td&gt;
&lt;td&gt;4.0&lt;/td&gt;
&lt;td&gt;3.5&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Deflated Sharpe probability, counting those&lt;/td&gt;
&lt;td&gt;0.996&lt;/td&gt;
&lt;td&gt;0.954&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;White's Reality Check p-value (beats buy-and-hold?)&lt;/td&gt;
&lt;td&gt;0.011&lt;/td&gt;
&lt;td&gt;0.43&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Hansen SPA p-value, re-studentized&lt;/td&gt;
&lt;td&gt;0.017&lt;/td&gt;
&lt;td&gt;0.35&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Strategies Romano-Wolf StepM says beat buy-and-hold&lt;/td&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Probability of backtest overfitting (CSCV)&lt;/td&gt;
&lt;td&gt;0.24&lt;/td&gt;
&lt;td&gt;0.76&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Three things surprised me.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Counting all 399 tries rejected the real edge.&lt;/strong&gt; On the trend data, the best strategy's Sharpe of 4.07 is below the 6.21 that the best of 399 independent coin-flip strategies would reach. So the deflated Sharpe ratio says "luck" (0.001). But these 399 strategies are not independent. Most are near-copies of each other, and their returns behave like about four independent bets. Counted that way, the same result passes easily (0.996).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Counting four tries passed the no-edge winner.&lt;/strong&gt; On the no-edge data, the effective count gives 0.954, just over the usual 0.95 bar. That is a pass, but there was no edge.&lt;/p&gt;

&lt;p&gt;The reason took me a while. The deflated Sharpe ratio tests whether the Sharpe ratio is above zero. In a market that drifts upward, a long-only strategy's Sharpe really is above zero; it just holds the market. The question I cared about was whether it beats holding the market. The Reality Check, SPA and StepM test that, and they said no: p = 0.43 and 0.35, and zero survivors.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. The overfitting probability moved the right way on both.&lt;/strong&gt; CSCV splits the history into halves many ways, picks the in-sample winner, and checks where it ranks out of sample. On the trend data, the winner landed below the median out of sample 24% of the time. On the no-edge data it was 76%, which is worse than picking at random.&lt;/p&gt;

&lt;h2&gt;
  
  
  Walk-forward: what picking the winners was worth
&lt;/h2&gt;

&lt;p&gt;I also tested the selection process itself. Every 63 days I ranked strategies by trailing one-year Sharpe, held the top five for the next 63 days, then repeated.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Trend data:&lt;/strong&gt; the picks averaged a Sharpe of 4.69 in sample and 3.76 out of sample. They kept most of their edge.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No-edge data:&lt;/strong&gt; 2.81 in sample, 1.29 out of sample. That is below simply holding all 399 strategies equally (1.90) and below buy-and-hold (1.90).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The gap between in-sample and out-of-sample Sharpe is roughly how much of the pick's Sharpe was luck.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I do now
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Report the deflated Sharpe under both counts.&lt;/strong&gt; The raw count of strategies is the conservative bound, and the effective count is the optimistic one. If a result passes only the optimistic one, I treat it as not proven.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Ask the benchmark question separately.&lt;/strong&gt; If a strategy is long-only in a rising market, "Sharpe above zero" is nearly free. The Reality Check, SPA and StepM against buy-and-hold answer "is this better than doing nothing clever".&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Test the picking, not just the pick.&lt;/strong&gt; A walk-forward over the selection rule shows how much edge survives once you can't see the future.&lt;/li&gt;
&lt;/ol&gt;

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

&lt;p&gt;All of this ran in about half a second per dataset with an MCP server I built, which runs locally on the data you send. The tournament, the walk-forward and the 399 strategy definitions are tools in it. The generator for both datasets:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;numpy&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;np&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;market&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;seed&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;trend&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;rng&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;np&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;random&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;default_rng&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;seed&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;R&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;rng&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;normal&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="mf"&gt;0.006&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;rng&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;normal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;0.0002&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;0.009&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;trend&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;R&lt;/span&gt;&lt;span class="p"&gt;[:,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="n"&gt;np&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;repeat&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;rng&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;choice&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mf"&gt;0.0025&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;125&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nf"&gt;return &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;100&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;np&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;exp&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;np&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;cumsum&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;R&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;axis&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="nf"&gt;round&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;trend&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;no_edge&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;market&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;404&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="nf"&gt;market&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;405&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="bp"&gt;False&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then call &lt;code&gt;sleeve_tournament&lt;/code&gt; with &lt;code&gt;prices&lt;/code&gt; set to either matrix and &lt;code&gt;seed&lt;/code&gt; 7 (the bootstrap seed, so the SPA and StepM numbers repeat exactly), and &lt;code&gt;sleeve_walk_forward&lt;/code&gt; with the defaults.&lt;/p&gt;

&lt;p&gt;The package is canli-quant-mcp on npm: &lt;a href="https://www.npmjs.com/package/canli-quant-mcp" rel="noopener noreferrer"&gt;canli-quant-mcp&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Synthetic data is the only way to know the right answer in advance. On real prices you do not get to know it, which is why the tests matter.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Correction (8 October).&lt;/strong&gt; The first version of this post reported an SPA p-value of 0.002 and 24 StepM survivors on the trend data (0.21 on the no-edge data). Those came from a studentized SPA that divides every bootstrap resample by one fixed variance estimate, with a long default block. My own benchmark, the Null Zoo, measured that exact setup rejecting 13.5% of skill-less searches at a nominal 5% when returns are autocorrelated. The tool now re-estimates the variance in every resample, as Romano and Wolf recommend, uses a shorter default block, and reports White's Reality Check alongside. The table above shows the corrected numbers: the planted edge is still found, by 4 strategies instead of 24.&lt;/p&gt;

</description>
      <category>quant</category>
      <category>statistics</category>
      <category>algotrading</category>
      <category>mcp</category>
    </item>
    <item>
      <title>My agent kept dropping numbers when it copied data into a tool call. A file path fixed it.</title>
      <dc:creator>Arhan Canli</dc:creator>
      <pubDate>Wed, 07 Oct 2026 17:58:53 +0000</pubDate>
      <link>https://dev.to/arhancanli/my-agent-kept-dropping-numbers-when-it-copied-data-into-a-tool-call-a-file-path-fixed-it-46gf</link>
      <guid>https://dev.to/arhancanli/my-agent-kept-dropping-numbers-when-it-copied-data-into-a-tool-call-a-file-path-fixed-it-46gf</guid>
      <description>&lt;p&gt;I maintain an MCP server that checks backtests: give it a strategy's daily returns and it tells you, among other things, how many years of track record you'd need before the Sharpe ratio means anything.&lt;/p&gt;

&lt;p&gt;The obvious way to use it is to paste the returns into the chat and let the agent pass them to the tool. That turned out to be the worst way. Here is the measurement.&lt;/p&gt;

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

&lt;p&gt;Three questions, each needing the whole audit chain. For example: "how many years of track record does this return series need before its Sharpe beats 1.0 at 95% confidence?" The series had 400, 504 and 756 daily returns. Each question was asked 3 times of the same model (gpt-5.4-mini), so 9 runs per arm. The ground truth is the same local computation the tool runs, so an answer is either right (within 2%) or it isn't.&lt;/p&gt;

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

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Old tools, returns pasted.&lt;/strong&gt; The server as it was before the one-call audit tool existed, so the agent had to chain several validators itself.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;One-call audit tool, returns pasted.&lt;/strong&gt; The series sits in the prompt as a JSON array; the agent copies it into the tool call.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;One-call audit tool, file path.&lt;/strong&gt; The series is in a CSV; the prompt names the file and the tool reads it.&lt;/li&gt;
&lt;/ol&gt;

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

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;arm&lt;/th&gt;
&lt;th&gt;correct&lt;/th&gt;
&lt;th&gt;median tokens per task&lt;/th&gt;
&lt;th&gt;median time&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;old tools, pasted&lt;/td&gt;
&lt;td&gt;0 of 9&lt;/td&gt;
&lt;td&gt;11,180&lt;/td&gt;
&lt;td&gt;2.4 s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;audit tool, pasted&lt;/td&gt;
&lt;td&gt;4 of 9&lt;/td&gt;
&lt;td&gt;27,036&lt;/td&gt;
&lt;td&gt;10.0 s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;audit tool, file path&lt;/td&gt;
&lt;td&gt;8 of 9&lt;/td&gt;
&lt;td&gt;6,524&lt;/td&gt;
&lt;td&gt;2.4 s&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Same model, same tool, same numbers. Moving the data out of the prompt doubled the accuracy and cut the tokens by about three quarters.&lt;/p&gt;

&lt;h2&gt;
  
  
  What went wrong when it pasted
&lt;/h2&gt;

&lt;p&gt;The interesting part is &lt;em&gt;how&lt;/em&gt; the pasted runs failed. On the 756-return question, all three pasted runs gave the same wrong answer: 0.1718 years instead of 0.1452. Not three different mistakes, one identical mistake, three times.&lt;/p&gt;

&lt;p&gt;That's what you get when values go missing on the way into the call. The model is re-typing 756 numbers as output tokens. Somewhere in there it skipped some, the tool dutifully computed the right answer for the wrong series, and the agent reported it with full confidence. Nothing errored. If I hadn't had the true value, I'd have believed it.&lt;/p&gt;

&lt;p&gt;Pasting also costs. Every number is paid for twice: once as input in the prompt, once as output when the model writes the tool call. One pasted run used 76,930 tokens for a single question.&lt;/p&gt;

&lt;p&gt;The one miss in the file arm was different: the agent answered 388, which is the right answer in &lt;em&gt;observations&lt;/em&gt; (about 1.54 years × 252 trading days) reported where years were asked. A unit slip, not lost data.&lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;The audit tool takes &lt;code&gt;returns_file&lt;/code&gt; (a path to a CSV) on the local server. It reads numbers only, caps the file size, and reports errors by row position, never by echoing file content back into the context.&lt;/li&gt;
&lt;li&gt;Tool descriptions say plainly that a long series should go in as a file.&lt;/li&gt;
&lt;li&gt;The hosted version can't read your disk, so it refuses file input instead of pretending.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Limits of this test
&lt;/h2&gt;

&lt;p&gt;It's one small model, 9 runs per arm, on synthetic series. I didn't run Claude models for this one. So read it as "this failure is real and easy to hit", not "pasting loses exactly 55% accuracy". The run records and the task code are in &lt;a href="https://github.com/arhancanli/canlicapital/tree/main/mcp/bench/agent/experiments/2026-09-26-audit-file" rel="noopener noreferrer"&gt;the repo&lt;/a&gt;, if you'd like to rerun it with a different model.&lt;/p&gt;

&lt;p&gt;The general lesson I took: &lt;strong&gt;if a tool needs a lot of data, don't make the model carry it.&lt;/strong&gt; Pass a reference (a path, an ID, a URL) and let the tool fetch it. The model is good at deciding what to compute and bad at being a copy machine.&lt;/p&gt;

&lt;p&gt;Have you seen the same thing with other kinds of data, like long JSON, CSV rows or logs? I'm curious where the length threshold sits for bigger models.&lt;/p&gt;

</description>
      <category>mcp</category>
      <category>ai</category>
      <category>agents</category>
      <category>llm</category>
    </item>
    <item>
      <title>Your backtest beat a t-test. Would it beat a placebo?</title>
      <dc:creator>Arhan Canli</dc:creator>
      <pubDate>Wed, 07 Oct 2026 12:31:16 +0000</pubDate>
      <link>https://dev.to/arhancanli/your-backtest-beat-a-t-test-would-it-beat-a-placebo-1a6n</link>
      <guid>https://dev.to/arhancanli/your-backtest-beat-a-t-test-would-it-beat-a-placebo-1a6n</guid>
      <description>&lt;p&gt;You test 11 moving-average crossovers on five years of daily prices. The best one has a Sharpe of 1.2, and a t-test on its returns says p = 0.01. Is it real?&lt;/p&gt;

&lt;p&gt;We simulated that situation 300,000 times, in markets where by construction nothing can be predicted, and counted how often the usual tests said "edge". A t-test on the best rule said yes between 10.9% and 78.9% of the time, depending on the market. The usual fix for testing many rules, Bonferroni, held up in flat markets but said yes 8.0% to 10.3% of the time when the market simply drifted upward, and 58.5% to 59.7% of the time when stocks just had different average returns. A placebo test stayed between 4.6% and 5.5% everywhere, against a promised 5%.&lt;/p&gt;

&lt;p&gt;This post is about why, and how to run the placebo on your own pipeline.&lt;/p&gt;

&lt;h2&gt;
  
  
  Two things that look like skill
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;The search.&lt;/strong&gt; The best of 11 rules is the best of 11 draws. Even with no edge, the maximum is high. On flat simulated markets, an 11-rule SMA grid's best Sharpe averaged 0.30 to 0.32 with nothing to find. That is what Bonferroni and the deflated Sharpe ratio correct for, if you know how many rules you really tried.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What the market does on its own.&lt;/strong&gt; A long-only rule earns the drift of a rising market without predicting anything. A cross-sectional ranking that buys past winners earns the gap between assets with different long-run average returns, again without predicting anything: in our simulated markets where assets only differed in their constant means, a 20-stock momentum grid's best Sharpe averaged 1.27 to 1.29. A t-test on the strategy's returns asks "did it make money?", not "did it predict?", so drift and dispersion pass for skill, and dividing alpha by the number of rules does nothing about that.&lt;/p&gt;

&lt;h2&gt;
  
  
  The placebo
&lt;/h2&gt;

&lt;p&gt;Keep your data's returns, but put the days in a random order, the same order for every asset. Each asset keeps its own returns, fat tails and average; each day keeps its cross-section; but nothing about a day tells you anything about the next one. That is a placebo: a market with the same raw material and nothing to predict.&lt;/p&gt;

&lt;p&gt;Now run your whole pipeline on the real data and on 19 placebos: the same search, the same filters, the same parameter choices. If the real result beats all 19, the chance of that happening by luck is 1 in 20 (p = 0.05). If it beats 15 of them, p = 0.25. Every choice the pipeline makes is counted, because it makes it again on every placebo, and drift and dispersion are in the placebos too, so they stop looking like skill.&lt;/p&gt;

&lt;p&gt;When the days are interchangeable, this p-value is exact: the real ordering is just one of the orderings. Volatility clustering breaks that, so we measured it.&lt;/p&gt;

&lt;h2&gt;
  
  
  The study
&lt;/h2&gt;

&lt;p&gt;We wrote the rules down before running anything: three ways of reordering (a full permutation, a permutation of blocks of 11 days, and a stationary bootstrap), a bar of at most 6% false positives at a nominal 5% in every simulated market, and the rule that picks the winner. The pre-registration was committed before the run, and a test in the repository checks the run used it.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Markets with nothing to predict:&lt;/strong&gt; five return shapes (normal, fat-tailed, negatively skewed, GARCH volatility clustering, volatility regimes), each flat or drifting, or for the cross-section, with equal or dispersed average returns. 1,260 days each.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Pipelines:&lt;/strong&gt; an 11-rule SMA crossover grid, a 6-lookback time-series momentum grid, and a 4-lookback cross-sectional momentum grid over 20 assets, each reporting its best Sharpe.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;30 combinations, 10,000 tests each,&lt;/strong&gt; with 19 placebos per test, plus planted trends to measure power.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The full permutation met the bar in all 30: 4.6% to 5.5%, including GARCH and regime markets. The block permutation also met it (5.8% at worst) with less power. The stationary bootstrap was far too cautious: never above 1.5%, so it would miss real edges.&lt;/p&gt;

&lt;p&gt;How often does the placebo catch a real edge? With a planted trend that a perfect forecaster would trade at a Sharpe of about 2, the SMA grid beat all 19 placebos 45.1% of the time and the time-series grid 47.3%; at a Sharpe of about 1, both were near 14.8%. The cross-sectional grid caught its strongest planted trend 89.0% of the time. An honest caveat: with only 19 placebos, Bonferroni catches some trends more often in flat markets, where it is valid. 99 placebos give a finer p-value.&lt;/p&gt;

&lt;h2&gt;
  
  
  What it does not tell you
&lt;/h2&gt;

&lt;p&gt;Beating the placebos shows that the real order of your returns holds more than random orders of the same returns. It says nothing about costs, capacity or whether the edge lasts. And it counts only the search this pipeline runs: if you tried and dropped ten other pipelines before this one, those trials are not in the number.&lt;/p&gt;

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

&lt;p&gt;The placebo is the &lt;code&gt;placebo_test&lt;/code&gt; tool in &lt;a href="https://github.com/arhancanli/canli-validation-mcp" rel="noopener noreferrer"&gt;canli-validation-mcp&lt;/a&gt;, an open-source MCP server. &lt;code&gt;plan&lt;/code&gt; writes your price file and 19 placebo files to a temporary folder; run your pipeline on each, then &lt;code&gt;compare&lt;/code&gt; returns the p-value. It runs on your machine; your code never leaves it. In Claude Code:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;claude mcp add canli-validation &lt;span class="nt"&gt;--&lt;/span&gt; npx &lt;span class="nt"&gt;-y&lt;/span&gt; canli-validation-mcp
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The study's code, pre-registration and results are in the &lt;a href="https://github.com/arhancanli/canlicapital/tree/main/scripts/research/placebo/v1" rel="noopener noreferrer"&gt;canlicapital repository&lt;/a&gt;. If you run the placebo on a real pipeline, I would like to hear what it finds.&lt;/p&gt;

</description>
      <category>algotrading</category>
      <category>python</category>
      <category>datascience</category>
      <category>statistics</category>
    </item>
  </channel>
</rss>
