<?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: Christian Pichichero</title>
    <description>The latest articles on DEV Community by Christian Pichichero (@tradevodata).</description>
    <link>https://dev.to/tradevodata</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%2F4017923%2Fe278d800-4f1b-4a9a-8659-027387e2544f.png</url>
      <title>DEV Community: Christian Pichichero</title>
      <link>https://dev.to/tradevodata</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/tradevodata"/>
    <language>en</language>
    <item>
      <title>The Uncertain State Between Sending and Mining</title>
      <dc:creator>Christian Pichichero</dc:creator>
      <pubDate>Thu, 24 Sep 2026 18:41:15 +0000</pubDate>
      <link>https://dev.to/tradevodata/the-uncertain-state-between-sending-and-mining-3iij</link>
      <guid>https://dev.to/tradevodata/the-uncertain-state-between-sending-and-mining-3iij</guid>
      <description>&lt;p&gt;An automated executor can make a valid decision and still have no safe action to take.&lt;/p&gt;

&lt;p&gt;The awkward interval is between deciding to submit a transaction and learning whether the chain included it. During that interval, permissions can disappear, processes can crash, RPC requests can time out, and retries can create a second execution.&lt;/p&gt;

&lt;p&gt;Treating all of this as a generic &lt;code&gt;pending&lt;/code&gt; status hides the information needed for safe recovery. A useful executor distinguishes at least three states:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;DECIDED_UNSENT -&amp;gt; SENT_UNMINED -&amp;gt; CONFIRMED | REVERTED
       |               |
       v               v
   ABANDONED        UNCERTAIN
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;UNCERTAIN&lt;/code&gt; is not another spelling of pending. It means the executor lacks enough evidence to act automatically.&lt;/p&gt;

&lt;h2&gt;
  
  
  Decided but unsent
&lt;/h2&gt;

&lt;p&gt;In this state, the executor has chosen an action and recorded that decision, but nothing has left the machine.&lt;/p&gt;

&lt;p&gt;This is the easy case operationally because abandoning the action has no chain effect. If the relevant permission disappears before submission, the executor can mark the decision abandoned and stop.&lt;/p&gt;

&lt;p&gt;Before making any external call, persist enough information to explain and reproduce the decision:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;An immutable intent ID&lt;/li&gt;
&lt;li&gt;The account and chain&lt;/li&gt;
&lt;li&gt;The exact proposed action&lt;/li&gt;
&lt;li&gt;The policy or configuration version used&lt;/li&gt;
&lt;li&gt;The decision timestamp&lt;/li&gt;
&lt;li&gt;The observed input data or a reference to it&lt;/li&gt;
&lt;li&gt;The permission state observed during evaluation&lt;/li&gt;
&lt;li&gt;A terminal reason if the intent is abandoned&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Do not overwrite an abandoned intent with a newer decision. Create another intent. An append-only history makes it possible to distinguish “we chose not to send” from “we lost track of the send.”&lt;/p&gt;

&lt;p&gt;The executor may re-check whether submission is still allowed, but that check does not create an atomic boundary across a database, signer, RPC provider, and chain. If the permission check succeeds and then the process crashes, recovery still needs to know whether a request left the machine.&lt;/p&gt;

&lt;p&gt;That requires a write-ahead submission record.&lt;/p&gt;

&lt;h2&gt;
  
  
  Persist before the network call
&lt;/h2&gt;

&lt;p&gt;Once a transaction has been constructed and signed, its raw bytes and transaction hash should be persisted before broadcasting when the signing setup permits that. The hash is derived locally, so the executor does not need an RPC response to identify the transaction.&lt;/p&gt;

&lt;p&gt;A submission record might contain:&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;"intent_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;"intent-123"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"attempt"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"status"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"submission_prepared"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"chain_id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;8453&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"nonce"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;42&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"transaction_hash"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"0x..."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"signed_payload"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"0x..."&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;If an external signing or relay service does not expose the signed payload, persist an idempotency key and provider request ID before the call. This is weaker because recovery now depends on the provider’s idempotency and lookup semantics. That limitation should be explicit in the design rather than hidden behind a retry loop.&lt;/p&gt;

&lt;p&gt;The key rule is simple: the durable record must advance before the next irreversible external interaction.&lt;/p&gt;

&lt;h2&gt;
  
  
  Sent but unmined
&lt;/h2&gt;

&lt;p&gt;A transaction is sent but unmined when the executor has evidence of submission but no inclusion receipt.&lt;/p&gt;

&lt;p&gt;The executor may observe. It may query multiple RPC providers, inspect the account nonce, rebroadcast the identical signed payload, or ask a relay service about the original request ID. It must not casually construct a fresh transaction.&lt;/p&gt;

&lt;p&gt;Rebroadcasting identical signed bytes is generally different from creating another transaction. The identical payload has the same hash and nonce. A newly built payload may have a new nonce, different fee fields, or altered call data, making a second execution possible.&lt;/p&gt;

&lt;p&gt;This distinction becomes especially important with application-level relayers. Retrying an HTTP request without an idempotency key may cause the relayer to create another transaction even if the first request merely timed out on the client side.&lt;/p&gt;

&lt;p&gt;A timeout says that the caller did not receive an answer. It does not say that the callee did nothing.&lt;/p&gt;

&lt;p&gt;While the transaction remains observable by hash, the executor can keep the state as &lt;code&gt;SENT_UNMINED&lt;/code&gt;. It should record each observation without rewriting the original submission facts.&lt;/p&gt;

&lt;h2&gt;
  
  
  Uncertain must be a first-class status
&lt;/h2&gt;

&lt;p&gt;Some failures cannot be settled automatically:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The process crashed during a remote signing or relay call.&lt;/li&gt;
&lt;li&gt;The provider accepted a request but supplied no durable identifier.&lt;/li&gt;
&lt;li&gt;The account nonce advanced, but the executor cannot identify which transaction used it.&lt;/li&gt;
&lt;li&gt;Different providers disagree about whether a transaction exists.&lt;/li&gt;
&lt;li&gt;A replacement transaction may have been created outside the executor.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Marking these cases failed is unsafe because a retry may duplicate the action. Marking them successful is dishonest because inclusion has not been established.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;UNCERTAIN&lt;/code&gt; should therefore be terminal for automatic execution. The system may continue collecting evidence, but it must not silently move the record back into the normal retry path. A user or operator can review the evidence and make a new, explicit decision.&lt;/p&gt;

&lt;p&gt;The executor also should not invent a compensating transaction. An opposite action is not an undo operation. Prices may have changed, fees have already been spent, and the relevant permission may no longer exist. Compensation is a separate intent requiring separate authorization.&lt;/p&gt;

&lt;p&gt;This is less convenient than automatic recovery. It is also more accurate. Distributed systems sometimes lose the evidence needed to distinguish “did not happen” from “happened, but the acknowledgement was lost.” A status model should preserve that fact instead of guessing.&lt;/p&gt;

&lt;p&gt;Disclosure: I build &lt;a href="https://app.tradevo.co/?utm_source=devto&amp;amp;utm_medium=syndication&amp;amp;utm_campaign=syndicate&amp;amp;utm_content=the-uncertain-state-between-sending-and-mining" rel="noopener noreferrer"&gt;Tradevo&lt;/a&gt;, software where users describe, test, paper-trade, publish, subscribe to, and automatically run crypto strategies from wallets they control.&lt;/p&gt;

&lt;p&gt;The honest executor is not the one that always keeps moving. It is the one that knows when movement would be an unsupported assumption.&lt;/p&gt;

</description>
      <category>web3</category>
      <category>architecture</category>
      <category>ethereum</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>What Regime Splits Actually Tell You (and Where the Math Runs Out)</title>
      <dc:creator>Christian Pichichero</dc:creator>
      <pubDate>Thu, 17 Sep 2026 18:34:45 +0000</pubDate>
      <link>https://dev.to/tradevodata/what-regime-splits-actually-tell-you-and-where-the-math-runs-out-1p41</link>
      <guid>https://dev.to/tradevodata/what-regime-splits-actually-tell-you-and-where-the-math-runs-out-1p41</guid>
      <description>&lt;p&gt;Regime splitting is one of the first "advanced" moves people make once they've built a backtest they trust. You have a couple hundred closed trades, an equity curve that looks fine in aggregate, and a nagging question: does this actually work in all markets, or did it just get lucky during a two-year trend? So you split the history — trending vs choppy, high volatility vs low, before some macro event vs after — and look at the four (or eight) resulting curves.&lt;/p&gt;

&lt;p&gt;This is a genuinely good instinct. It's also one of the easiest ways to produce false confidence, for a reason that has nothing to do with your strategy and everything to do with arithmetic.&lt;/p&gt;

&lt;h2&gt;
  
  
  The sample size problem, concretely
&lt;/h2&gt;

&lt;p&gt;Say you have 200 closed trades. You define a regime filter with two states — trending or choppy — using something like ADX or a moving-average slope threshold. If the market cooperated and split roughly evenly, you now have two buckets of 100. Split again by volatility (high/low), and you have four buckets of ~50. Add a third cut — say, before/after a rate-hike cycle — and you're down to sub-25 per bucket, sometimes much less, because market regimes don't obligingly divide your history into equal quarters. A trending, high-vol regime might have supplied 60 trades and a choppy, low-vol one might have supplied 8.&lt;/p&gt;

&lt;p&gt;Fifty trades is not enough to say much about a strategy's edge, and eight is not enough to say anything. Here's the shape of the problem using a rough binomial confidence interval on win rate:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;n = 200, observed win rate = 55%  -&amp;gt; 95% CI ≈ [48%, 62%]
n = 50,  observed win rate = 55%  -&amp;gt; 95% CI ≈ [41%, 69%]
n = 15,  observed win rate = 55%  -&amp;gt; 95% CI ≈ [30%, 78%]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;At n=200 you can at least argue the edge is probably real if the interval stays above 50%. At n=50 the interval already swallows a coin flip. At n=15 — a completely normal size for a "regime bucket" carved out of a modest history — the interval tells you almost nothing. You can't distinguish a strategy with a genuine 55% edge in that regime from a fair coin that happened to run hot for a couple weeks.&lt;/p&gt;

&lt;p&gt;This is the trap: the split itself feels like rigor. You went looking for a weakness, you found a bucket where performance looked worse, and you either panic and discard a strategy based on 12 trades, or rationalize the bad bucket away ("that regime is rare, doesn't matter") using the same small sample that should make you distrust the conclusion in either direction. Both moves are overconfident. The honest answer for a 12-trade bucket is usually "I don't know," which is an unsatisfying thing to put in a report, so people tend not to write it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where regime splits still earn their keep
&lt;/h2&gt;

&lt;p&gt;None of this means regime analysis is worthless. It's good for a narrower job than people use it for: sanity-checking that performance isn't concentrated in a single, unrepeatable environment — discovering, say, that 80% of your profit came from March 2020. That's a real, useful finding even from a small bucket, because you're not estimating a rate, you're checking for concentration. "Most of the P&amp;amp;L came from one regime" is a safer claim than "the strategy has a 61% win rate in choppy markets," because the first is close to arithmetic on the data you already have, and the second is an inference about data you don't.&lt;/p&gt;

&lt;p&gt;It also helps a lot to define the regime boundaries before looking at performance, not after. If you tune the ADX threshold until the choppy bucket looks bad (or good), you've turned a diagnostic into a curve-fit, and the regime split now carries the same overfitting risk as the strategy it was supposed to check.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the alternatives do differently
&lt;/h2&gt;

&lt;p&gt;Walk-forward testing sidesteps part of this by re-optimizing on a rolling window and testing out-of-sample on the next chunk, which at least keeps you honest about lookahead. It doesn't fix small-sample problems inside any single fold, though, and it's sensitive to window-length choices that are themselves a quiet form of overfitting. Purged k-fold cross-validation, borrowed from the ML side of quant research, handles leakage between adjacent samples better than a naive split — at the cost of being more work to implement correctly, and it still assumes your trades are close to independent, which they often aren't. Monte Carlo resampling of trade order or returns answers a different question again: whether your equity curve's shape depended on the sequence trades happened to occur in, rather than whether a regime subset has a real edge. It's worth running alongside a regime split, not instead of one.&lt;/p&gt;

&lt;p&gt;None of these make the sample-size problem disappear. They're different lenses on the same underlying scarcity: you only have as many independent trades as you have, and slicing them doesn't create more evidence — it redistributes the evidence you already had into smaller, noisier piles.&lt;/p&gt;

&lt;p&gt;Disclosure: I build &lt;a href="https://verify.tradevo.co/?utm_source=devto&amp;amp;utm_medium=syndication&amp;amp;utm_campaign=syndicate&amp;amp;utm_content=regime-splits-and-what-they-cannot-tell-you" rel="noopener noreferrer"&gt;Tradevo Verify&lt;/a&gt;, which runs this kind of stress-testing — including path-dependency and regime checks — against a closed-trade export and reports how much weight the evidence can actually carry, rather than tuning a bucket until it looks convincing.&lt;/p&gt;

&lt;h2&gt;
  
  
  A practical rule of thumb
&lt;/h2&gt;

&lt;p&gt;If a regime bucket has fewer than roughly 30 trades, treat any rate computed from it — win rate, average R, Sharpe — as a data point, not a conclusion, and report the count next to the number every time. And if you catch yourself narrowing a regime definition after seeing what the split produced, stop. You've left testing and entered storytelling, and the two look identical in a report until the next 50 trades arrive and disagree with you.&lt;/p&gt;

</description>
      <category>statistics</category>
      <category>trading</category>
      <category>datascience</category>
      <category>python</category>
    </item>
    <item>
      <title>Return Concentration: How to Tell If Your Backtest Has Ten Trades or One</title>
      <dc:creator>Christian Pichichero</dc:creator>
      <pubDate>Thu, 10 Sep 2026 17:53:42 +0000</pubDate>
      <link>https://dev.to/tradevodata/return-concentration-how-to-tell-if-your-backtest-has-ten-trades-or-one-48a8</link>
      <guid>https://dev.to/tradevodata/return-concentration-how-to-tell-if-your-backtest-has-ten-trades-or-one-48a8</guid>
      <description>&lt;p&gt;You've got a trade log. Two hundred closed trades, a decent win rate, a Sharpe ratio that looks respectable. Before you trust any of it, ask one question: how many of those trades actually matter?&lt;/p&gt;

&lt;p&gt;This is the return concentration problem, and it's one of the easier things to check in a backtest and one of the most commonly skipped. The idea is simple. Take your closed trades, sort them by profit, and ask what happens to the equity curve if you remove the best few. If removing five trades out of two hundred turns a profitable strategy into a flat or losing one, your two-hundred-trade backtest is actually a five-trade backtest wearing a costume.&lt;/p&gt;

&lt;h3&gt;
  
  
  The test itself
&lt;/h3&gt;

&lt;p&gt;You don't need anything sophisticated to run this. Sort trades descending by PnL, then walk down the list removing the top 1, top 3, top 5, top 10, and recompute total return each time.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;trades_sorted&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;sorted&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;trades&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="k"&gt;lambda&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;pnl&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;reverse&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;total&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;sum&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;pnl&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;trades&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;n&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;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt;
 &lt;span class="n"&gt;remaining&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;trades_sorted&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;n&lt;/span&gt;&lt;span class="p"&gt;:]&lt;/span&gt;
 &lt;span class="n"&gt;new_total&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;sum&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;pnl&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;remaining&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;n&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;new_total&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;new_total&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="n"&gt;total&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If &lt;code&gt;new_total&lt;/code&gt; goes negative or near-zero after dropping the top 5, you're looking at a strategy where almost all the reported edge lives in a handful of trades. That's not automatically damning — more on that below — but it changes what question you should be asking next.&lt;/p&gt;

&lt;p&gt;A useful companion number: what fraction of total profit comes from the top 10% of winning trades? For a strategy trading a mean-reverting, high-frequency pattern, healthy concentration might mean the top 10% of trades account for 20-30% of profit — the rest of the distribution is doing real work. For a trend-following system that holds through long drawdowns waiting for a few large moves, it's normal for the top 10% of trades to account for 60-80% of profit. Both can be fine. The number alone doesn't tell you which case you're in.&lt;/p&gt;

&lt;h3&gt;
  
  
  The trap: assuming concentration means broken
&lt;/h3&gt;

&lt;p&gt;Here's where people get it wrong in both directions. The naive read is "concentrated returns are fragile, spread returns are robust." That's not true. Trend-following, breakout, and long-volatility strategies are &lt;em&gt;supposed&lt;/em&gt; to look concentrated — they're structurally a long series of small losses funding a few large wins. If you ran the drop-top-N test on a well-known trend system's historical trade list, you'd likely find the same lottery-ticket-looking distribution, because that's the mechanism, not a flaw in it.&lt;/p&gt;

&lt;p&gt;The actual question isn't "is it concentrated," it's "is the concentration structural or accidental." Structural concentration comes from a repeatable trigger: your strategy has a rule that lets winners run and cuts losers early, and the big trades share a common cause — a volatility regime, a trend-continuation pattern, a specific setup that recurs. Accidental concentration is a few trades that happened to catch a one-off event (a short squeeze, a flash crash reversal, an earnings gap you didn't intend to hold through) that has no mechanism tying it to the rest of your trades. Same table of numbers, very different implications.&lt;/p&gt;

&lt;p&gt;A few ways to tell them apart without much tooling:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Cluster the top trades by date and instrument.&lt;/strong&gt; If your five biggest winners all fired in the same three-week window, or all on the same underlying, that's a concentration-in-time problem — you got one regime right, not an edge.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Check the exit reason.&lt;/strong&gt; If the big winners exited on the same rule as the rest of your trades (say, a trailing stop or a signal reversal), that's structural. If they exited because you happened to close the account before a reversal, or the backtest's fill logic let you ride a gap that wouldn't be tradeable live, that's a data artifact.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Look at whether the same setup that produced the big winners also produced most of your losers.&lt;/strong&gt; A strategy where the big-win trades and the many-small-loss trades share the identical entry logic is doing something coherent. If the big winners are a different distribution of setups than everything else, you may be looking at noise that got included in the same backtest by coincidence.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;None of this is definitive from a spreadsheet alone. A trade log doesn't tell you about slippage on the entries that mattered, whether the position size on the big winners was realistic to fill, or whether the same setup will recur going forward — that requires separate checks on execution and forward behavior, not just the PnL column.&lt;/p&gt;

&lt;p&gt;Disclosure: I build &lt;a href="https://verify.tradevo.co/?utm_source=devto&amp;amp;utm_medium=syndication&amp;amp;utm_campaign=syndicate&amp;amp;utm_content=return-concentration-in-a-trade-list" rel="noopener noreferrer"&gt;Tradevo Verify&lt;/a&gt;, which runs this kind of concentration check (along with execution, path-dependency, and regime tests) against a closed-trade CSV and produces a versioned record of the result — it doesn't tell you whether the strategy is good, and a concentrated result often comes back flagged as fragile evidence rather than a pass.&lt;/p&gt;

&lt;p&gt;Either way, before you trust a backtest's headline number, spend the ten minutes sorting the trade list and dropping the top few. It won't tell you if your edge is real. It will tell you how many trades you're actually betting the conclusion on.&lt;/p&gt;

</description>
      <category>statistics</category>
      <category>datascience</category>
      <category>python</category>
      <category>trading</category>
    </item>
    <item>
      <title>Allowlists Are Not Spending Caps: Two Different Security Properties, Often Confused</title>
      <dc:creator>Christian Pichichero</dc:creator>
      <pubDate>Thu, 03 Sep 2026 18:05:38 +0000</pubDate>
      <link>https://dev.to/tradevodata/allowlists-are-not-spending-caps-two-different-security-properties-often-confused-lk0</link>
      <guid>https://dev.to/tradevodata/allowlists-are-not-spending-caps-two-different-security-properties-often-confused-lk0</guid>
      <description>&lt;p&gt;If you've ever configured a delegated signer, a session key, or a bot's wallet permissions, you've probably reached for two knobs: an allowlist of assets it's allowed to touch, and a cap on how much value it can move. It's tempting to treat these as two settings on the same dial — both feel like "limits." They're not. They prevent different failure modes, and conflating them is how permission systems end up weaker than their configuration screen suggests.&lt;/p&gt;

&lt;h3&gt;
  
  
  What a spending cap actually prevents
&lt;/h3&gt;

&lt;p&gt;A spending cap bounds &lt;em&gt;magnitude&lt;/em&gt;. If a delegated key can move at most 500 USDC per day, then no matter what it does with that authority — swap, stake, send to an arbitrary address — the maximum damage from a compromised or misbehaving key is 500 USDC. It says nothing about &lt;em&gt;what&lt;/em&gt; the money touches. A cap enforced correctly stops a runaway loop or a fully compromised signer from draining a wallet in one shot. It does not stop that signer from moving the capped amount into something worthless.&lt;/p&gt;

&lt;h3&gt;
  
  
  What an allowlist actually prevents
&lt;/h3&gt;

&lt;p&gt;An allowlist bounds &lt;em&gt;scope&lt;/em&gt;. If a signer can only interact with three named token contracts, then even with no spending cap at all, it categorically cannot send funds to an arbitrary address or interact with an unknown contract. This is a different property: it's not about how much moves, it's about which counterparties are even reachable.&lt;/p&gt;

&lt;p&gt;The two compose well in theory — cap the blast radius, and separately narrow the attack surface — but each has failure modes the other doesn't cover, and "we have both" is not the same claim as "we're safe."&lt;/p&gt;

&lt;h3&gt;
  
  
  Failure mode 1: the allowlisted token isn't the code you allowlisted
&lt;/h3&gt;

&lt;p&gt;An allowlist entry is usually just an address. But an address is not fixed behavior. If the token contract sits behind a proxy — which a large fraction of tokens do, for legitimate upgrade reasons — the logic executed when your signer calls &lt;code&gt;transfer&lt;/code&gt; or &lt;code&gt;approve&lt;/code&gt; can change after you added the address to your list. You audited version 1. The proxy now points at version 2. Your allowlist still matches, because the allowlist only ever checked the address, and the address never moved. This isn't a hypothetical: it's the entire reason "audit the contract, then allowlist the address" is a weaker guarantee than people assume — the audit has a timestamp, the allowlist doesn't.&lt;/p&gt;

&lt;h3&gt;
  
  
  Failure mode 2: the router that accepts arbitrary calldata
&lt;/h3&gt;

&lt;p&gt;A lot of "swap-only" permission schemes are enforced by pointing the signer at a router contract instead of individual token contracts, on the theory that a router only does swaps. But look at what the router's function signature actually accepts. Plenty of router and aggregator interfaces expose something shaped like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;function execute(address target, bytes calldata data) external;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's not a swap function. That's a generic call forwarder that happens to usually be used for swaps. If your policy allowlists the router's address and stops there, you've allowlisted "anything the router is willing to forward," which — depending on the router's own internal checks — can include calls into arbitrary target contracts with arbitrary calldata. The allowlist has to be checked against the &lt;em&gt;target and selector inside the calldata&lt;/em&gt;, not just the outermost contract address, or it isn't restricting anything meaningful.&lt;/p&gt;

&lt;h3&gt;
  
  
  Failure mode 3: "it can only swap" is a claim about a function signature you haven't read
&lt;/h3&gt;

&lt;p&gt;This is the general version of failure mode 2. "Swap-only" is not a security property until you know exactly what parameters the swap function takes. A swap function that hard-codes the output token and the pool is a narrow, auditable action. A swap function that takes a &lt;code&gt;path: address[]&lt;/code&gt; or a &lt;code&gt;router: address&lt;/code&gt; parameter chosen by the caller is, functionally, "call arbitrary code with a legitimate-looking wrapper around it." The English sentence "it can only swap" is doing no work — the actual boundary is whatever the function's parameter types and the policy engine's validation of those parameters allow. Read the signature, not the label.&lt;/p&gt;

&lt;h3&gt;
  
  
  A practical checklist
&lt;/h3&gt;

&lt;p&gt;When someone tells you an authorization is scoped, four questions separate a real boundary from a documentation claim:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Allowlisted at what granularity — contract address, function selector, or full calldata shape?&lt;/li&gt;
&lt;li&gt;Capped on what — per-transaction, cumulative, or notional value at execution time (which can be gamed by slippage)?&lt;/li&gt;
&lt;li&gt;Enforced &lt;em&gt;where&lt;/em&gt; — checked by a policy engine before signing, checked by the contract itself on-chain, or just described in a UI with no enforcement at all?&lt;/li&gt;
&lt;li&gt;What happens when a check fails — does the transaction revert, or does the check silently pass because the policy engine and the contract disagree about what "the same asset" means?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;None of this makes allowlists useless. It makes them one layer, with a specific and narrow claim, that has to be paired with selector-level restriction and cap enforcement at the layer that actually executes the call — and it means the honest description of any "restricted signer" is a list of exactly what's checked and where, not the word "scoped" on its own.&lt;/p&gt;

&lt;p&gt;Disclosure: I build &lt;a href="https://app.tradevo.co/?utm_source=devto&amp;amp;utm_medium=syndication&amp;amp;utm_campaign=syndicate&amp;amp;utm_content=allowlists-as-a-security-boundary" rel="noopener noreferrer"&gt;Tradevo&lt;/a&gt;, which runs delegated strategy execution through a scoped session signer with a token allowlist, per-subscription allocation cap, and slippage limits — enforced by the signing provider's policy and by our own executor, not by the chain, which is exactly why I spent this piece on where enforcement actually lives instead of what the label says.&lt;/p&gt;

</description>
      <category>web3</category>
      <category>security</category>
      <category>ethereum</category>
      <category>architecture</category>
    </item>
    <item>
      <title>The Fill Model Is Where Backtests Quietly Cheat</title>
      <dc:creator>Christian Pichichero</dc:creator>
      <pubDate>Thu, 03 Sep 2026 12:54:19 +0000</pubDate>
      <link>https://dev.to/tradevodata/the-fill-model-is-where-backtests-quietly-cheat-4mhe</link>
      <guid>https://dev.to/tradevodata/the-fill-model-is-where-backtests-quietly-cheat-4mhe</guid>
      <description>&lt;p&gt;Every backtest has to answer a boring question: when the strategy says "buy," what price does it actually get? Most backtesting frameworks answer this question badly by default, and the badness is almost always in the strategy's favor.&lt;/p&gt;

&lt;p&gt;Here are the four assumptions that do the most damage, roughly in order of how often they show up.&lt;/p&gt;

&lt;h3&gt;
  
  
  Mid-price fills
&lt;/h3&gt;

&lt;p&gt;If your backtest fills orders at the midpoint of the bid-ask spread, you are assuming you trade for free. You don't. A market order pays at least half the spread to cross it; a marketable limit order pays something close to that too, once you're honest about how often it actually gets hit versus sitting unfilled while the market moves away. Mid-price fills are the single most common way a backtest manufactures edge that doesn't exist, because the effect compounds with trade frequency — a strategy that trades often looks great on mid-price fills and mediocre-to-negative once it pays the spread on every round trip.&lt;/p&gt;

&lt;h3&gt;
  
  
  Zero slippage
&lt;/h3&gt;

&lt;p&gt;Slippage is the gap between the price your signal fired at and the price your order actually executed at, and it's not just a queuing artifact — it's partly information. If your strategy is buying because something changed, other participants are reacting to the same thing, and the price you wanted is often gone by the time your order reaches the book. A backtest with zero slippage is quietly assuming the market waits for you.&lt;/p&gt;

&lt;h3&gt;
  
  
  Unlimited size at the touch
&lt;/h3&gt;

&lt;p&gt;Backtests routinely assume you can execute your full position size at the best bid or ask, no matter how large the order is relative to the visible size there. In practice, a large order walks the book, and the average fill price is worse than the touch price by an amount that depends on how thin the book is. This one is invisible until you try to size up, which is exactly when a strategy that looked fine in testing starts bleeding.&lt;/p&gt;

&lt;h3&gt;
  
  
  Commissions omitted or averaged
&lt;/h3&gt;

&lt;p&gt;Commissions and fees are usually small per trade and therefore easy to skip or fold into a rough average. But a strategy with thin per-trade edge and high turnover can have its entire expectancy eaten by costs that were treated as a rounding error.&lt;/p&gt;

&lt;h3&gt;
  
  
  A worked example
&lt;/h3&gt;

&lt;p&gt;Take a mean-reversion strategy trading a $30 stock with a 2-cent spread: average win 18¢, average loss 14¢, win rate 55%.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;expectancy (per share, before costs)
= 0.55 × 18¢ − 0.45 × 14¢
= 9.9¢ − 6.3¢
= 3.6¢
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That 3.6¢ looks like a real edge. But if the backtest filled at the mid, it never paid the spread it would pay in live trading. A more honest fill — buying near the ask, selling near the bid — costs roughly the full spread on the round trip, here about 2¢. Subtract that:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;3.6¢ − 2¢ = 1.6¢
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The edge didn't disappear, but it lost more than half its value to an assumption that never showed up as a line item anywhere in the report. Add a per-share commission and a little realistic slippage on top, and it's easy to see how a strategy with a "good" backtest turns out to be trading the spread, not an actual signal.&lt;/p&gt;

&lt;h3&gt;
  
  
  What you can actually check from a trade list
&lt;/h3&gt;

&lt;p&gt;If all you have is a CSV of closed trades — entry, exit, size, timestamps — there's a limit to how much of this you can diagnose. You can check sensitivity: rerun expectancy with a range of assumed slippage and spread costs and see how much of the edge survives. You can check whether wins are concentrated in trades with unusually favorable prices relative to the surrounding bars, which is a proxy for lookahead or mid-price fills. You can check whether performance depends on a handful of trades — if removing the best 5% of trades erases the edge, that's worth knowing regardless of the fill model.&lt;/p&gt;

&lt;p&gt;What you generally can't check from a trade list alone is anything that requires order book state: actual queue position, actual available size at the touch at the moment of the signal, actual latency between signal and order arrival. Those require tick-level or order-book data and a simulator that models the exchange mechanics, not just entry and exit prices. If someone tells you they can fully validate execution realism from a CSV of closed trades, they're skipping something — the honest version of this check is partial, and it should say so.&lt;/p&gt;

&lt;h3&gt;
  
  
  The practical test
&lt;/h3&gt;

&lt;p&gt;The cheap version of all this: take your reported average win and average loss, subtract a full spread crossing on both entry and exit, and see if the strategy still has positive expectancy. If it doesn't survive that adjustment, the edge was largely the spread it never paid, and no amount of additional testing further downstream is going to rescue it. If it does survive, you've at least confirmed the edge isn't purely a fill-model artifact — which is a different question from whether it will hold up in other ways, but it's the first one worth asking.&lt;/p&gt;

&lt;p&gt;Disclosure: I build &lt;a href="https://verify.tradevo.co/?utm_source=devto&amp;amp;utm_medium=syndication&amp;amp;utm_campaign=syndicate&amp;amp;utm_content=execution-assumptions-that-flatter-a-backtest" rel="noopener noreferrer"&gt;Tradevo Verify&lt;/a&gt;, which takes a closed-trade export and runs it through this kind of stress-testing, among other checks, and produces a versioned evidence record rather than a verdict on whether the strategy is good.&lt;/p&gt;

</description>
      <category>python</category>
      <category>trading</category>
      <category>datascience</category>
      <category>statistics</category>
    </item>
    <item>
      <title>What 'Revocable' Actually Means at the Contract Level</title>
      <dc:creator>Christian Pichichero</dc:creator>
      <pubDate>Tue, 01 Sep 2026 11:05:03 +0000</pubDate>
      <link>https://dev.to/tradevodata/what-revocable-actually-means-at-the-contract-level-5201</link>
      <guid>https://dev.to/tradevodata/what-revocable-actually-means-at-the-contract-level-5201</guid>
      <description>&lt;p&gt;If you've ever called &lt;code&gt;approve()&lt;/code&gt; on an ERC-20 token and then moved on with your life, you've already brushed up against the thing this post is about: an approval is not a setting inside some app, it's a row in a smart contract's storage, and every system built on top of it is only as honest as its last read of that row.&lt;/p&gt;

&lt;p&gt;Most token approvals work the same way. A user signs a transaction granting a spender contract permission to move up to some amount of a token from their wallet. The ERC-20 standard stores this as &lt;code&gt;allowance[owner][spender]&lt;/code&gt;. Any contract that wants to move the user's tokens checks that number before doing so, and the check happens inside the same transaction that tries to move funds — so the contract-level enforcement is real. The token contract itself will not let a transfer through if the allowance is insufficient.&lt;/p&gt;

&lt;p&gt;Revocation is just another write to that same slot, usually setting it to zero. It's a normal transaction. It has to be signed, broadcast, and mined like any other. That's the part people gloss over: revoking is not a UI toggle, it's a transaction with all the same properties as the transaction that created the approval in the first place — it sits in a mempool, it can be delayed by network congestion, and it isn't final until it's in a confirmed block.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where the gap shows up
&lt;/h2&gt;

&lt;p&gt;Say you're building a service that executes on a user's behalf using a stored approval — a trading bot, a subscription payment puller, anything with an off-chain component that decides &lt;em&gt;when&lt;/em&gt; to spend and an on-chain component that actually moves the funds. The natural design is to keep a local record: "user X has approved up to Y, active." That record is convenient. It's also just an opinion your own server holds about the world, and it can be wrong in both directions.&lt;/p&gt;

&lt;p&gt;It can be wrong stale-permissive: the user revokes, your service hasn't seen it yet, and if the on-chain check before spending is missing or weak, you build and sign a transaction anyway. Depending on how you structured the check, this either fails harmlessly at the token contract (wasted gas, a failed tx, an alert) or, if you did something sloppier — like checking your database instead of the chain — you send a transaction that the chain itself will still reject, because the allowance really is zero now. The token contract is the backstop here, which is good, but you don't want your system's normal path to depend on that backstop catching your own mistake.&lt;/p&gt;

&lt;p&gt;It can also be wrong stale-restrictive: your database says revoked, but the revoke transaction is still sitting unconfirmed, and the user expects it to be in effect immediately because that's what the button said. This one is more of a UX problem than a safety problem, but it's the same root cause — a record and the chain disagreeing about what's true right now.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why the check has to happen at the moment of execution
&lt;/h2&gt;

&lt;p&gt;The fix sounds almost too obvious to write down: before doing anything that spends a user's tokens, read the allowance from the chain, right then, not from whatever your database cached the last time you looked.&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;function&lt;/span&gt; &lt;span class="nf"&gt;attemptExecution&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;requiredAmount&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
 &lt;span class="nx"&gt;onChainAllowance&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;readAllowance&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// the ground truth, right now&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;onChainAllowance&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="nx"&gt;requiredAmount&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
 &lt;span class="nf"&gt;skipAndLog&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;allowance insufficient at execution time&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
 &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
 &lt;span class="nf"&gt;sendExecutionTx&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;requiredAmount&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;This has real costs. Every execution now needs an RPC round-trip before it can act, which adds latency and adds load on whatever node provider you're using. If you're executing for many users on a schedule, that's a lot of extra reads for what is, most of the time, a value that hasn't changed since the last check. There's also a subtler question buried in "read from the chain": read at what block? RPC providers don't all agree on the very latest block during a reorg, and a read that lands on a block that later gets replaced is its own small version of the same trust problem, one layer down. Treating the freshest confirmed state as authoritative, and re-checking rather than caching, is the mechanism — it doesn't make the read instantaneous or immune to provider disagreement, it just means you're asking the right question at the right time instead of trusting an answer that might be minutes or days old.&lt;/p&gt;

&lt;h2&gt;
  
  
  What breaks mid-cycle
&lt;/h2&gt;

&lt;p&gt;The uncomfortable case is a multi-step action — say a swap that routes through two pools, or a strategy that does three on-chain calls in sequence to complete one logical action. If a user revokes between step one and step two, step one already happened. You can't undo it. The system has to be built so a partial completion is a safe, loggable state rather than an unrecoverable one — which mostly means designing each step so it's fine to stop after it, not chaining steps that only make sense together and hoping revocation never lands in the middle. That's a design constraint, not something the revoke mechanism itself solves for you.&lt;/p&gt;

&lt;p&gt;None of this makes revocation less real. The user genuinely can remove the authorization, and once that transaction confirms, the contract genuinely will not let the spender move their tokens anymore — that part is enforced by the token contract itself, not by anyone's goodwill. What it means is narrower and more mechanical than "revocable" sounds: it's a state change on a specific chain, subject to that chain's confirmation times, and any executor sitting on top of it is only trustworthy if it treats its own records as a guess and the chain as the check.&lt;/p&gt;

&lt;p&gt;Disclosure: I build Tradevo, which runs automated strategies on Base from wallets whose keys the user holds. More at &lt;a href="https://app.tradevo.co/?utm_source=devto&amp;amp;utm_medium=syndication&amp;amp;utm_campaign=syndicate&amp;amp;utm_content=what-revoking-actually-does" rel="noopener noreferrer"&gt;app.tradevo.co&lt;/a&gt;. The trade-offs above are ones I have had to think through rather than ones I am claiming to have solved.&lt;/p&gt;

</description>
      <category>web3</category>
      <category>ethereum</category>
      <category>security</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>Scoped Permissions vs Custody: Letting Software Act On-Chain Without Holding Your Keys</title>
      <dc:creator>Christian Pichichero</dc:creator>
      <pubDate>Tue, 01 Sep 2026 02:19:59 +0000</pubDate>
      <link>https://dev.to/tradevodata/scoped-permissions-vs-custody-letting-software-act-on-chain-without-holding-your-keys-4pfe</link>
      <guid>https://dev.to/tradevodata/scoped-permissions-vs-custody-letting-software-act-on-chain-without-holding-your-keys-4pfe</guid>
      <description>&lt;p&gt;If you've ever set up a recurring on-chain action — a DCA buy, a rebalance, a claim-and-restake — you've run into the same fork in the road: either you hand a private key to something (a bot, a script, a service), or you sign every transaction yourself and the automation stops being automatic. Custody is the easy way out of that problem, and it's also the thing that goes wrong most often in this industry. So it's worth being precise about what "non-custodial automation" actually means mechanically, because the phrase gets used loosely.&lt;/p&gt;

&lt;p&gt;There are two structurally different ways to let a piece of software act from your wallet without giving it your funds.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Delegated session signers.&lt;/strong&gt; You generate a separate keypair, authorize it (usually via a signed message or an on-chain transaction) to act on your account, and hand the private half to a service. The service's backend now holds a key that can sign transactions on your behalf. Non-custodial in the narrow sense that your main wallet's key never leaves your device — but the &lt;em&gt;scope&lt;/em&gt; of what that session key can do is enforced by the vendor's own logic, before it ever reaches the chain. Something like this:&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="c1"&gt;// backend, off-chain, before it decides to sign&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;isCallInScope&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;call&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;policy&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;call&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;target&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="nx"&gt;policy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;allowedTarget&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt;
 &lt;span class="nx"&gt;policy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;spent&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;call&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="nx"&gt;policy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;cap&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's a real check, and a careful team writes it correctly. But notice where it lives: in a server you don't control, checked against a policy object that server maintains. If the backend has a bug, gets compromised, or is simply told by an operator to ignore the cap for one call, nothing on-chain stops it. The chain sees a validly signed transaction from an authorized key and executes it. The account itself has no opinion about whether the call was "in scope" — it trusted the key, full stop.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Account-enforced permissions (ERC-7715 style).&lt;/strong&gt; The newer approach, built for smart accounts under ERC-4337 and formalized in proposals like ERC-7715, moves the scope check into the account's own validation logic via caveat enforcers. When you grant a permission, you're not just handing over a key — you're installing a rule &lt;em&gt;inside your account&lt;/em&gt; that every action from that permission has to pass, checked by the account contract itself at execution time:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;// runs in the account's validation path, on-chain
function enforceCaveat(bytes calldata call, Caveat calldata c) external view {
 require(call.target == c.allowedTarget, "target not allowlisted");
 require(c.spent + call.value &amp;lt;= c.cap, "exceeds cap");
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The difference sounds small but it isn't. In the 7715 model, even if the delegate's key is fully compromised, the account contract rejects any call that violates the caveat — the enforcement is data the account carries, not trust the account extends. Revocation is also a first-class on-chain action against your own account state, not a request you send to someone else's server and hope gets honored before the next scheduled run.&lt;/p&gt;

&lt;p&gt;The honest trade-off: 7715 requires your wallet to be a smart account that supports the standard, and tooling for it is still young — wallet support is uneven, and the caveat-enforcer ecosystem (which enforcers exist, which are audited, which compose safely with each other) is not mature. The session-signer approach works today with wallets that have none of that infrastructure, and for a lot of use cases it is a reasonable, shippable answer. It's just that when you use it, the meaning of "scoped" is a promise made by a company's backend code, not a rule your account carries. That's a real distinction, not a pedantic one, and it's worth asking any "non-custodial automation" product which side of it they're on.&lt;/p&gt;

&lt;p&gt;A few other things are true regardless of which model you pick. Gas sponsorship (someone else pays gas so the user doesn't need a native-token balance sitting around) is orthogonal to custody — you can sponsor gas under either architecture. An allowlist of target contracts and an allocation cap reduce blast radius but don't eliminate it: a bug in an allowlisted contract, or a cap set too high, still lets real money move. And revocability is only as good as how quickly it takes effect — an on-chain revocation is final the moment it's mined; an off-chain "we'll stop signing now" is final whenever the backend process notices.&lt;/p&gt;

&lt;p&gt;Disclosure: I build Tradevo (app.tradevo.co), which runs algorithmic strategies from a wallet whose keys the user holds. More at &lt;a href="https://app.tradevo.co/?utm_source=devto&amp;amp;utm_medium=syndication&amp;amp;utm_campaign=syndicate&amp;amp;utm_content=scoped-permissions-instead-of-custody" rel="noopener noreferrer"&gt;app.tradevo.co&lt;/a&gt;. I'm not claiming it's the account-enforced version described above; it's worth being clear about which one you're actually getting, from us or anyone else, before you sign anything.&lt;/p&gt;

</description>
      <category>web3</category>
      <category>ethereum</category>
      <category>security</category>
      <category>architecture</category>
    </item>
    <item>
      <title>What Shuffling Your Trade History Actually Tells You (And What It Doesn't)</title>
      <dc:creator>Christian Pichichero</dc:creator>
      <pubDate>Mon, 31 Aug 2026 17:50:01 +0000</pubDate>
      <link>https://dev.to/tradevodata/what-shuffling-your-trade-history-actually-tells-you-and-what-it-doesnt-1fa7</link>
      <guid>https://dev.to/tradevodata/what-shuffling-your-trade-history-actually-tells-you-and-what-it-doesnt-1fa7</guid>
      <description>&lt;p&gt;You ran a backtest. The equity curve is smooth, the drawdown is small, the Sharpe ratio looks respectable. Here's the thing nobody tells you early enough: that curve is one path. It's the result of your trades happening in exactly the order they happened. If trade #14 (a big loser) had landed right after trade #3 (another big loser) instead of scattered safely between winners, your drawdown number would be a different number, from the same trades, with the same win rate, same average win, same average loss.&lt;/p&gt;

&lt;p&gt;That's the whole motivation for Monte Carlo trade resampling. You take the list of closed trades — just the P&amp;amp;L values, stripped of their original sequence — and you reshuffle them, thousands of times, rebuilding an equity curve for each shuffle. Two common flavors:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Permutation (resampling without replacement):&lt;/strong&gt; every shuffle uses the exact same set of trades, just in a different order. Total return is fixed across all shuffles; only the &lt;em&gt;path&lt;/em&gt; changes — which is precisely the point, since path determines drawdown and time-underwater.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Bootstrap (resampling with replacement):&lt;/strong&gt; each shuffle draws trades at random, allowing repeats and omissions. This also perturbs the total return, not just the order, giving you a sense of variance from sample size, not just sequence.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Here's a minimal version of the permutation approach:&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;max_drawdown&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;equity&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
 &lt;span class="n"&gt;peak&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;maximum&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;accumulate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;equity&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
 &lt;span class="n"&gt;dd&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;equity&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;peak&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="n"&gt;peak&lt;/span&gt;
 &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;dd&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;min&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;resample_paths&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;trade_returns&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;n_sims&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;5000&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
 &lt;span class="n"&gt;trades&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;array&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;trade_returns&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
 &lt;span class="n"&gt;results&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;_&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;range&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;n_sims&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
 &lt;span class="n"&gt;shuffled&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;permutation&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;trades&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
 &lt;span class="n"&gt;equity&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;cumprod&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;shuffled&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
 &lt;span class="n"&gt;results&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;max_drawdown&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;equity&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;np&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;array&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;results&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Run this on 80 closed trades and you don't get one drawdown number, you get a distribution of 5,000 drawdown numbers. That distribution is the actual output worth looking at. Somewhere in there is a 5th percentile case — a drawdown notably worse than the one your backtest happened to show you, built from the exact same trades. If your original backtest's drawdown sits near the friendly end of that distribution, your single equity curve got lucky on ordering, and you didn't know it because you only ever looked at the one path that occurred.&lt;/p&gt;

&lt;h3&gt;
  
  
  The median-path trap
&lt;/h3&gt;

&lt;p&gt;Here's where people go wrong once they've done the resampling correctly: they look at the median drawdown, or the median final equity, and treat it as "the expected outcome going forward." This is a mistake for a specific mathematical reason, not just a vague warning. Max drawdown is a nonlinear, path-dependent statistic — it's the result of a maximum and a minimum operating over the whole sequence. When you average or take the median of thousands of &lt;em&gt;nonlinear&lt;/em&gt; statistics, that summary number doesn't correspond to any real path anyone actually experiences. There is no shuffle in your simulation set whose drawdown equals the median drawdown by construction — it's a statistic about the population of outcomes, not a description of a plausible one.&lt;/p&gt;

&lt;p&gt;Worse, the distribution of drawdowns is almost always right-skewed (bounded at zero, long tail toward catastrophic). The median underrepresents the tail. If you're using this number to size positions or set risk limits, you want the 90th or 95th percentile of drawdown, not the middle of the pack — the middle is the case where nothing went particularly wrong, which is not the case you're trying to survive.&lt;/p&gt;

&lt;h3&gt;
  
  
  The assumption everyone skips: independence
&lt;/h3&gt;

&lt;p&gt;Both permutation and bootstrap resampling rest on one assumption: that each trade's outcome is independent of the trades around it — that shuffling the order doesn't destroy any real information, because there wasn't any sequential structure to begin with. For a lot of systematic strategies this is roughly fine. For momentum strategies, it's false, and it's false in a way that matters.&lt;/p&gt;

&lt;p&gt;Momentum strategies, by construction, tend to produce autocorrelated trade outcomes: winning trades cluster during trending regimes, losing trades cluster during chop or reversals, because the underlying edge itself is regime-dependent. When you randomly permute those trades, you break up the clusters. This can cut both ways — sometimes it makes the resampled drawdowns look &lt;em&gt;worse&lt;/em&gt; than reality, because it creates unlucky strings of losses that would never actually co-occur (a losing streak needs a choppy regime, and regimes don't get randomly interleaved trade-by-trade in real markets). Other times it hides the real risk, because the actual worst case is "strategy stops working when the regime changes for three months straight," and no reshuffling of historical trade P&amp;amp;L will manufacture a scenario the strategy never lived through.&lt;/p&gt;

&lt;p&gt;Block bootstrapping — resampling contiguous chunks of trades instead of individual ones — partially addresses this by preserving some local correlation structure. It's a real improvement, not a full fix. It still can't invent a regime your strategy never traded through, and it still assumes the &lt;em&gt;blocks&lt;/em&gt; are exchangeable, which is a weaker but still real assumption.&lt;/p&gt;

&lt;p&gt;So what resampling actually tells you: how much of your backtest's apparent smoothness depended on the specific order the trades happened to arrive in, and how much of that ordering is even ordering you can trust reshuffling to explore honestly. It's a stress test on a fixed sample, not a forecast, and treating the tidy version — the median path, or a permutation test on a momentum book — as the expected future is the fast way to be surprised by a drawdown your simulation told you was rare.&lt;/p&gt;

&lt;p&gt;Disclosure: I build Tradevo Verify, which runs this kind of resampling (among other checks) against closed-trade exports and reports the resulting distribution rather than a single pass/fail number — because the distribution is the honest answer and a single number usually isn't.&lt;/p&gt;

</description>
      <category>python</category>
      <category>statistics</category>
      <category>datascience</category>
      <category>trading</category>
    </item>
    <item>
      <title>What a Clean Equity Curve Doesn't Prove</title>
      <dc:creator>Christian Pichichero</dc:creator>
      <pubDate>Mon, 31 Aug 2026 17:27:25 +0000</pubDate>
      <link>https://dev.to/tradevodata/what-a-clean-equity-curve-doesnt-prove-1fif</link>
      <guid>https://dev.to/tradevodata/what-a-clean-equity-curve-doesnt-prove-1fif</guid>
      <description>&lt;p&gt;You've seen the shape before: equity curve going up and to the right, Sharpe ratio north of 2, max drawdown that looks survivable. It's convincing. It's also, on its own, close to worthless as evidence that the strategy will do anything useful going forward.&lt;/p&gt;

&lt;p&gt;This isn't a claim that backtesting is pointless. It's that a single backtest, however clean, can hide four specific failure modes that don't show up in the summary stats you're staring at. You have to go looking for them in the trade list itself.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Return concentration
&lt;/h2&gt;

&lt;p&gt;Open your closed-trade export and sort by PnL, descending. Sum the top 3 trades. Sum everything else. If the top 3 account for most of your total return, your backtest isn't describing a strategy — it's describing a small number of events that happened to occur in your sample window.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csvs"&gt;&lt;code&gt;&lt;span class="k"&gt;trade&lt;/span&gt;&lt;span class="err"&gt;_&lt;/span&gt;&lt;span class="k"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;pnl&lt;/span&gt;
&lt;span class="mf"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;120&lt;/span&gt;
&lt;span class="mf"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="err"&gt;-&lt;/span&gt;&lt;span class="mf"&gt;40&lt;/span&gt;
&lt;span class="mf"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;3800&lt;/span&gt;   &lt;span class="err"&gt;&amp;lt;-&lt;/span&gt; &lt;span class="k"&gt;one&lt;/span&gt; &lt;span class="k"&gt;trade&lt;/span&gt;
&lt;span class="mf"&gt;4&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="err"&gt;-&lt;/span&gt;&lt;span class="mf"&gt;60&lt;/span&gt;
&lt;span class="mf"&gt;5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;90&lt;/span&gt;
&lt;span class="err"&gt;...&lt;/span&gt;
&lt;span class="mf"&gt;47&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="err"&gt;-&lt;/span&gt;&lt;span class="mf"&gt;30&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A strategy with 47 trades where one trade is 90% of total profit isn't a systematic edge, it's a lottery ticket that paid off once during backtesting. The Sharpe ratio doesn't tell you this. The equity curve doesn't tell you this, because one big trade still draws a smooth-looking line. You have to look at the distribution of individual trade PnL, not the cumulative sum.&lt;/p&gt;

&lt;p&gt;The fix isn't to throw away the trade — maybe it was real. The fix is to know it's there before you size a live position based on "the strategy averages X% per trade."&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Regime dependence
&lt;/h2&gt;

&lt;p&gt;A strategy backtested from 2019 to 2023 has lived through exactly one dominant regime: a multi-year bull market interrupted by one sharp, fast-recovering crash. If your entries are long-biased and your backtest window doesn't include a prolonged sideways or bear regime, you haven't tested a strategy — you've tested a strategy's performance during one macro condition, and gotten a result that will not generalize.&lt;/p&gt;

&lt;p&gt;The way this hides itself: date range selection feels neutral. "I used all the data I had" sounds responsible. But if all the data you had happens to be one regime, your Sharpe ratio is really a Sharpe ratio conditioned on that regime, and nothing in the report tells you that conditioning exists. You find it by segmenting trades by market condition (trend vs. chop, high vol vs. low vol) and checking whether performance holds up in each segment separately, not just in aggregate.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Execution assumptions doing the work
&lt;/h2&gt;

&lt;p&gt;Every backtest makes assumptions about fills: what price you get, how much slippage, whether you can actually get filled at all at the size you're testing. These assumptions are usually buried in a config file or a default in your backtesting library, and they are frequently the entire source of the edge.&lt;/p&gt;

&lt;p&gt;A mean-reversion strategy that assumes fills at the exact touch price on a thinly traded instrument is not describing a strategy — it's describing what happens if you had a magic wand for that one variable. Move the assumption from "fill at touch" to "fill at touch plus one tick" and watch a lot of "profitable" systematic strategies go flat or negative. This is worth doing as a deliberate stress test: take your existing trade list, degrade the fill assumption by a fixed amount, and recompute. If the strategy's profitability doesn't survive a small, realistic degradation, the edge was execution assumption, not signal.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Path luck
&lt;/h2&gt;

&lt;p&gt;Even a strategy with real, non-concentrated, regime-robust edge produced one specific sequence of trades, in one specific order, during one specific slice of history. That sequence is a single sample from a distribution of possible sequences. Some of those alternate sequences look much worse — deeper drawdowns, longer flat periods, sequences where the losing trades cluster early and you'd have quit before the edge showed up.&lt;/p&gt;

&lt;p&gt;The standard way to check this is to shuffle: take your trade returns, resample them (with replacement, or reorder them, depending on what you're testing for), and generate a distribution of possible equity curves instead of the one you happened to get. If your actual curve sits near the median of that distribution, the drawdown you experienced is typical. If your actual curve is near the best-case tail, you got lucky with path, and a live version of this strategy is more likely to look like the median case — which might include a drawdown you didn't plan for.&lt;/p&gt;

&lt;h2&gt;
  
  
  What this adds up to
&lt;/h2&gt;

&lt;p&gt;None of these four checks are exotic. Concentration is a sort and a sum. Regime dependence is a segmentation. Execution sensitivity is a parameter sweep. Path luck is a resample. You can build all of this yourself in an afternoon with pandas and numpy, and if you're serious about running a systematic strategy with real money, you should — at minimum look at your trade list sorted by PnL and ask if 3 trades are secretly the whole story.&lt;/p&gt;

&lt;p&gt;The uncomfortable conclusion is that a clean equity curve is weak evidence by default. It's evidence of what happened in one sample, once, under one set of fill assumptions. Turning it into evidence you can actually lean on means trying to break it, not admiring it.&lt;/p&gt;

&lt;p&gt;Disclosure: I build Tradevo Verify, which runs a version of these checks (execution stress, path/Monte Carlo, concentration, regime tests) against a closed-trade CSV and returns a report on how much weight the evidence can carry — not a verdict that the strategy is good. It's a $99 one-time check, and it's built to return a fragile result whenever the trade list earns one.&lt;/p&gt;

</description>
      <category>python</category>
      <category>datascience</category>
      <category>trading</category>
      <category>statistics</category>
    </item>
    <item>
      <title>Point-in-Time Fundamentals for Numerai Signals: Killing Lookahead in Your Feature Join</title>
      <dc:creator>Christian Pichichero</dc:creator>
      <pubDate>Wed, 19 Aug 2026 16:00:35 +0000</pubDate>
      <link>https://dev.to/tradevodata/point-in-time-fundamentals-for-numerai-signals-killing-lookahead-in-your-feature-join-1h54</link>
      <guid>https://dev.to/tradevodata/point-in-time-fundamentals-for-numerai-signals-killing-lookahead-in-your-feature-join-1h54</guid>
      <description>&lt;p&gt;If you build features for Numerai Signals from fundamentals, the single most common way to silently overstate your live performance is joining on the wrong date. A table keyed by fiscal period end, or by a single "report date" that is overwritten on every revision, cannot tell you when a number was actually knowable. Our rows are keyed by &lt;code&gt;first_filed&lt;/code&gt; — the date the value first appeared on EDGAR — with the first-reported value stored separately from the current one. Numerai's tournament resolves against real future returns, so any leakage in your feature construction shows up as a validation metric that decays the moment you go live. (This is not investment advice, and nothing here is a performance promise.)&lt;/p&gt;

&lt;p&gt;This article covers three things: why lookahead leaks into fundamentals-based signals even when you think you've handled it, how a &lt;code&gt;first_filed&lt;/code&gt;-keyed join fixes the mechanics, and where a small, honest, US-annual-only dataset like ours fits — and where it doesn't.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why fundamentals leak into signals more than price data does
&lt;/h2&gt;

&lt;p&gt;Price and volume are point-in-time by construction — the close on Tuesday was known Tuesday night. Fundamentals are not. A 10-K covering fiscal year 2022 might be filed in March 2023, restated in an amendment in August 2023, and then sit in a vendor's database keyed only by "period end 2022-12-31." If your pipeline joins on period end and pulls whatever value is in the database &lt;em&gt;today&lt;/em&gt;, you're feeding your model information that didn't exist yet, and sometimes a corrected number that didn't exist until months later.&lt;/p&gt;

&lt;p&gt;This is a bigger problem for fundamentals than most people expect, because:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Filing lag varies a lot by company size and quality.&lt;/strong&gt; Some filers report a few weeks after period end (rare), most take one to three months, and small caps can lag further.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Restatements are common and not always flagged.&lt;/strong&gt; If a value changes across amendments and you don't know which version was live on a given day, backtests can pick up the &lt;em&gt;revised&lt;/em&gt; number, which can correlate with future returns simply because it was derived with hindsight.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Numerai's eras compound the problem.&lt;/strong&gt; Signals are scored weekly across thousands of tickers; a systematic few-week lookahead bias across the whole universe doesn't average out — it can inflate validation metrics uniformly, which is worth checking for if a validation curve looks unusually strong.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;We wrote a longer, more technical breakdown of this mechanism in &lt;a href="https://tradevodata.com/blog/lookahead-bias-fundamental-backtests?utm_source=devto&amp;amp;utm_medium=syndication&amp;amp;utm_campaign=syndicate&amp;amp;utm_content=point-in-time-data-for-numerai-signals" rel="noopener noreferrer"&gt;Lookahead Bias in Fundamental Backtests&lt;/a&gt; if you want the failure modes in more detail.&lt;/p&gt;

&lt;h2&gt;
  
  
  The correct join: &lt;code&gt;first_filed&lt;/code&gt;, not period end
&lt;/h2&gt;

&lt;p&gt;The fix is mechanical once you have the right column. Every fundamentals row needs a &lt;code&gt;first_filed&lt;/code&gt; timestamp — the date the value became public via SEC EDGAR — separate from the fiscal period it describes. For a given Numerai submission date &lt;code&gt;as_of&lt;/code&gt;, the query is:&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="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;fundamentals&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;ticker&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;?&lt;/span&gt;
  &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;first_filed&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="n"&gt;as_of&lt;/span&gt;   &lt;span class="c1"&gt;-- same-day inclusive&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;first_filed&lt;/span&gt; &lt;span class="k"&gt;DESC&lt;/span&gt;
&lt;span class="k"&gt;LIMIT&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That is the filing-date half of the leakage fix: the filter is &lt;code&gt;first_filed &amp;lt;= as_of&lt;/code&gt;, applied per row, instead of a uniform "lag by 90 days" heuristic across every filer (which both under- and over-corrects depending on the company). You also want two values per row, not one:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;original_value&lt;/code&gt; — the first-reported figure, safe for point-in-time backtests.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;latest_value&lt;/code&gt; — the current, revision-including figure, useful only if you're deliberately studying restatement effects.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;We key our dataset this way: &lt;code&gt;first_filed&lt;/code&gt;, &lt;code&gt;original_value&lt;/code&gt;, &lt;code&gt;latest_value&lt;/code&gt;, a &lt;code&gt;restated&lt;/code&gt; flag (set when a same-tag revision exceeds 0.5%, including amendments), and &lt;code&gt;qa_status&lt;/code&gt;. Across our current build, 18,723 rows carry that restated flag out of 312,751 total — restatements are common enough that ignoring them isn't a rounding error.&lt;/p&gt;

&lt;h2&gt;
  
  
  What our dataset is (and explicitly is not)
&lt;/h2&gt;

&lt;p&gt;Tradevo Data (&lt;a href="https://tradevodata.com/?ref=blog&amp;amp;utm_source=devto&amp;amp;utm_medium=syndication&amp;amp;utm_campaign=syndicate&amp;amp;utm_content=point-in-time-data-for-numerai-signals" rel="noopener noreferrer"&gt;tradevodata.com&lt;/a&gt;) is a point-in-time US equity fundamentals dataset sourced entirely from SEC EDGAR (public domain data, not redistributed from a paid vendor). Currently: 5,189 US companies, 312,751 point-in-time rows, 7 core concepts (Revenue, NetIncome, Assets, StockholdersEquity, OperatingCashFlow, EPSDiluted, DilutedShares), up to 12 fiscal years of history, &lt;strong&gt;annual only&lt;/strong&gt; — 10-K and 10-K/A filings. Quarterly is on the roadmap, not shipped. If your Numerai signal design needs quarterly fundamentals or non-US tickers, this dataset will not cover you today — say so up front rather than let you find out after checkout.&lt;/p&gt;

&lt;p&gt;Access is one JSON endpoint, &lt;code&gt;/v1/fundamentals?ticker&amp;amp;as_of[&amp;amp;concept]&lt;/code&gt;, server-side &lt;code&gt;first_filed &amp;lt;= as_of&lt;/code&gt; filtering built in, plus the full dataset via &lt;code&gt;/v1/download&lt;/code&gt; and whole-universe cross-sections via &lt;code&gt;/v1/snapshot?as_of&lt;/code&gt; — all included in the $29/mo plan, 5,000 requests/day and 2,500 distinct tickers/day (use &lt;code&gt;/v1/snapshot&lt;/code&gt; or &lt;code&gt;/v1/download&lt;/code&gt; for cross-sections). No quarterly, no non-US, no Parquet yet (CSV/gzip only; Parquet is roadmap).&lt;/p&gt;

&lt;p&gt;On lag: on the reliable-filing rows of our 40-company free sample, the gap between fiscal period end and &lt;code&gt;first_filed&lt;/code&gt; was mean 43.4 days / max 61 days. That figure is scoped to the sample. Across the full 5,189-company universe the reliable-row gap is wider — mean 66 days, median 60, p90 90 — because large caps are the fastest filers, so don't extrapolate the sample figure to the whole universe.&lt;/p&gt;

&lt;h2&gt;
  
  
  Fair comparison
&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;Tradevo Data&lt;/th&gt;
&lt;th&gt;Sharadar (Nasdaq Data Link)&lt;/th&gt;
&lt;th&gt;Tiingo&lt;/th&gt;
&lt;th&gt;QuantConnect&lt;/th&gt;
&lt;th&gt;Build it yourself from EDGAR&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Point-in-time fields&lt;/td&gt;
&lt;td&gt;Yes (&lt;code&gt;first_filed&lt;/code&gt;, original + latest)&lt;/td&gt;
&lt;td&gt;Yes, per their docs&lt;/td&gt;
&lt;td&gt;Fundamentals PIT coverage varies, check their docs&lt;/td&gt;
&lt;td&gt;Yes, via their data infra&lt;/td&gt;
&lt;td&gt;Yes, if you build it correctly&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Frequency&lt;/td&gt;
&lt;td&gt;Annual only&lt;/td&gt;
&lt;td&gt;Annual + quarterly, per their docs&lt;/td&gt;
&lt;td&gt;Varies by plan&lt;/td&gt;
&lt;td&gt;Varies by plan&lt;/td&gt;
&lt;td&gt;Whatever you extract&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Coverage&lt;/td&gt;
&lt;td&gt;US only&lt;/td&gt;
&lt;td&gt;US, check their docs for depth/history&lt;/td&gt;
&lt;td&gt;US-focused&lt;/td&gt;
&lt;td&gt;Multi-asset via platform&lt;/td&gt;
&lt;td&gt;Whatever you scope&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Price&lt;/td&gt;
&lt;td&gt;$29/mo flat&lt;/td&gt;
&lt;td&gt;See their pricing page&lt;/td&gt;
&lt;td&gt;See their pricing page&lt;/td&gt;
&lt;td&gt;See their pricing page&lt;/td&gt;
&lt;td&gt;Your engineering time&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Restatement flags&lt;/td&gt;
&lt;td&gt;Yes, explicit&lt;/td&gt;
&lt;td&gt;Check their docs&lt;/td&gt;
&lt;td&gt;Check their docs&lt;/td&gt;
&lt;td&gt;Check their docs&lt;/td&gt;
&lt;td&gt;You build the logic&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;We don't know competitors' current prices and won't guess — check their pricing pages directly, they change.&lt;/p&gt;

&lt;h2&gt;
  
  
  When another option is genuinely better
&lt;/h2&gt;

&lt;p&gt;If you need quarterly fundamentals for Numerai Signals features (which many quality/growth factors want), Sharadar or a comparable vendor with quarterly PIT coverage is the right call today — we don't have it. If you need international equities, none of what's here helps; we're US-only. If you're already inside QuantConnect's ecosystem and want fundamentals integrated with their backtester and live trading, their bundled data may save you more integration time than a standalone API, even before comparing price.&lt;/p&gt;

&lt;h2&gt;
  
  
  When to build it yourself
&lt;/h2&gt;

&lt;p&gt;EDGAR's data is public and free. If you only need a handful of concepts for a handful of tickers, and you're comfortable parsing XBRL and handling amendment logic yourself, you can build a &lt;code&gt;first_filed&lt;/code&gt;-keyed table in a weekend. The tradeoffs: you own restatement detection, filer-level edge cases (fiscal year changes, non-calendar year ends, multiple amendments to the same period), and ongoing maintenance as EDGAR's XBRL taxonomy shifts. For a few tickers, doable. For thousands of tickers across multiple years, it becomes a real data-engineering project — which is the gap we built this to fill.&lt;/p&gt;

&lt;h2&gt;
  
  
  Try before you subscribe
&lt;/h2&gt;

&lt;p&gt;The free sample — 40 companies, 3,280 rows, full methodology, no signup — is on &lt;a href="https://tradevodata.com/go/github-sample?cta_location=blog-numerai&amp;amp;utm_source=devto&amp;amp;utm_medium=syndication&amp;amp;utm_campaign=syndicate&amp;amp;utm_content=point-in-time-data-for-numerai-signals" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;. Run your own join logic against it before paying for anything. If it fits your Signals pipeline, the full dataset is &lt;a href="https://tradevodata.com/?ref=blog&amp;amp;utm_source=devto&amp;amp;utm_medium=syndication&amp;amp;utm_campaign=syndicate&amp;amp;utm_content=point-in-time-data-for-numerai-signals#pricing" rel="noopener noreferrer"&gt;$29/mo&lt;/a&gt;, instant key after Stripe checkout, cancel anytime. More on the mechanics of PIT fundamentals generally: &lt;a href="https://tradevodata.com/blog/point-in-time-fundamentals-data?utm_source=devto&amp;amp;utm_medium=syndication&amp;amp;utm_campaign=syndicate&amp;amp;utm_content=point-in-time-data-for-numerai-signals" rel="noopener noreferrer"&gt;Point-in-Time Fundamentals Data, Explained&lt;/a&gt;.&lt;/p&gt;




&lt;p&gt;Not investment advice; verify any competitor pricing yourself on their current pricing pages.&lt;/p&gt;

</description>
      <category>quant</category>
      <category>python</category>
      <category>finance</category>
      <category>datascience</category>
    </item>
    <item>
      <title>Why the Same Fundamental Lives Under Different XBRL Tags in SEC EDGAR</title>
      <dc:creator>Christian Pichichero</dc:creator>
      <pubDate>Wed, 12 Aug 2026 16:31:22 +0000</pubDate>
      <link>https://dev.to/tradevodata/why-the-same-fundamental-lives-under-different-xbrl-tags-in-sec-edgar-fmi</link>
      <guid>https://dev.to/tradevodata/why-the-same-fundamental-lives-under-different-xbrl-tags-in-sec-edgar-fmi</guid>
      <description>&lt;p&gt;If you've tried to pull "revenue" for a US company directly out of SEC EDGAR's XBRL data, you've probably noticed something annoying: the same line item on the income statement doesn't always show up under the same tag. One year it's &lt;code&gt;Revenues&lt;/code&gt;. Another year, for the same company, it's &lt;code&gt;SalesRevenueNet&lt;/code&gt;. After 2018 it might switch again to &lt;code&gt;RevenueFromContractWithCustomerExcludingAssessedTax&lt;/code&gt;. Nothing about the business changed — the taxonomy did.&lt;/p&gt;

&lt;p&gt;This is one of the least-discussed but most consequential problems in building point-in-time fundamentals from EDGAR, and it's the reason a lot of DIY XBRL scrapers quietly produce broken time series without anyone noticing until a backtest looks weird.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why the tags change at all
&lt;/h2&gt;

&lt;p&gt;The SEC requires filers to tag financial statement line items using the US GAAP XBRL taxonomy, which the FASB updates annually. A few things drive tag churn:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Taxonomy revisions.&lt;/strong&gt; New tags get added, old ones get deprecated, and companies (or their filing agents) migrate to the current tag in a later filing — sometimes mid-history, sometimes not at all.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Accounting standard changes.&lt;/strong&gt; The rollout of ASC 606 (revenue recognition) around 2018 is the clearest example: many filers moved from &lt;code&gt;Revenues&lt;/code&gt; or &lt;code&gt;SalesRevenueNet&lt;/code&gt; to &lt;code&gt;RevenueFromContractWithCustomerExcludingAssessedTax&lt;/code&gt; or the "IncludingAssessedTax" variant, sometimes in the same fiscal year they adopted the standard.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Filer inconsistency.&lt;/strong&gt; Two companies in the same industry, filing in the same quarter, can choose different (both technically valid) tags for what an analyst would call the same concept. Smaller filers and their outside preparers are especially inconsistent year over year.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Custom/extension tags.&lt;/strong&gt; Filers can create company-specific extension tags instead of using a standard one, which is valid XBRL but invisible to anyone matching on a fixed tag list.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;None of this is a data error. It's just how the taxonomy and filer behavior evolve. But if your pipeline hardcodes "revenue = tag X," you will silently lose or corrupt history the moment a company switches tags.&lt;/p&gt;

&lt;h2&gt;
  
  
  How this breaks naive datasets
&lt;/h2&gt;

&lt;p&gt;The common failure mode looks like this: a script pulls &lt;code&gt;Revenues&lt;/code&gt; for every 10-K, going back as far as the filer used that tag. The moment the company switches to &lt;code&gt;RevenueFromContractWithCustomerExcludingAssessedTax&lt;/code&gt;, the naive pull sees a gap — the concept looks like it disappeared. Depending on how the downstream code handles missing values, you get one of three quiet failures:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;A false gap in the time series (treated as no revenue reported).&lt;/li&gt;
&lt;li&gt;A forward-fill of the last known value, understating growth or flatlining a metric that actually changed.&lt;/li&gt;
&lt;li&gt;A join against the wrong tag entirely, if a fallback rule grabs a &lt;em&gt;different&lt;/em&gt; line item that happens to exist (e.g., falling back to &lt;code&gt;SalesRevenueGoodsNet&lt;/code&gt; and picking up only product revenue, not total revenue).&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;All three are worse than an explicit "no data," because they don't look like errors — they look like real numbers. That's the trap: tag drift doesn't crash your pipeline, it silently biases it.&lt;/p&gt;

&lt;h2&gt;
  
  
  How synonym-tag resolution actually works
&lt;/h2&gt;

&lt;p&gt;The fix is conceptually simple but tedious to do correctly: instead of mapping one concept to one tag, you maintain a &lt;strong&gt;synonym set&lt;/strong&gt; of tags per concept and resolve among the candidates present in each filing, rather than baking in a single tag at ingest time.&lt;/p&gt;

&lt;p&gt;In practice, for Tradevo Data that means:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Each of our seven tracked concepts (Revenue, NetIncome, Assets, StockholdersEquity, OperatingCashFlow, EPSDiluted, DilutedShares) maps to a list of known US GAAP tags that filers have used for that concept, not a single tag.&lt;/li&gt;
&lt;li&gt;When a filing is parsed, the synonym tags present in that same filing are compared rather than ranked. Every candidate on the list is intended to be a consolidated total, so when two of them disagree materially the larger is the total and the smaller is a component of it, and the larger wins. The tag that actually resolved is recorded on the row, so the mapping is auditable rather than a black box. We used to apply a fixed priority order, and it shipped a 10× error on General Mills — the &lt;a href="https://tradevodata.com/blog/we-shipped-a-10x-error?utm_source=devto&amp;amp;utm_medium=syndication&amp;amp;utm_campaign=syndicate&amp;amp;utm_content=xbrl-tags-for-fundamentals-explained" rel="noopener noreferrer"&gt;post-mortem&lt;/a&gt; explains why no fixed ordering gets MetLife and General Mills right at the same time.&lt;/li&gt;
&lt;li&gt;We track &lt;code&gt;first_filed&lt;/code&gt; (when the value became public) separately from &lt;code&gt;latest_value&lt;/code&gt; (the current, possibly amended figure), and flag rows as &lt;code&gt;restated&lt;/code&gt; when a same-tag revision moves the value by more than 0.5%, including amendments. That's how the dataset ends up with 18,723 labeled restatements — those are tag-consistent revisions, not tag-switch artifacts.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;qa_status&lt;/code&gt; on each row exists specifically so a switch that looks suspicious (e.g., a jump coinciding with a tag change) is visible to whoever is using the data, rather than silently smoothed over.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The full mapping logic and the reasoning behind it — including which tags we treat as synonyms for each concept and why — is public in the methodology alongside the free sample, not hidden behind the paid API. If you want to see exactly how a specific company's revenue tag changed over time, that's the place to check it yourself: github.com/christianpichichero-max/pit-fundamentals (3,280 rows across 40 companies, full methodology, no signup).&lt;/p&gt;

&lt;p&gt;For the point-in-time angle specifically — why &lt;code&gt;first_filed&lt;/code&gt; matters independently of tag resolution — see &lt;a href="https://tradevodata.com/blog/point-in-time-fundamentals-data?utm_source=devto&amp;amp;utm_medium=syndication&amp;amp;utm_campaign=syndicate&amp;amp;utm_content=xbrl-tags-for-fundamentals-explained" rel="noopener noreferrer"&gt;/blog/point-in-time-fundamentals-data&lt;/a&gt; and &lt;a href="https://tradevodata.com/blog/lookahead-bias-fundamental-backtests?utm_source=devto&amp;amp;utm_medium=syndication&amp;amp;utm_campaign=syndicate&amp;amp;utm_content=xbrl-tags-for-fundamentals-explained" rel="noopener noreferrer"&gt;/blog/lookahead-bias-fundamental-backtests&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  A fair comparison of your options
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Approach&lt;/th&gt;
&lt;th&gt;Handles tag drift?&lt;/th&gt;
&lt;th&gt;Point-in-time (&lt;code&gt;first_filed&lt;/code&gt;)?&lt;/th&gt;
&lt;th&gt;Frequency&lt;/th&gt;
&lt;th&gt;Cost&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Raw SEC EDGAR + your own scraper&lt;/td&gt;
&lt;td&gt;Only if you build synonym mapping yourself&lt;/td&gt;
&lt;td&gt;Only if you build it (EDGAR gives you filing dates, not a PIT API)&lt;/td&gt;
&lt;td&gt;Whatever you implement&lt;/td&gt;
&lt;td&gt;Free (your engineering time)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Tradevo Data&lt;/td&gt;
&lt;td&gt;Yes, synonym sets per concept, documented in the free methodology&lt;/td&gt;
&lt;td&gt;Yes, &lt;code&gt;first_filed&lt;/code&gt; + &lt;code&gt;original_value&lt;/code&gt; on every row&lt;/td&gt;
&lt;td&gt;Annual only (10-K / 10-K/A); quarterly is on the roadmap, not available today&lt;/td&gt;
&lt;td&gt;$29/mo&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Sharadar / Tiingo / QuantConnect fundamentals&lt;/td&gt;
&lt;td&gt;Not independently verified by us — these are established, mature vendors, so check their docs for specifics&lt;/td&gt;
&lt;td&gt;Varies by product; check each vendor's docs&lt;/td&gt;
&lt;td&gt;Varies by product; check each vendor's docs&lt;/td&gt;
&lt;td&gt;See their pricing pages: &lt;a href="https://data.nasdaq.com/publishers/SHARADAR" rel="noopener noreferrer"&gt;Sharadar via Nasdaq Data Link&lt;/a&gt;, &lt;a href="https://www.tiingo.com/" rel="noopener noreferrer"&gt;Tiingo&lt;/a&gt;, &lt;a href="https://www.quantconnect.com/datasets" rel="noopener noreferrer"&gt;QuantConnect&lt;/a&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;This isn't an attempt to declare a winner. We haven't audited Sharadar's, Tiingo's, or QuantConnect's internal tag-resolution logic, so we're not claiming to know how they handle it — only that they're credible, established sources worth comparing against. If price or feature fit matters to your decision, their pricing pages will tell you what's on offer; we're not going to guess a number for you.&lt;/p&gt;

&lt;h2&gt;
  
  
  When the established players are the better choice
&lt;/h2&gt;

&lt;p&gt;Be honest with yourself about what you actually need before defaulting to the cheaper option:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;If you need &lt;strong&gt;quarterly fundamentals&lt;/strong&gt;, Tradevo Data doesn't have them yet (roadmap only) — an established vendor that already offers quarterly data is the right call today.&lt;/li&gt;
&lt;li&gt;If you need &lt;strong&gt;non-US markets&lt;/strong&gt;, more historical depth than 12 fiscal years, or a broader concept set beyond our seven, a larger vendor's coverage is likely to fit better.&lt;/li&gt;
&lt;li&gt;If you need a &lt;strong&gt;track record&lt;/strong&gt; — a data provider that's been used in production research for years, with support SLAs and a sales team you can talk to — that's a real advantage of established players over a $29/mo budget tool run by a small team.&lt;/li&gt;
&lt;li&gt;If Parquet or other formats matter to your pipeline today (not roadmap, today), check whether an established vendor already ships it.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;We'd rather point you to the right tool than pretend we're the right tool for every use case.&lt;/p&gt;

&lt;h2&gt;
  
  
  When you should build it yourself
&lt;/h2&gt;

&lt;p&gt;Building your own EDGAR XBRL parser is a legitimate choice, not just a fallback for people who can't afford data. It's the right call if:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;You need concepts or tags outside the seven we track (segment data, non-GAAP reconciliations, footnote disclosures).&lt;/li&gt;
&lt;li&gt;You need international filers or non-EDGAR sources.&lt;/li&gt;
&lt;li&gt;You have engineering time to spend and want full control over the synonym-resolution rules rather than trusting someone else's judgment calls.&lt;/li&gt;
&lt;li&gt;Your research only needs a handful of companies and a few concepts — at that scale, hand-checking tag switches against the actual 10-K filings is faster than integrating a new data source.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Where a vendor (any vendor, not just us) earns its cost is in the tedious part: tracking taxonomy changes across thousands of filers over many years, catching restatements, and doing it consistently so you're not re-solving the same tag-drift problem every time the FASB updates the taxonomy.&lt;/p&gt;

&lt;h2&gt;
  
  
  Try it before you pay for it
&lt;/h2&gt;

&lt;p&gt;The free sample has the same tag-resolution logic as the paid dataset, just scoped to 40 companies and 3,280 rows — enough to inspect a real tag switch yourself and decide if the approach holds up: github.com/christianpichichero-max/pit-fundamentals.&lt;/p&gt;

&lt;p&gt;If it does and you need the full 5,189-company, 312,751-row universe with a server-side &lt;code&gt;as_of&lt;/code&gt; query, the API and bulk download (&lt;code&gt;/v1/download&lt;/code&gt;, &lt;code&gt;/v1/snapshot?as_of&lt;/code&gt;) are $29/mo, cancel anytime: &lt;a href="https://tradevodata.com/?ref=blog&amp;amp;utm_source=devto&amp;amp;utm_medium=syndication&amp;amp;utm_campaign=syndicate&amp;amp;utm_content=xbrl-tags-for-fundamentals-explained#pricing" rel="noopener noreferrer"&gt;tradevodata.com/?ref=blog&lt;/a&gt;.&lt;/p&gt;




&lt;p&gt;Not investment advice. This dataset describes what was publicly filed and when — it makes no claims about future performance. Verify competitor pricing and feature claims yourself on their websites before deciding.&lt;/p&gt;

</description>
      <category>quant</category>
      <category>python</category>
      <category>finance</category>
      <category>datascience</category>
    </item>
    <item>
      <title>SEC 10-K Filing Deadline Data: The Structural Reason Fundamentals Lag Weeks Behind</title>
      <dc:creator>Christian Pichichero</dc:creator>
      <pubDate>Wed, 12 Aug 2026 16:30:51 +0000</pubDate>
      <link>https://dev.to/tradevodata/sec-10-k-filing-deadline-data-the-structural-reason-fundamentals-lag-weeks-behind-2h36</link>
      <guid>https://dev.to/tradevodata/sec-10-k-filing-deadline-data-the-structural-reason-fundamentals-lag-weeks-behind-2h36</guid>
      <description>&lt;p&gt;If you've ever wondered why a company's "Q4 numbers" aren't publicly known the day the fiscal year ends, the answer isn't vendor laziness or data-pipeline delay. It's the SEC's own filing calendar. The deadlines are public, stable, and have been in place for years — and they're the single biggest structural reason any honest fundamentals dataset has a gap between period-end and public availability.&lt;/p&gt;

&lt;p&gt;This matters for anyone backtesting on fundamentals: if your data model assumes a number was known the moment the quarter closed, you're not modeling reality. You're modeling a fantasy calendar that doesn't exist.&lt;/p&gt;

&lt;h2&gt;
  
  
  The SEC's 10-K Deadlines Are Public and Fixed by Filer Class
&lt;/h2&gt;

&lt;p&gt;Under SEC rules (Exchange Act Rule 12b-2 and related Regulation S-K guidance), the deadline to file an annual report (Form 10-K) after fiscal year-end depends on the company's filer category, which is based on public float:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Filer Category&lt;/th&gt;
&lt;th&gt;Public Float Threshold&lt;/th&gt;
&lt;th&gt;10-K Deadline After Fiscal Year-End&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Large Accelerated Filer&lt;/td&gt;
&lt;td&gt;$700M or more&lt;/td&gt;
&lt;td&gt;60 days&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Accelerated Filer&lt;/td&gt;
&lt;td&gt;$75M to $700M&lt;/td&gt;
&lt;td&gt;75 days&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Non-Accelerated Filer&lt;/td&gt;
&lt;td&gt;Under $75M&lt;/td&gt;
&lt;td&gt;90 days&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;These thresholds and deadlines are public record, not proprietary knowledge. Any company can and does file earlier than its deadline — but a meaningful share file close to it, because compiling audited financials, running the audit committee process, and drafting MD&amp;amp;A takes real time. The deadline is a ceiling companies work toward, not a floor they clear early by default.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why the Deadline Creates a Point-in-Time Gap
&lt;/h2&gt;

&lt;p&gt;Here's the mechanical consequence: a fiscal year that ends December 31 might not have its 10-K publicly filed until late February (large accelerated), mid-March (accelerated), or the end of March (non-accelerated). That's a 60-to-90-day window baked into the regulatory structure, before you even account for filers who use extensions (Form 12b-25) or file 10-K/A amendments later.&lt;/p&gt;

&lt;p&gt;So when you see a "FY2023 Revenue" figure, the honest question is: known to the public &lt;em&gt;when&lt;/em&gt;? Not "as of fiscal year-end" — as of the date the 10-K (or amendment) actually hit EDGAR. Any dataset that timestamps fundamentals by fiscal period end instead of filing date is implicitly assuming zero-day disclosure, which the SEC's own deadlines show doesn't happen.&lt;/p&gt;

&lt;h2&gt;
  
  
  From Filing Date to "First Known" Value
&lt;/h2&gt;

&lt;p&gt;This is why point-in-time (PIT) datasets track more than just the number. Tradevo Data's schema, sourced directly from SEC EDGAR (public domain), records for each row:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;first_filed&lt;/strong&gt; — the date the value first became public&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;original_value&lt;/strong&gt; — the first-reported figure (the point-in-time-safe one)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;latest_value&lt;/strong&gt; — the current, possibly-restated figure&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;restated flag&lt;/strong&gt; — set when a later filing changes the same tag by more than 0.5%, including amendments&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;qa_status&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Across the dataset, 18,723 restatements carry the restated flag — a reminder that "the number" for a given fiscal year isn't fixed at first filing; it can move, and a PIT-correct backtest needs to use what was known on a given date, not what we know now with hindsight.&lt;/p&gt;

&lt;h2&gt;
  
  
  What We Actually Measured (Scoped Honestly)
&lt;/h2&gt;

&lt;p&gt;On the free 40-company sample's reliable-filing rows, the measured lookahead — the gap between fiscal period end and first_filed — averaged 43.4 days, with a maximum of 61 days. That number is specific to the 40-company sample and should not be quoted as a property of the full 5,189-company, 312,751-row dataset, where the same gap is wider — mean 66 days, median 60, p90 90 on reliable-filing rows — because large caps are the fastest filers. We're not going to blur that line for a cleaner headline.&lt;/p&gt;

&lt;h2&gt;
  
  
  When a Bigger or Quarterly-Aware Vendor Is the Better Choice
&lt;/h2&gt;

&lt;p&gt;In the interest of not overselling: if your strategy needs quarterly (10-Q) fundamentals, non-US equities, Parquet delivery, or a vendor with a longer operating track record and broader SLA guarantees, providers like Sharadar, Tiingo, or QuantConnect are worth evaluating — see their pricing pages directly, since we won't quote competitor prices here. QuantConnect in particular is worth a look if you want fundamentals data pre-integrated into a backtesting engine rather than delivered as a standalone feed. Tradevo Data is annual-only (10-K plus 10-K/A) for now, US-only, and delivered as JSON/CSV — quarterly and Parquet are roadmap items, not shipped features.&lt;/p&gt;

&lt;h2&gt;
  
  
  When to Build It Yourself from EDGAR
&lt;/h2&gt;

&lt;p&gt;EDGAR's raw filings and XBRL data are free and public. If you only need a handful of tickers, have engineering time to spare, and want full control over parsing logic, building your own extractor is a legitimate option. The tradeoffs to budget for: XBRL taxonomy changes across years, handling 10-K/A amendments correctly so you don't silently overwrite original_value with latest_value, and building the first_filed logic so your backtest can't see a number before it existed. None of this is exotic, but it's also not a weekend project if you want it PIT-correct rather than just "mostly right."&lt;/p&gt;

&lt;h2&gt;
  
  
  Where Tradevo Data Fits
&lt;/h2&gt;

&lt;p&gt;Tradevo Data exists for the middle case: you want SEC-sourced, point-in-time-correct annual fundamentals — Revenue, NetIncome, Assets, StockholdersEquity, OperatingCashFlow, EPSDiluted, DilutedShares, up to 12 fiscal years — across 5,189 US companies (312,751 rows), without building the EDGAR pipeline yourself or paying for a full institutional-grade platform. The $29/mo plan includes the full bulk &lt;code&gt;/v1/download&lt;/code&gt;, the whole-universe &lt;code&gt;/v1/snapshot?as_of&lt;/code&gt; cross-section, and the &lt;code&gt;/v1/fundamentals?ticker&amp;amp;as_of&lt;/code&gt; JSON endpoint — 5,000 requests/day and 2,500 distinct tickers/day; use &lt;code&gt;/v1/snapshot&lt;/code&gt; or &lt;code&gt;/v1/download&lt;/code&gt; for cross-sections — with a Stripe checkout that issues a key instantly. Cancel anytime. Docs are at /docs.&lt;/p&gt;

&lt;p&gt;This is a budget tier of research-grade PIT data — not a claim to be the only cheap option, and not a promise about what it will do for your returns.&lt;/p&gt;

&lt;h2&gt;
  
  
  Try Before You Subscribe
&lt;/h2&gt;

&lt;p&gt;The free sample — 40 companies, 3,280 rows, full methodology, no signup — is on GitHub: github.com/christianpichichero-max/pit-fundamentals. It's the same schema, same first_filed/original_value/restated logic, just smaller. If the filing-deadline mechanics above are new to you, the deeper walkthroughs are at /blog/lookahead-bias-fundamental-backtests and /blog/point-in-time-fundamentals-data.&lt;/p&gt;

&lt;p&gt;If the schema fits your backtests, the full dataset is at tradevodata.com/?ref=blog. If you're still comparing PIT vendors, &lt;a href="https://tradevodata.com/alternatives?ref=blog&amp;amp;utm_source=devto&amp;amp;utm_medium=syndication&amp;amp;utm_campaign=syndicate&amp;amp;utm_content=sec-10-k-filing-deadlines-and-point-in-time-data" rel="noopener noreferrer"&gt;our comparison index&lt;/a&gt; lays out the options side by side.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Not investment advice; verify competitor pricing yourself.&lt;/em&gt;&lt;/p&gt;

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