<?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: Tomozo / HAWK Backtester</title>
    <description>The latest articles on DEV Community by Tomozo / HAWK Backtester (@hawkbtcommander).</description>
    <link>https://dev.to/hawkbtcommander</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%2F4176258%2Fc12f7cca-4b70-4bd6-9748-8116f38b30f3.png</url>
      <title>DEV Community: Tomozo / HAWK Backtester</title>
      <link>https://dev.to/hawkbtcommander</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/hawkbtcommander"/>
    <language>en</language>
    <item>
      <title>I let Claude Code search for a trading strategy, but hid the last two years of data from it</title>
      <dc:creator>Tomozo / HAWK Backtester</dc:creator>
      <pubDate>Thu, 22 Oct 2026 13:45:02 +0000</pubDate>
      <link>https://dev.to/hawkbtcommander/i-let-claude-code-search-for-a-trading-strategy-but-hid-the-last-two-years-of-data-from-it-48ee</link>
      <guid>https://dev.to/hawkbtcommander/i-let-claude-code-search-for-a-trading-strategy-but-hid-the-last-two-years-of-data-from-it-48ee</guid>
      <description>&lt;p&gt;I gave Claude Code ten years of USD/JPY daily bars and asked it to find a trading rule. It ran backtests through an API, as many as it wanted up to 30, but the last 20% of the data was hidden from it the whole time. At the end it got exactly one look at the hidden part, and it had to write down what it expected before it looked.&lt;/p&gt;

&lt;p&gt;The point wasn't to find a strategy. It was to see what happens to "the best backtest an AI found" when the AI can't quietly try its way into it.&lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Data:&lt;/strong&gt; USD/JPY daily, April 2015 to April 2025, 2,604 bars. The last 520 bars (from 2023-04-26) were cut off on the server, so the agent's runs never received those prices.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Agent:&lt;/strong&gt; Claude Opus 5.5 in Claude Code, 2026-10-19, working through Hawk Backtester's Agent API. It wrote each strategy as a Python file and ran it with a small script.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Rules:&lt;/strong&gt; at most 30 search runs. After each run, a note saying what it changed and why. When done, pick one strategy, write a prediction for the hidden period, then run it once on the hidden period.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The ledger:&lt;/strong&gt; every run goes into one study. Each result comes back with the trial number and a deflated Sharpe ratio: the probability that the run beats the best you'd expect from that many tries of pure noise.&lt;/li&gt;
&lt;/ul&gt;

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

&lt;p&gt;It used 13 of its 30 runs and then stopped on its own: "further searching would mostly have added selection bias."&lt;/p&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;Rule&lt;/th&gt;
&lt;th&gt;Visible return&lt;/th&gt;
&lt;th&gt;Max DD&lt;/th&gt;
&lt;th&gt;Trades&lt;/th&gt;
&lt;th&gt;Deflated Sharpe&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;50-day breakout, fixed stop and target, 1,000 units&lt;/td&gt;
&lt;td&gt;−53.9%&lt;/td&gt;
&lt;td&gt;−55%&lt;/td&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;0.13&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;same, sized to risk 2% per trade&lt;/td&gt;
&lt;td&gt;+14.6%&lt;/td&gt;
&lt;td&gt;−16.2%&lt;/td&gt;
&lt;td&gt;35&lt;/td&gt;
&lt;td&gt;0.06&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;50-day breakout, 3-yen trailing stop, no target&lt;/td&gt;
&lt;td&gt;+12.0%&lt;/td&gt;
&lt;td&gt;−7.5%&lt;/td&gt;
&lt;td&gt;42&lt;/td&gt;
&lt;td&gt;0.003&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;trailing stop scaled to recent volatility&lt;/td&gt;
&lt;td&gt;−7.3%&lt;/td&gt;
&lt;td&gt;−21.4%&lt;/td&gt;
&lt;td&gt;75&lt;/td&gt;
&lt;td&gt;0.0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;#3 plus a 200-day trend filter&lt;/td&gt;
&lt;td&gt;+6.2%&lt;/td&gt;
&lt;td&gt;−9.5%&lt;/td&gt;
&lt;td&gt;37&lt;/td&gt;
&lt;td&gt;0.002&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;6&lt;/td&gt;
&lt;td&gt;6-month momentum, always in the market&lt;/td&gt;
&lt;td&gt;+14.6%&lt;/td&gt;
&lt;td&gt;−21.0%&lt;/td&gt;
&lt;td&gt;62&lt;/td&gt;
&lt;td&gt;0.0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;7&lt;/td&gt;
&lt;td&gt;hold when 3 of 4 lookbacks agree&lt;/td&gt;
&lt;td&gt;−2.4%&lt;/td&gt;
&lt;td&gt;−26.2%&lt;/td&gt;
&lt;td&gt;123&lt;/td&gt;
&lt;td&gt;0.0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;8&lt;/td&gt;
&lt;td&gt;12-month momentum, checked monthly&lt;/td&gt;
&lt;td&gt;+14.4%&lt;/td&gt;
&lt;td&gt;−15.7%&lt;/td&gt;
&lt;td&gt;16&lt;/td&gt;
&lt;td&gt;0.04&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;9&lt;/td&gt;
&lt;td&gt;buy 5-day lows above the 200-day average&lt;/td&gt;
&lt;td&gt;−5.5%&lt;/td&gt;
&lt;td&gt;−17.1%&lt;/td&gt;
&lt;td&gt;119&lt;/td&gt;
&lt;td&gt;0.0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;10–13&lt;/td&gt;
&lt;td&gt;#3 with a 100-day / 20-day lookback, 2-yen / 4-yen trail&lt;/td&gt;
&lt;td&gt;−3.3% to +9.9%&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;32–73&lt;/td&gt;
&lt;td&gt;≤ 0.007&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Run 1 was a sizing bug, not a test of the signal, and the agent said so in its next note. Runs 10 to 13 weren't new ideas; it was checking whether #3 sat on a plateau or a spike. The lookback held up (20, 50 and 100 days were all positive). The trailing-stop distance didn't (2 yen lost money, 4 yen made a third as much), and it noted that "3 yen is partly a lucky value."&lt;/p&gt;

&lt;p&gt;Here is the dashboard's research note at the end. The labels are Japanese: hidden period 20% (520 bars), hidden period used once, 13 trials, and the run list with each trial's deflated Sharpe in the last column.&lt;/p&gt;

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

&lt;h2&gt;
  
  
  What the ledger said
&lt;/h2&gt;

&lt;p&gt;Every search run came back "within chance, given the number of trials." The luck threshold (the per-trade Sharpe you'd expect from the best of N noise strategies) jumped to about 0.5 by the third trial and stayed between 0.47 and 0.53. The pick, #3, had a per-trade Sharpe of 0.127 and a deflated Sharpe of 0.003.&lt;/p&gt;

&lt;p&gt;The agent agreed with it. From its notes before the final run:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Ledger says chance, and I agree: I expect roughly break-even, not a confirmation.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  The prediction, then the hidden period
&lt;/h2&gt;

&lt;p&gt;Written before the run: &lt;strong&gt;about +1%&lt;/strong&gt; over the hidden two years (range −6% to +8%), max drawdown about 6%, about 10 trades, shorts losing money again.&lt;/p&gt;

&lt;p&gt;What happened:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Equity over the hidden period:&lt;/strong&gt; +1.2% (11,201 at the cut to 11,334), max drawdown −7.4%.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Trades entered in the hidden period:&lt;/strong&gt; 24, net +245, profit factor 1.31, win rate 46%, per-trade Sharpe 0.065 (about half the visible 0.127).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Trade count:&lt;/strong&gt; missed. It expected about 10 and got 24. The hidden period had far more breakouts and stop-outs than the visible average.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Its own summary:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;The pick didn't fall apart, but it didn't prove anything either. It earned a small positive return in the hidden period, per trade about half as good as on the visible data. That fits a weak edge, or no edge plus some luck. I would not call this a working strategy.&lt;/p&gt;
&lt;/blockquote&gt;

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

&lt;p&gt;The interesting part isn't the strategy. It's that the agent didn't oversell it. Without the ledger, the story writes itself: "AI finds a USD/JPY breakout system, +12% with a 7.5% drawdown." With the ledger in every response, the agent kept reading "within chance," stopped searching early, ran sensitivity checks instead of new ideas, and predicted the hidden period close to what happened.&lt;/p&gt;

&lt;p&gt;Two things I changed after watching it:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The holdout response mixed the periods.&lt;/strong&gt; A holdout run reported a total return of +13.3% because the outcome covered the whole run. The agent noticed and worked out the hidden part itself. Holdout runs now also return &lt;code&gt;research.hidden_period&lt;/code&gt; with the equity change and drawdown for the hidden period alone.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Agent Mode was broken in production&lt;/strong&gt; before this run (the browser runner rejected every session with "runId is missing"), and a failure in the browser came back to the agent as an empty success. Both are fixed: a browser-side failure now comes back as &lt;code&gt;failed&lt;/code&gt; with the message, and a run no browser picks up says so after 30 seconds.&lt;/li&gt;
&lt;/ul&gt;

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

&lt;ul&gt;
&lt;li&gt;One dataset, one period, one model. Daily bars, no swap or carry costs.&lt;/li&gt;
&lt;li&gt;The model's training data includes general knowledge of USD/JPY history up to 2025. It said so in its summary, and its picks followed the visible runs, but I can't rule out that it helped.&lt;/li&gt;
&lt;li&gt;A 2-year holdout can't confirm a weak edge. All this shows is that this pick wasn't an overfit illusion that collapsed.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The ledger, the hidden period and the single holdout are part of &lt;a href="https://app.hawk-backtester.com/en/agent-manual/?utm_source=devto&amp;amp;utm_medium=article&amp;amp;utm_campaign=agent_holdout" rel="noopener noreferrer"&gt;Hawk Backtester's Agent Mode&lt;/a&gt;. Strategies are plain Python files that run on your machine (&lt;code&gt;pip install hawk-bt&lt;/code&gt;).&lt;/p&gt;

&lt;p&gt;If you let agents run backtests, what do you use to keep them from picking the lucky run?&lt;/p&gt;

</description>
      <category>ai</category>
      <category>python</category>
      <category>trading</category>
      <category>testing</category>
    </item>
    <item>
      <title>A reader said my "exit problem" detector could be fooled by noise. I added a random-entry control</title>
      <dc:creator>Tomozo / HAWK Backtester</dc:creator>
      <pubDate>Mon, 19 Oct 2026 13:50:05 +0000</pubDate>
      <link>https://dev.to/hawkbtcommander/a-reader-said-my-exit-problem-detector-could-be-fooled-by-noise-i-added-a-random-entry-control-1bb1</link>
      <guid>https://dev.to/hawkbtcommander/a-reader-said-my-exit-problem-detector-could-be-fooled-by-noise-i-added-a-random-entry-control-1bb1</guid>
      <description>&lt;p&gt;My MT5 analyser splits losing trades into two kinds: the ones that were in profit first (an exit problem) and the ones that went wrong right away (an entry problem). A reader, &lt;a class="mentioned-user" href="https://dev.to/arhancanli"&gt;@arhancanli&lt;/a&gt;, left a comment on an earlier post that took that split apart in two sentences:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;With a stop of 1% and noisy bars, a random entry will often show a small positive MFE before it is stopped out, so "most losers were once in profit" can come out high even when the entry has no edge.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;They suggested two fixes: measure MFE as a multiple of the stop distance, and compare against the same exits applied to random entry times. If the real losers don't beat the random ones, it isn't an exit problem.&lt;/p&gt;

&lt;p&gt;That was right, so I built it. This is what came out, including the part where I got it wrong first.&lt;/p&gt;

&lt;h2&gt;
  
  
  The control
&lt;/h2&gt;

&lt;p&gt;For each losing trade, the tool replays 100 entries at random bar opens, with:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;the same side (buy or sell),&lt;/li&gt;
&lt;li&gt;the same loss distance, used as a stop,&lt;/li&gt;
&lt;li&gt;the same holding time.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;When one bar reaches both the stop and a new high, the stop is taken first, so random entries aren't flattered. Real trades are measured without the range of the bar they opened in (the fill was somewhere inside it), so the random ones are measured the same way.&lt;/p&gt;

&lt;p&gt;The random trades that end in a loss are then classified with the same rule as the real ones: "in profit first" if the open profit reached at least half the loss distance, "wrong right away" if it stayed under 15% of it. Then a one-sided test (about 95%) asks whether the real share is higher than the random share.&lt;/p&gt;

&lt;p&gt;If it isn't, the tool no longer says "exit problem" or "entry problem". It says the share is no higher than random entries give, and drops the suggestion that goes with it.&lt;/p&gt;

&lt;h2&gt;
  
  
  The part I got wrong
&lt;/h2&gt;

&lt;p&gt;My first version measured each random trade's open profit against its &lt;em&gt;own&lt;/em&gt; loss. A random trade that timed out down 2 pips with 3 pips of open profit along the way counted as "in profit first". On a test with flat, quiet bars, 98% of random losers came out as exit problems.&lt;/p&gt;

&lt;p&gt;On the bundled sample, that version said the exit finding was noise too: 34% for the real losers, 43% for random entries.&lt;/p&gt;

&lt;p&gt;The fix is what the comment actually said: measure against the real loser's loss distance, like a multiple of R. A random trade only counts as "in profit first" if it went as far your way as the real trade's loss was.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the sample says now
&lt;/h2&gt;

&lt;p&gt;The sample is a breakout EA on synthetic EURUSD M15 data, 136 trades, 90 losers.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Kind of loser&lt;/th&gt;
&lt;th&gt;This EA&lt;/th&gt;
&lt;th&gt;Random entries&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;In profit first&lt;/td&gt;
&lt;td&gt;34%&lt;/td&gt;
&lt;td&gt;25%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Went wrong right away&lt;/td&gt;
&lt;td&gt;33%&lt;/td&gt;
&lt;td&gt;39%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Went wrong right away (3 pips cost)&lt;/td&gt;
&lt;td&gt;44%&lt;/td&gt;
&lt;td&gt;52%&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The exit finding holds (z ≈ 1.96). The entry finding doesn't: random entries go wrong right away at least as often as this EA does.&lt;/p&gt;

&lt;p&gt;That second line changed something I had published. The guide on reading MT5 reports said that once you subtract a 3-pip cost, this EA's losses "lean towards the entry" (40 entry problems against 22 exit problems). Without a control, that sounded fair. With one, 44% against 52% for random entries says the opposite: the entries are no worse than random. The cost moves both groups the same way. I took the sentence out.&lt;/p&gt;

&lt;h2&gt;
  
  
  Other things the comments changed
&lt;/h2&gt;

&lt;p&gt;The same reader left a second comment on the post about re-running on save, and a few smaller things came out of both:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Same-bar ties.&lt;/strong&gt; When a bar touches both the target and the stop, the engine assumes the stop came first. The exit analysis now counts those trades and shows how much profit would change if the target had come first. On the demo's hourly bars with 1% levels the count is 0, since an hourly bar there moves far less than the 2% between the levels. On tighter levels it won't be.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Size-only changes in the diff.&lt;/strong&gt; The strategy in that post sizes from equity, so one changed exit resizes every later trade. The diff only counted a trade as "changed" when its exit moved, so those weren't in the count, but the PnL line mixed them in. They now have their own line: same bars, size changed, with their PnL.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Heatmaps say how many cells the best one was picked from.&lt;/strong&gt; More cells means a higher best cell by luck alone.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Limits of the control
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Random entries are drawn from the whole test period. If your EA only trades London open, the control doesn't match that. A control matched on time of day is the obvious next step.&lt;/li&gt;
&lt;li&gt;With 90 losers the test is rough. It can tell "clearly more than random" from "not clearly", but a share 5 points above random won't pass.&lt;/li&gt;
&lt;li&gt;It says whether the split is real, not what to change. A real exit problem still needs a tested change to the exit, on a different period than the one you found it on.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The analyser is free, runs in the browser, and the report never leaves your machine:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://app.hawk-backtester.com/en/tools/mt5-report/?utm_source=devto&amp;amp;utm_medium=article&amp;amp;utm_campaign=control" rel="noopener noreferrer"&gt;https://app.hawk-backtester.com/en/tools/mt5-report/?utm_source=devto&amp;amp;utm_medium=article&amp;amp;utm_campaign=control&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;If you test EAs, I'd like to know whether a time-of-day matched control would change your numbers, and what else you would compare against.&lt;/p&gt;

</description>
      <category>trading</category>
      <category>statistics</category>
      <category>javascript</category>
      <category>testing</category>
    </item>
    <item>
      <title>I built a backtester that shows why a trading rule lost, not just how much</title>
      <dc:creator>Tomozo / HAWK Backtester</dc:creator>
      <pubDate>Tue, 13 Oct 2026 13:40:02 +0000</pubDate>
      <link>https://dev.to/hawkbtcommander/i-built-a-backtester-that-shows-why-a-trading-rule-lost-not-just-how-much-17n9</link>
      <guid>https://dev.to/hawkbtcommander/i-built-a-backtester-that-shows-why-a-trading-rule-lost-not-just-how-much-17n9</guid>
      <description>&lt;p&gt;Most backtests end with one number. Total return, maybe a drawdown and a win rate. I kept getting stuck at that point: the number told me a rule was bad, but not which part of it to change.&lt;/p&gt;

&lt;p&gt;So I built Hawk Backtester around a different question. Not "how much did this rule make", but "why did it win, and why did it lose". This is the first time I'm showing it outside Japan, so here is the whole thing, including what it can't do yet.&lt;/p&gt;

&lt;h2&gt;
  
  
  Same total, opposite fixes
&lt;/h2&gt;

&lt;p&gt;Two strategies can both end at -2.8% and need completely different changes.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;In one, most losing trades were in profit at some point, then reversed and hit the stop. That's an exit problem.&lt;/li&gt;
&lt;li&gt;In the other, most losers were never in profit at all. That's an entry problem, often entering in the wrong kind of market.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you only look at the total, you tweak the take profit in both cases, and in one of them you're fixing the part that wasn't broken. Everything in the app is built to make that split visible.&lt;/p&gt;

&lt;h2&gt;
  
  
  Try it without signing up
&lt;/h2&gt;

&lt;p&gt;The demo runs in your browser with no account. The engine is C++ compiled to WebAssembly, and a 1,500-bar run takes about 15 milliseconds, so it re-runs every time you move a slider.&lt;/p&gt;

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

&lt;p&gt;It uses synthetic USD/JPY-like hourly data where trends, ranges and shocks take turns. Pick a market type and a built-in strategy (MA cross, Donchian breakout, Bollinger, RSI) and move the sliders.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://app.hawk-backtester.com/en/demo/?utm_source=devto&amp;amp;utm_medium=article&amp;amp;utm_campaign=showdev" rel="noopener noreferrer"&gt;https://app.hawk-backtester.com/en/demo/?utm_source=devto&amp;amp;utm_medium=article&amp;amp;utm_campaign=showdev&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What it tells you about a run
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Which markets it won and lost in.&lt;/strong&gt; Trades are grouped by the market regime at entry. The same breakout rule made about +10% in trending data and lost about 6% in ranging data. The total hid that completely.&lt;/p&gt;

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

&lt;p&gt;&lt;strong&gt;Whether the entry or the exit is the problem.&lt;/strong&gt; Every trade records its best open profit (MFE), its worst open loss (MAE) and why it closed: take profit, stop loss or the opposite signal. A few hints are generated from those trades, and clicking one filters the trade list down to the trades behind it.&lt;/p&gt;

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

&lt;p&gt;&lt;strong&gt;Whether a change really helped.&lt;/strong&gt; Re-run after a change and the app shows which trades changed compared to the previous run. When I moved a take profit from 1% to 2%, the trade count stayed at 60, but 20 trades ended differently and about half of those got worse. The total said "slightly better".&lt;/p&gt;

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

&lt;p&gt;&lt;strong&gt;Whether a parameter is just luck.&lt;/strong&gt; Sweep two parameters and you get a heatmap. The average of the best cell's neighbours is shown next to it. If the neighbours are good too, it's a plateau. If they aren't, the best cell was probably noise.&lt;/p&gt;

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

&lt;p&gt;There's also a button that sends the aggregated numbers (not your code, not raw data) to an LLM and gets back what to look at and what to test next. It's told not to give trading advice.&lt;/p&gt;

&lt;h2&gt;
  
  
  Your own strategy, in Python
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pip &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-U&lt;/span&gt; hawk-bt
hawk-bt run my_strategy.py
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;hawk_bt&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Strategy&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Context&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Param&lt;/span&gt;

&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;MaCross&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Strategy&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;fast&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Param&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="nb"&gt;min&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;max&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;50&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;step&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;slow&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Param&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;50&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;min&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;max&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;step&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;step&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Context&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="p"&gt;...&lt;/span&gt;  &lt;span class="c1"&gt;# read ctx.state, call ctx.engine.place_ticket(...)
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Your Python process talks to the browser over a localhost websocket, so the strategy code never leaves your machine. Every &lt;code&gt;Param&lt;/code&gt; shows up as a slider, and saving the file re-runs the backtest and shows the diff against the previous run.&lt;/p&gt;

&lt;h2&gt;
  
  
  How fills work
&lt;/h2&gt;

&lt;p&gt;I found four fill-model bugs in my own engine. Fixing them moved results by up to 4% and changed which strategy ranked first, so I write the assumptions down now:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;TP and SL fill at their level, or at the open if the bar gaps through&lt;/li&gt;
&lt;li&gt;if one bar touches both, the stop is assumed to come first&lt;/li&gt;
&lt;li&gt;spread, fees and slippage are charged on every fill&lt;/li&gt;
&lt;li&gt;you can enter at the signal bar's close or the next bar's open&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://app.hawk-backtester.com/en/simulation-model/?utm_source=devto&amp;amp;utm_medium=article&amp;amp;utm_campaign=showdev" rel="noopener noreferrer"&gt;https://app.hawk-backtester.com/en/simulation-model/?utm_source=devto&amp;amp;utm_medium=article&amp;amp;utm_campaign=showdev&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Data
&lt;/h2&gt;

&lt;p&gt;No market data is bundled. You can upload a CSV, generate synthetic data, or import bars with your own data-provider API key (Twelve Data, for example). The key is stored encrypted and only used for your imports.&lt;/p&gt;

&lt;h2&gt;
  
  
  What it can't do yet
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;one instrument per backtest, no portfolios&lt;/li&gt;
&lt;li&gt;bar data only, no ticks&lt;/li&gt;
&lt;li&gt;the app is built for desktop; on a phone only the demo really works&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Pricing
&lt;/h2&gt;

&lt;p&gt;The free plan has unlimited backtests (built-in strategies, Python and watch mode), 5 saved results, sweeps up to 25 runs and 3 AI summaries a month. Paid plans raise those limits.&lt;/p&gt;

&lt;p&gt;I also wrote up the ideas behind each view: &lt;a href="https://app.hawk-backtester.com/en/guides/mfe-mae/?utm_source=devto&amp;amp;utm_medium=article&amp;amp;utm_campaign=showdev" rel="noopener noreferrer"&gt;MFE and MAE&lt;/a&gt;, &lt;a href="https://app.hawk-backtester.com/en/guides/market-regime/?utm_source=devto&amp;amp;utm_medium=article&amp;amp;utm_campaign=showdev" rel="noopener noreferrer"&gt;splitting results by regime&lt;/a&gt; and &lt;a href="https://app.hawk-backtester.com/en/guides/overfitting/?utm_source=devto&amp;amp;utm_medium=article&amp;amp;utm_campaign=showdev" rel="noopener noreferrer"&gt;avoiding overfitting&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://app.hawk-backtester.com/en/?utm_source=devto&amp;amp;utm_medium=article&amp;amp;utm_campaign=showdev" rel="noopener noreferrer"&gt;https://app.hawk-backtester.com/en/?utm_source=devto&amp;amp;utm_medium=article&amp;amp;utm_campaign=showdev&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Almost nobody uses this yet. If you backtest in Python today, what do you look at after the total return, and what would you need to see before trying this on a real strategy?&lt;/p&gt;

</description>
      <category>python</category>
      <category>showdev</category>
      <category>webassembly</category>
      <category>fintech</category>
    </item>
    <item>
      <title>Is your MT5 EA losing on its entries or its exits? A free report analyser</title>
      <dc:creator>Tomozo / HAWK Backtester</dc:creator>
      <pubDate>Sun, 11 Oct 2026 23:33:18 +0000</pubDate>
      <link>https://dev.to/hawkbtcommander/is-your-mt5-ea-losing-on-its-entries-or-its-exits-a-free-report-analyser-4de2</link>
      <guid>https://dev.to/hawkbtcommander/is-your-mt5-ea-losing-on-its-entries-or-its-exits-a-free-report-analyser-4de2</guid>
      <description>&lt;p&gt;The MT5 Strategy Tester gives you a lot of numbers: profit factor, drawdown, win rate, consecutive losses. What it doesn't tell you is which part of the EA to change. A profit factor of 0.58 says the EA is losing. It doesn't say whether the entries are wrong or the exits are.&lt;/p&gt;

&lt;p&gt;So I made a small free tool that reads the Strategy Tester's HTML report and answers that question. It runs in the browser, needs no account, and the report never leaves your machine.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://app.hawk-backtester.com/en/tools/mt5-report/?utm_source=devto&amp;amp;utm_medium=article&amp;amp;utm_campaign=mt5" rel="noopener noreferrer"&gt;https://app.hawk-backtester.com/en/tools/mt5-report/?utm_source=devto&amp;amp;utm_medium=article&amp;amp;utm_campaign=mt5&lt;/a&gt;&lt;/p&gt;

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

&lt;h2&gt;
  
  
  Two kinds of losing trade
&lt;/h2&gt;

&lt;p&gt;Every losing trade belongs somewhere on a line between two cases.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;It was in profit first.&lt;/strong&gt; The price moved your way, then came back and hit the stop. The entry was fine; the exit gave it away. Work on the take profit, a trailing stop or moving the stop to break even.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;It went wrong right away.&lt;/strong&gt; The price never moved your way. No exit rule would have saved it. Work on the entry: the signal itself, or the market conditions you enter in.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;To tell them apart you need each trade's best open profit (MFE) and worst open loss (MAE). The MT5 report doesn't have those, so the tool asks for one more file: a bar CSV exported from MT5 for the same symbol and period. With the bars it walks every trade from entry to exit and records how far it went each way.&lt;/p&gt;

&lt;p&gt;On the sample report (a breakout EA on synthetic EURUSD M15 data), 90 of 136 trades lost:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;31 had been in profit first, by 0.00204 on average, before losing. Exit problem.&lt;/li&gt;
&lt;li&gt;30 were barely ever in profit. Entry problem.&lt;/li&gt;
&lt;li&gt;29 were somewhere in between.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That is an even split, which is itself useful: changing only the take profit would address a third of the losses at most.&lt;/p&gt;

&lt;h2&gt;
  
  
  Is the stop wider than it needs to be?
&lt;/h2&gt;

&lt;p&gt;The tool also looks at the winners. In the sample, 90% of winning trades never went more than 0.00218 against you, and the median stop was 0.00250, so the stop is not wider than winners needed. If it were 0.00500, you would be paying for room no winner used.&lt;/p&gt;

&lt;p&gt;This check can't tell you a stop is too tight, though. A trade that needed more room was stopped out and counts as a loser, so it never shows up among the winners. (Updated after a reader pointed this out in the comments.)&lt;/p&gt;

&lt;h2&gt;
  
  
  Where it makes and loses money
&lt;/h2&gt;

&lt;p&gt;With the bars, each trade also gets the market regime just before entry: uptrend, downtrend or range, from ADX(14) on hourly bars. In the sample, ranging markets accounted for 93 trades and -685 of the -965 total. For a breakout EA, that points to a filter rather than a new exit rule.&lt;/p&gt;

&lt;p&gt;There are also tables by exit reason, holding time and hour of day, and every finding links to the trades behind it.&lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;The report is UTF-16 HTML with labels in whatever language your terminal uses. The parser finds the deals table by its shape (13 columns, buy/sell and in/out values) rather than by header names, so a Japanese or German report works the same as an English one.&lt;/li&gt;
&lt;li&gt;Deals are rebuilt into trades first in, first out, so partial closes and reversals become separate trades.&lt;/li&gt;
&lt;li&gt;The order of prices inside a bar is unknown, so MFE and MAE use the highs and lows of bars that lie entirely between entry and exit. A trade opened mid-bar would otherwise be credited with moves from before it existed, so for the entry and exit bars only the fill prices count. That errs low; M1 or M5 bars recover more of the path. (Also changed after the same comment.)&lt;/li&gt;
&lt;li&gt;Everything runs in plain JavaScript in the page. Nothing is uploaded unless you sign in and choose to save.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Comparing two versions of an EA
&lt;/h2&gt;

&lt;p&gt;If you sign in (free), you can save reports and compare two of them. Trades are matched by side and entry time, so you can see which trades a change actually affected: which ones it saved, which ones it broke, and which were new. When you tweak an EA, the total usually moves a little; the per-trade view shows whether that came from the change you meant to make.&lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;MT5 Strategy Tester HTML reports only. MT4 is not supported yet.&lt;/li&gt;
&lt;li&gt;Without the bar CSV you get the basic statistics and exit reasons, but not MFE/MAE or regimes.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you build EAs, I would like to know whether this matches how you debug them, and whether MT4 support would matter to you.&lt;/p&gt;

&lt;p&gt;This tool is part of &lt;a href="https://app.hawk-backtester.com/en/?utm_source=devto&amp;amp;utm_medium=article&amp;amp;utm_campaign=mt5" rel="noopener noreferrer"&gt;Hawk Backtester&lt;/a&gt;, a backtester built around the question of why a rule won or lost. The ideas behind the split are in the guide on &lt;a href="https://app.hawk-backtester.com/en/guides/mfe-mae/?utm_source=devto&amp;amp;utm_medium=article&amp;amp;utm_campaign=mt5" rel="noopener noreferrer"&gt;MFE and MAE&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>showdev</category>
      <category>trading</category>
      <category>javascript</category>
      <category>fintech</category>
    </item>
    <item>
      <title>My backtest re-runs when I save the strategy file, and shows which trades changed</title>
      <dc:creator>Tomozo / HAWK Backtester</dc:creator>
      <pubDate>Sun, 11 Oct 2026 04:03:41 +0000</pubDate>
      <link>https://dev.to/hawkbtcommander/my-backtest-re-runs-when-i-save-the-strategy-file-and-shows-which-trades-changed-13cd</link>
      <guid>https://dev.to/hawkbtcommander/my-backtest-re-runs-when-i-save-the-strategy-file-and-shows-which-trades-changed-13cd</guid>
      <description>&lt;p&gt;I'm building a backtesting web app called Hawk Backtester on the side. This post is about one feature, a watch mode for local Python strategies.&lt;/p&gt;

&lt;p&gt;The loop that annoyed me most was edit the strategy, run it in a terminal, open the results, then try to remember what the previous run looked like. The last step was the worst. If PnL went up a little, I couldn't tell whether my change did it or a couple of trades just happened to land differently.&lt;/p&gt;

&lt;p&gt;So now the backtest runs again the moment I save, and it tells me which trades changed.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fh5t1fqh0zy8vwkwu3vme.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fh5t1fqh0zy8vwkwu3vme.png" alt="Moving a slider or saving the file re-runs the backtest" width="800" height="500"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Using it
&lt;/h2&gt;

&lt;p&gt;On your machine:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pip &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-U&lt;/span&gt; hawk-bt
hawk-bt run my_strategy.py
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A strategy looks like this. Anything declared with &lt;code&gt;Param&lt;/code&gt; shows up in the browser as a slider.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;hawk_bt&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Strategy&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Context&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Param&lt;/span&gt;

&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;MaCross&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Strategy&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;fast&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Param&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="nb"&gt;min&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;max&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;50&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;step&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;slow&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Param&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;50&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;min&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;max&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;step&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;step&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Context&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;snap&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;snapshot&lt;/span&gt;
        &lt;span class="n"&gt;close&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;candles&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;close&lt;/span&gt;&lt;span class="p"&gt;[:&lt;/span&gt; &lt;span class="nf"&gt;int&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;snap&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;step&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt;  &lt;span class="c1"&gt;# last element is the current bar
&lt;/span&gt;        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;close&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;slow&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt;
        &lt;span class="n"&gt;fast&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;slow&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;close&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;fast&lt;/span&gt;&lt;span class="p"&gt;:].&lt;/span&gt;&lt;span class="nf"&gt;mean&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="n"&gt;close&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;slow&lt;/span&gt;&lt;span class="p"&gt;:].&lt;/span&gt;&lt;span class="nf"&gt;mean&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="n"&gt;prev_fast&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;prev_slow&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;close&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;fast&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;mean&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="n"&gt;close&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;slow&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;mean&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;prev_fast&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="n"&gt;prev_slow&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;fast&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;slow&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;side&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;buy&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
        &lt;span class="k"&gt;elif&lt;/span&gt; &lt;span class="n"&gt;prev_fast&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="n"&gt;prev_slow&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;fast&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;slow&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;side&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;sell&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
        &lt;span class="k"&gt;else&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;price&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;float&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;close&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
        &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;engine&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;place_ticket&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;side&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;side&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;units&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nf"&gt;int&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;snap&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;price&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
                                      &lt;span class="n"&gt;take_profit&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;price&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mf"&gt;0.01&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;stop_loss&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;price&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mf"&gt;0.005&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In the browser you pick a dataset and press "Connect to Local Strategy" once. After that you just go back to your editor and save. Moving a slider also re-runs your local Python with the new value.&lt;/p&gt;

&lt;p&gt;The diff sits above the results. When I changed the take profit from 1% to 2% and saved, it said: trades 60→60, added 0, removed 0, changed 20, PnL +406.55. Same number of trades, but 20 of them ended differently, and about half of those 20 actually got worse. If I'd only looked at the total I would have called it "a bit better" and moved on.&lt;/p&gt;

&lt;h2&gt;
  
  
  How it's put together
&lt;/h2&gt;

&lt;p&gt;The execution engine is C++ compiled to WebAssembly and runs in the browser. Your strategy runs in your own Python process and talks to the browser over a localhost websocket, so the strategy code is never uploaded. I decided that early, because I wouldn't want to upload my own strategies to somebody's server either.&lt;/p&gt;

&lt;p&gt;The watch part is simple. &lt;code&gt;hawk-bt run&lt;/code&gt; polls the file's mtime, reloads the module when it changes, and asks the browser to start a new run. On each run, Python sends the strategy name and its &lt;code&gt;Param&lt;/code&gt; definitions, and the browser answers with the current slider values. For the diff, trades from the previous run and the new run are matched on entry time and side.&lt;/p&gt;

&lt;p&gt;One bug took me a while. The browser sometimes received the trade log for the same run twice, treated the second copy as "the next run", and every diff came out as zero. Tagging each run with an ID and ignoring repeats fixed it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Two other things in the same screen
&lt;/h2&gt;

&lt;p&gt;You can pick two parameters, sweep the grid, and look at it as a heatmap. The single best cell is easy to overfit, so the average of its neighbours is shown next to it.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fcrko9q9nxqlu2e7h05cs.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fcrko9q9nxqlu2e7h05cs.png" alt="Sweep of lookback x take profit for a channel breakout (sample data)" width="800" height="223"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;In this example the best cell is +17.01%, but its neighbours average +9.31%. The left column (10-bar lookback) is around +15% for every take profit, so the lookback matters much more than the TP here.&lt;/p&gt;

&lt;p&gt;There's also a button that sends the aggregated numbers to an LLM and gets back what to look at and what to try next. Only the aggregates go out, not your code or raw data, and it's told not to give trading advice.&lt;/p&gt;

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

&lt;p&gt;In the run above it picked up the diff too, and its first suggested experiment was to work out why 20 trades changed before comparing the two versions.&lt;/p&gt;

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

&lt;p&gt;One instrument per backtest. Bar data only, no ticks. When TP and SL are both touched inside one bar it assumes the stop came first. Portfolio support is what I'm working on next.&lt;/p&gt;

&lt;h2&gt;
  
  
  Trying it
&lt;/h2&gt;

&lt;p&gt;Honestly almost nobody uses this yet, so even a short "I tried it and X was confusing" helps a lot. There's a demo that needs no account (synthetic data).&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Demo: &lt;a href="https://app.hawk-backtester.com/en/demo/?utm_source=devto&amp;amp;utm_medium=article&amp;amp;utm_campaign=watch_mode" rel="noopener noreferrer"&gt;https://app.hawk-backtester.com/en/demo/?utm_source=devto&amp;amp;utm_medium=article&amp;amp;utm_campaign=watch_mode&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;App: &lt;a href="https://app.hawk-backtester.com/en/?utm_source=devto&amp;amp;utm_medium=article&amp;amp;utm_campaign=watch_mode" rel="noopener noreferrer"&gt;https://app.hawk-backtester.com/en/?utm_source=devto&amp;amp;utm_medium=article&amp;amp;utm_campaign=watch_mode&lt;/a&gt; (free plan)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;What would you want the diff to show that it doesn't yet?&lt;/p&gt;

</description>
      <category>python</category>
      <category>webassembly</category>
      <category>showdev</category>
      <category>fintech</category>
    </item>
  </channel>
</rss>
