<?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: Russlan Ramdowar</title>
    <description>The latest articles on DEV Community by Russlan Ramdowar (@russlanramdowar).</description>
    <link>https://dev.to/russlanramdowar</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%2F4072293%2F96cd0902-7e7a-43c5-8755-87e152f56272.png</url>
      <title>DEV Community: Russlan Ramdowar</title>
      <link>https://dev.to/russlanramdowar</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/russlanramdowar"/>
    <language>en</language>
    <item>
      <title>Your Backtest Can Pass Every Unit Test and Still Know the Future</title>
      <dc:creator>Russlan Ramdowar</dc:creator>
      <pubDate>Mon, 05 Oct 2026 19:34:43 +0000</pubDate>
      <link>https://dev.to/russlanramdowar/your-backtest-can-pass-every-unit-test-and-still-know-the-future-4940</link>
      <guid>https://dev.to/russlanramdowar/your-backtest-can-pass-every-unit-test-and-still-know-the-future-4940</guid>
      <description>&lt;p&gt;&lt;em&gt;A two-timestamp schema and a future-corruption test can catch silent look-ahead leakage in financial feature pipelines.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;A backtest does not need an obvious bug to cheat.&lt;/p&gt;

&lt;p&gt;The feature calculations can be correct. The train/test split can be chronological. Every unit test can pass. Yet the model may still be reading financial data that was revised, restated, or simply unavailable when the historical decision was supposed to occur.&lt;/p&gt;

&lt;p&gt;This is one of the nastier properties of look-ahead leakage: the pipeline can be internally consistent while the experiment is historically impossible.&lt;/p&gt;

&lt;p&gt;The fix starts with a distinction that deserves to be explicit in every time-sensitive data model:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;When was this fact true, and when could the system actually know it?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Those are different timestamps.&lt;/p&gt;

&lt;h2&gt;
  
  
  Event time is not knowledge time
&lt;/h2&gt;

&lt;p&gt;Suppose a company reports revenue for a quarter ending June 30.&lt;/p&gt;

&lt;p&gt;At least four dates may matter:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;period_end&lt;/code&gt;: the economic period the fact describes&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;published_at&lt;/code&gt;: when the filing became publicly available&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;ingested_at&lt;/code&gt;: when your system received and processed it&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;decision_at&lt;/code&gt;: when the model supposedly made its prediction&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A naïve feature pipeline often joins only on &lt;code&gt;period_end&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt;
  &lt;span class="n"&gt;d&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;symbol&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;d&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;decision_at&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;revenue&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;decisions&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;d&lt;/span&gt;
&lt;span class="k"&gt;JOIN&lt;/span&gt; &lt;span class="n"&gt;fundamentals&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt;
  &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;d&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;symbol&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;symbol&lt;/span&gt;
 &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;period_end&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="nb"&gt;DATE&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;d&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;decision_at&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This looks chronological. It is not necessarily point-in-time correct.&lt;/p&gt;

&lt;p&gt;A quarter can end weeks before its filing becomes public. A vendor can backfill a corrected value months later. A pipeline outage can delay ingestion. Joining only on the period being described says nothing about whether the value was available to the model.&lt;/p&gt;

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

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;max(published_at, ingested_at) &amp;lt;= decision_at
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The later of publication and ingestion becomes the earliest moment the system could legitimately use the record.&lt;/p&gt;

&lt;h2&gt;
  
  
  Give every fact an availability timestamp
&lt;/h2&gt;

&lt;p&gt;A useful schema for versioned financial facts might look like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;symbol
metric
period_end
value
published_at
ingested_at
source_id
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then derive:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;available_at = max(published_at, ingested_at)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Do not overwrite an earlier observation when a new filing, amendment, or vendor revision arrives. Append another version.&lt;/p&gt;

&lt;p&gt;That provides two useful properties:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The current application can still select the newest accepted value.&lt;/li&gt;
&lt;li&gt;A historical experiment can reconstruct only the values available before its cutoff.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The &lt;a href="https://www.sec.gov/search-filings/edgar-application-programming-interfaces" rel="noopener noreferrer"&gt;SEC company-facts API&lt;/a&gt; illustrates why this distinction matters. Its XBRL records include reporting-period dates, values, accession numbers, forms and filing dates. The API is updated as submissions are disseminated, while the SEC also documents post-acceptance corrections and removals.&lt;/p&gt;

&lt;p&gt;A production pipeline should capture the highest-precision dissemination timestamp available from its source. A filing date alone may be too coarse for intraday decisions.&lt;/p&gt;

&lt;p&gt;A single “latest value” table cannot reliably reconstruct every earlier information state.&lt;/p&gt;

&lt;h2&gt;
  
  
  Build features with an as-of boundary
&lt;/h2&gt;

&lt;p&gt;The following BigQuery-style SQL is illustrative. It selects, for each decision and metric, the latest reporting period that had become available by the decision timestamp.&lt;/p&gt;

&lt;p&gt;It assumes &lt;code&gt;published_at&lt;/code&gt; and &lt;code&gt;ingested_at&lt;/code&gt; are non-null timestamps. If either may be missing, define the fallback policy explicitly instead of letting null behavior decide it silently.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;WITH&lt;/span&gt; &lt;span class="n"&gt;eligible_facts&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="k"&gt;SELECT&lt;/span&gt;
    &lt;span class="n"&gt;d&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;decision_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;d&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;symbol&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;d&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;decision_at&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;metric&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;period_end&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;published_at&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ingested_at&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;source_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;GREATEST&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;published_at&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ingested_at&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;available_at&lt;/span&gt;
  &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;decisions&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;d&lt;/span&gt;
  &lt;span class="k"&gt;JOIN&lt;/span&gt; &lt;span class="n"&gt;fundamental_fact_versions&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt;
    &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;d&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;symbol&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;symbol&lt;/span&gt;
  &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;GREATEST&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;published_at&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ingested_at&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="n"&gt;d&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;decision_at&lt;/span&gt;
&lt;span class="p"&gt;),&lt;/span&gt;
&lt;span class="n"&gt;ranked_facts&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="k"&gt;SELECT&lt;/span&gt;
    &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;ROW_NUMBER&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="n"&gt;OVER&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="k"&gt;PARTITION&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;decision_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;metric&lt;/span&gt;
      &lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;period_end&lt;/span&gt; &lt;span class="k"&gt;DESC&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;available_at&lt;/span&gt; &lt;span class="k"&gt;DESC&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;source_id&lt;/span&gt; &lt;span class="k"&gt;DESC&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;version_rank&lt;/span&gt;
  &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;eligible_facts&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;EXCEPT&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;version_rank&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;ranked_facts&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;version_rank&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The exact ranking rule depends on the metric.&lt;/p&gt;

&lt;p&gt;For some features, you want the newest fiscal period. For others, you need a fixed period, trailing window or exact vintage. The important invariant is not the ranking expression. It is that no candidate row crosses the information boundary.&lt;/p&gt;

&lt;p&gt;Pandas provides &lt;a href="https://pandas.pydata.org/docs/reference/api/pandas.merge_asof.html" rel="noopener noreferrer"&gt;&lt;code&gt;merge_asof&lt;/code&gt;&lt;/a&gt; for backward-looking time joins, but the same warning applies: the join key should represent availability, not merely the date the observation describes.&lt;/p&gt;

&lt;h2&gt;
  
  
  Add a test that poisons the future
&lt;/h2&gt;

&lt;p&gt;Timestamp filters are necessary, but they are easy to break during later refactoring.&lt;/p&gt;

&lt;p&gt;A stronger test treats the pipeline as a black box:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Calculate features at a historical cutoff.&lt;/li&gt;
&lt;li&gt;Corrupt every source value that became available after that cutoff.&lt;/li&gt;
&lt;li&gt;Calculate the features again.&lt;/li&gt;
&lt;li&gt;Assert that the output is identical.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;If changing the future alters a historical feature, some path is leaking information backward.&lt;/p&gt;

&lt;p&gt;Here is a compact Python version. It assumes the timestamp fields are timezone-aware &lt;code&gt;datetime&lt;/code&gt; objects.&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;copy&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;deepcopy&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;available_at&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;row&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;max&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;row&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;published_at&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;row&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ingested_at&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;build_features&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;rows&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;symbol&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;decision_at&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;eligible&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
        &lt;span class="n"&gt;row&lt;/span&gt;
        &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;row&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;rows&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;row&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;symbol&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;symbol&lt;/span&gt;
        &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="nf"&gt;available_at&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;row&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="n"&gt;decision_at&lt;/span&gt;
    &lt;span class="p"&gt;]&lt;/span&gt;

    &lt;span class="n"&gt;latest&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;

    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;row&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;eligible&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;metric&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;row&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;metric&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
        &lt;span class="n"&gt;rank&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;row&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;period_end&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
            &lt;span class="nf"&gt;available_at&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;row&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
            &lt;span class="n"&gt;row&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;source_id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt;

        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;metric&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;latest&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="n"&gt;rank&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;latest&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;metric&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;rank&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt;
            &lt;span class="n"&gt;latest&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;metric&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;rank&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;rank&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;value&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;row&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;value&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
            &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;metric&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;selected&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;value&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
        &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;metric&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;selected&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;latest&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;items&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;assert_no_future_dependency&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;rows&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;symbol&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;decision_at&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;baseline&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;build_features&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;rows&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;symbol&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;decision_at&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;poisoned&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;deepcopy&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;rows&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;row&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;poisoned&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nf"&gt;available_at&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;row&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;decision_at&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;row&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;value&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;__FUTURE_SENTINEL__&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;

    &lt;span class="n"&gt;repeated&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;build_features&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;poisoned&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;symbol&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;decision_at&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="n"&gt;repeated&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;baseline&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Historical features changed after future data &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;was corrupted. Possible look-ahead leakage.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is a form of metamorphic testing. You may not know the one perfect feature vector for every historical date, but you do know a relationship that must hold:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Data unavailable at the cutoff must not influence the result.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Run the test against intermediate transformations as well as the final model matrix. Otherwise, a leaky aggregate can be materialized upstream and appear harmless downstream.&lt;/p&gt;

&lt;h2&gt;
  
  
  Warehouse time travel is not enough
&lt;/h2&gt;

&lt;p&gt;Database time travel is useful, but it solves a different problem.&lt;/p&gt;

&lt;p&gt;For example, &lt;a href="https://docs.cloud.google.com/bigquery/docs/time-travel" rel="noopener noreferrer"&gt;BigQuery time travel&lt;/a&gt; retains changed or deleted table data within a configurable two-to-seven-day window, with seven days as the default. That helps with recovery and recent table reconstruction.&lt;/p&gt;

&lt;p&gt;It does not automatically tell you when an external fact became public, when your pipeline first ingested it, or which vendor revision a year-old model would have seen.&lt;/p&gt;

&lt;p&gt;System-version history and domain-level availability timestamps complement each other:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Warehouse history answers: “What did this table contain?”&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;available_at&lt;/code&gt; answers: “What was this experiment allowed to know?”&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For durable research, preserve explicit fact versions rather than depending entirely on a short infrastructure-retention window.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where the test can still miss leakage
&lt;/h2&gt;

&lt;p&gt;A passing future-corruption test is not proof that the whole backtest is clean.&lt;/p&gt;

&lt;p&gt;It will not automatically detect:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;labels calculated with future-adjusted prices;&lt;/li&gt;
&lt;li&gt;a security universe reconstructed from today’s constituents;&lt;/li&gt;
&lt;li&gt;delisted companies missing from the dataset;&lt;/li&gt;
&lt;li&gt;hyperparameters selected after repeatedly inspecting the holdout;&lt;/li&gt;
&lt;li&gt;revised macroeconomic series stored without vintage information;&lt;/li&gt;
&lt;li&gt;documents whose publication timestamps are recorded too coarsely;&lt;/li&gt;
&lt;li&gt;an upstream vendor feed that already replaced historical values.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The test is best treated as one invariant in a broader evaluation system.&lt;/p&gt;

&lt;p&gt;You still need time-ordered validation, immutable experiment records, versioned datasets, explicit universe construction, and a final holdout that is not repeatedly used for model selection.&lt;/p&gt;

&lt;h2&gt;
  
  
  The practical rule
&lt;/h2&gt;

&lt;p&gt;If a feature table has only one timestamp, ask what that timestamp means.&lt;/p&gt;

&lt;p&gt;If the answer is “the date this fact describes,” the table is probably missing the date that matters most to a backtest: when the fact became knowable.&lt;/p&gt;

&lt;p&gt;Store both. Filter on knowledge time. Then poison the future and prove that the past does not move.&lt;/p&gt;

&lt;p&gt;A backtest should not merely reproduce its calculations.&lt;/p&gt;

&lt;p&gt;It should reproduce the information constraints of the decision it claims to simulate.&lt;/p&gt;




&lt;h2&gt;
  
  
  Sources
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://www.sec.gov/search-filings/edgar-application-programming-interfaces" rel="noopener noreferrer"&gt;SEC EDGAR Application Programming Interfaces&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.sec.gov/search-filings/edgar-search-assistance/accessing-edgar-data" rel="noopener noreferrer"&gt;SEC: Accessing EDGAR Data&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://pandas.pydata.org/docs/reference/api/pandas.merge_asof.html" rel="noopener noreferrer"&gt;pandas &lt;code&gt;merge_asof&lt;/code&gt; documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.cloud.google.com/bigquery/docs/time-travel" rel="noopener noreferrer"&gt;BigQuery data retention with time travel&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;em&gt;AI-assistance disclosure: This article was developed with AI assistance for structure, drafting and code review. The author reviewed and is responsible for the technical claims, examples, code and sources.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>machinelearning</category>
      <category>datascience</category>
      <category>python</category>
    </item>
    <item>
      <title>Audit your AI forecast dataset before calling it a benchmark</title>
      <dc:creator>Russlan Ramdowar</dc:creator>
      <pubDate>Sun, 04 Oct 2026 10:12:41 +0000</pubDate>
      <link>https://dev.to/futureedgegroup/audit-your-ai-forecast-dataset-before-calling-it-a-benchmark-29f</link>
      <guid>https://dev.to/futureedgegroup/audit-your-ai-forecast-dataset-before-calling-it-a-benchmark-29f</guid>
      <description>&lt;p&gt;An AI consensus dataset is easy to mistake for a benchmark. It has scores, multiple advisor perspectives, forecast horizons, and enough rows to make a chart look convincing.&lt;/p&gt;

&lt;p&gt;Before asking which forecast performed best, I ask a more basic engineering question: &lt;strong&gt;what does one row represent, and what evidence does it actually contain?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Here is a small audit of a public release from &lt;a href="https://ipulseai.com" rel="noopener noreferrer"&gt;iPulse AI&lt;/a&gt;, the Open Agentic Investment Research Platform we build at Future Edge Group. You can reproduce it without a production account or private market data.&lt;/p&gt;

&lt;h2&gt;
  
  
  Start with the unit of observation
&lt;/h2&gt;

&lt;p&gt;On October 4, 2026, I audited all &lt;strong&gt;746 records&lt;/strong&gt; in our &lt;a href="https://huggingface.co/datasets/future-edge-group/ipulse-ai-historical-consensus-snapshots" rel="noopener noreferrer"&gt;historical consensus snapshot dataset&lt;/a&gt;. Each row describes an asset's stored consensus output for a particular snapshot and forecast horizon. It is not an individual advisor forecast, a realized return, or an independent trading experiment.&lt;/p&gt;

&lt;p&gt;The audit found:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Check&lt;/th&gt;
&lt;th&gt;Result&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Records / distinct record IDs&lt;/td&gt;
&lt;td&gt;746 / 746&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;One-year / five-year records&lt;/td&gt;
&lt;td&gt;373 / 373&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Missing values in the six required metadata fields checked below&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Records with &lt;code&gt;model_count = 1&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;746&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Records marked &lt;code&gt;not_applicable_not_evaluated&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;746&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The last two rows matter more than a polished leaderboard. The release records forecasts, but it explicitly does not supply a completed performance evaluation. And its model count tells us to be careful about treating different advisor perspectives as independent models.&lt;/p&gt;

&lt;p&gt;This is a bounded metadata audit of one public release. It does not validate its source data, forecast quality, or every field in its schema.&lt;/p&gt;

&lt;h2&gt;
  
  
  Run the audit before plotting the scores
&lt;/h2&gt;

&lt;p&gt;This Python example uses only the standard library and the public Hugging Face viewer API. It reads the &lt;code&gt;snapshots&lt;/code&gt; configuration's &lt;code&gt;train&lt;/code&gt; split in pages. Here, &lt;code&gt;train&lt;/code&gt; is the dataset split name; it does not establish that a model was trained on these records.&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="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;collections&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Counter&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;urllib.parse&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;urlencode&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;urllib.request&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;urlopen&lt;/span&gt;

&lt;span class="n"&gt;query&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;dataset&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;future-edge-group/ipulse-ai-historical-consensus-snapshots&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;config&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;snapshots&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;split&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;train&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="n"&gt;rows&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;offset&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[],&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;

&lt;span class="k"&gt;while&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;params&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="o"&gt;**&lt;/span&gt;&lt;span class="n"&gt;query&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;offset&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;offset&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;length&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="n"&gt;url&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://datasets-server.huggingface.co/rows?&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nf"&gt;urlencode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;with&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;url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;timeout&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;30&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;page&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;load&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;rows&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;extend&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;item&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;row&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;item&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;page&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;rows&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
    &lt;span class="n"&gt;offset&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;page&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;rows&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;offset&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="n"&gt;page&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;num_rows_total&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt;
        &lt;span class="k"&gt;break&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;page&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;rows&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt;
        &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;RuntimeError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Incomplete pagination&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;required&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;record_id&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;snapshot_id&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;forecast_horizon&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;scoring_completed_at_utc&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;source_algorithm_version&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;record_checksum&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="nf"&gt;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;rows:&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;rows&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;span class="nf"&gt;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;distinct IDs:&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="n"&gt;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;record_id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;rows&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;missing metadata:&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;field&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;sum&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;field&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;None&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="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;rows&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;field&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;required&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;horizons:&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;Counter&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;forecast_horizon&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;rows&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;model counts:&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;Counter&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;model_count&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;rows&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;evaluation status:&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;Counter&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;evaluation_methodology_version&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;rows&lt;/span&gt;
&lt;span class="p"&gt;))&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The API serves the current public release. Record your execution time, repository revision, and downloaded file digest when doing a formal study; today's row count is not a promise about the next release. This example checks that checksum fields exist. It does &lt;strong&gt;not&lt;/strong&gt; recompute or verify them.&lt;/p&gt;

&lt;h2&gt;
  
  
  Distinct voices do not establish independent evidence
&lt;/h2&gt;

&lt;p&gt;An advisor persona can ask a useful different question: one perspective might emphasize business quality while another emphasizes downside risks. That diversity can improve the review process.&lt;/p&gt;

&lt;p&gt;It does not establish statistical independence. Shared models, evidence, prompts, and aggregation rules can create correlated errors. Counting perspectives is therefore a different operation from measuring error correlation against realized outcomes.&lt;/p&gt;

&lt;p&gt;For a future comparison, I would declare the experimental unit first: model configuration, advisor configuration, asset-horizon pair, or publication snapshot. Then I would preserve those identities through the evaluation rather than treating every displayed opinion as another independent observation.&lt;/p&gt;

&lt;p&gt;A consensus score also needs its own interpretation. Agreement is useful metadata; it is not automatically a calibrated probability that a forecast will be correct.&lt;/p&gt;

&lt;h2&gt;
  
  
  Keep three layers separate
&lt;/h2&gt;

&lt;p&gt;My preferred design has three explicit layers:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Recorded output:&lt;/strong&gt; the forecast, its identity, horizon, versions, and publication provenance.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Evaluation inputs:&lt;/strong&gt; observed outcomes, data source, observation window, and adjustment rules.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Evaluation result:&lt;/strong&gt; the metric, baseline, eligible population, exclusions, and evaluation version.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;For example, a one-year forecast that has not reached its declared endpoint should not silently become a failed forecast or disappear from a table. Record it as unresolved under a stated policy. A shorter interim comparison can be useful, but it answers a different question and needs a different label.&lt;/p&gt;

&lt;p&gt;Likewise, a financial-health field that does not apply to an asset category should not be converted to zero merely to simplify a chart. Missing, inapplicable, and measured zero are different states.&lt;/p&gt;

&lt;p&gt;Before reporting performance, specify the horizon, available-information cutoff, outcome definition, baseline, duplicate policy, and treatment of unresolved records. Those choices belong in the evaluation contract before seeing the result.&lt;/p&gt;

&lt;h2&gt;
  
  
  Preserve provenance without overstating it
&lt;/h2&gt;

&lt;p&gt;We also work on &lt;a href="https://forecastlibrary.com" rel="noopener noreferrer"&gt;Forecast Library&lt;/a&gt; and the public &lt;a href="https://github.com/TheFutureEdge/open-forecast-receipt" rel="noopener noreferrer"&gt;Open Forecast Receipt specification and verifier&lt;/a&gt;. These address the record-preservation side of the problem: making forecast records portable and inspectable.&lt;/p&gt;

&lt;p&gt;A matching digest can help detect a changed artifact. It cannot establish that the forecast was insightful, the inputs were correct, or the record was published before the event. A retrospective receipt needs an explicit retrospective label; stronger timing claims require separate evidence.&lt;/p&gt;

&lt;p&gt;That separation is deliberate. A trustworthy research workflow should let a reader verify a recorded claim, inspect its origin, and evaluate its outcome as distinct questions.&lt;/p&gt;

&lt;p&gt;For developers working on forecasting or agent evaluation: &lt;strong&gt;what do you use as your experimental unit when several advisor personas share one underlying model?&lt;/strong&gt; I'd be interested in approaches that preserve useful perspectives without inflating the apparent sample size.&lt;/p&gt;

</description>
      <category>python</category>
      <category>ai</category>
      <category>datascience</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>Designing AI research reports that people can actually review</title>
      <dc:creator>Russlan Ramdowar</dc:creator>
      <pubDate>Sat, 03 Oct 2026 10:06:36 +0000</pubDate>
      <link>https://dev.to/russlanramdowar/designing-ai-research-reports-that-people-can-actually-review-2c92</link>
      <guid>https://dev.to/russlanramdowar/designing-ai-research-reports-that-people-can-actually-review-2c92</guid>
      <description>&lt;p&gt;An AI research report can be fluent and still be difficult to review. A reader sees a conclusion, but cannot tell which facts support it, which assumptions drive it, or what would make it wrong.&lt;/p&gt;

&lt;p&gt;I am Russlan Ramdowar, founder of Future Edge Group and builder of &lt;a href="https://ipulseai.com" rel="noopener noreferrer"&gt;iPulse AI&lt;/a&gt;, an Open Agentic Investment Research Platform. This is a design note about making research inspectable. The examples below are illustrative design patterns, not a claim that every field is implemented in our production system.&lt;/p&gt;

&lt;h2&gt;
  
  
  Start with a review contract
&lt;/h2&gt;

&lt;p&gt;Before choosing agents or writing prompts, define what a reviewer needs to see. A compact research view should distinguish:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Observations: what a source actually says, with its timestamp.&lt;/li&gt;
&lt;li&gt;Interpretations: what the system infers from those observations.&lt;/li&gt;
&lt;li&gt;Assumptions: conditions required for the interpretation to hold.&lt;/li&gt;
&lt;li&gt;Scenarios: plausible paths, rather than one unqualified prediction.&lt;/li&gt;
&lt;li&gt;Limitations: missing data, stale evidence and unresolved disagreement.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These distinctions matter because prose tends to blend them. “Demand may rise” is an inference. A reported order figure is an observation. Keeping them separate makes it easier to challenge an argument without losing the underlying evidence.&lt;/p&gt;

&lt;h2&gt;
  
  
  Give evidence an identity
&lt;/h2&gt;

&lt;p&gt;A URL alone is often insufficient. The page may change, and its publication date may differ from the period described. A proposed evidence record could look like this:&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;"evidence_id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"example-001"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"source_url"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"https://example.org/report"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"published_at"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2026-09-30"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"observed_at"&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-10-03"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"claim_type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"observation"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"summary"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"A reported fact, with its scope made explicit"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"limitations"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"Illustrative record; not real market evidence"&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 useful property is traceability: a claim points to a specific record, and that record exposes the context needed to assess it. In a real implementation, the schema should also reflect the permissions and retention rules of the data source.&lt;/p&gt;

&lt;h2&gt;
  
  
  Preserve disagreement before summarizing it
&lt;/h2&gt;

&lt;p&gt;Independent advisor perspectives are valuable only if their differences remain visible. Several agents agreeing on a conclusion does not establish that the conclusion is correct; they may share the same missing input or assumption.&lt;/p&gt;

&lt;p&gt;A review interface can display the conclusion alongside the strongest opposing argument, the evidence each view relies on, and the assumptions that explain the difference. Consensus then becomes a summary of recorded perspectives, rather than a substitute for evidence.&lt;/p&gt;

&lt;p&gt;This is also a useful failure mode to test: if one advisor raises a material limitation, can the final summary accidentally hide it? A reviewer should be able to recover that limitation from the report.&lt;/p&gt;

&lt;h2&gt;
  
  
  Treat forecasts as dated artifacts
&lt;/h2&gt;

&lt;p&gt;A forecast is easier to evaluate when its original horizon, inputs and assumptions remain inspectable. Updating a view should create a new dated version while preserving what the earlier version said. Otherwise, a history of forecasts can quietly become a history of rewritten explanations.&lt;/p&gt;

&lt;p&gt;Evaluation should also show what failed. Selecting only successful examples gives readers little basis for understanding the system’s limits. The relevant question is what a person could have known at the time the view was produced.&lt;/p&gt;

&lt;h2&gt;
  
  
  Put openness in the user experience
&lt;/h2&gt;

&lt;p&gt;For iPulse AI, “open” refers to making research, methodology, evidence, past forecasts, evaluation, limitations and lessons inspectable. It does not automatically mean all source code, proprietary data or production infrastructure is open source.&lt;/p&gt;

&lt;p&gt;The platform brings together ranked Top Picks, asset-level forecast paths, independent AI advisor reports, risk and driver summaries, and transparent consensus scoring. The purpose is to support research and human decision-making, without guaranteeing returns or carrying out autonomous trading.&lt;/p&gt;

&lt;p&gt;For anyone building an AI research product, a practical starting point is one report with a visible claim, its evidence, its assumptions, a competing view and a dated limitation. If a reader cannot inspect those pieces, adding more agents will not solve the review problem.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;AI assistance disclosure: This article was drafted with AI assistance from approved iPulse AI positioning and design principles. The schema is an illustrative proposal, not production documentation.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
    </item>
  </channel>
</rss>
