<?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: laclance</title>
    <description>The latest articles on DEV Community by laclance (@laclance).</description>
    <link>https://dev.to/laclance</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%2F4106457%2Fa5d8e66f-7e83-4d42-8807-03c88c18e41d.jpeg</url>
      <title>DEV Community: laclance</title>
      <link>https://dev.to/laclance</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/laclance"/>
    <language>en</language>
    <item>
      <title>Detecting Support and Resistance Without Lookahead Bias in Go</title>
      <dc:creator>laclance</dc:creator>
      <pubDate>Wed, 02 Sep 2026 15:29:09 +0000</pubDate>
      <link>https://dev.to/laclance/detecting-support-and-resistance-without-lookahead-bias-in-go-akc</link>
      <guid>https://dev.to/laclance/detecting-support-and-resistance-without-lookahead-bias-in-go-akc</guid>
      <description>&lt;p&gt;Support and resistance is easy to draw after the fact.&lt;/p&gt;

&lt;p&gt;Looking at a completed chart, a swing high around $51,000 or repeated lows around $49,000 can seem obvious. The difficulty starts when the same algorithm has to run one candle at a time in a live system.&lt;/p&gt;

&lt;p&gt;At that point, one rule matters more than almost anything else:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A decision at candle N must not depend on candle N+1.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;That sounds trivial. It is surprisingly easy to violate.&lt;/p&gt;

&lt;h2&gt;
  
  
  The hidden lookahead problem
&lt;/h2&gt;

&lt;p&gt;Imagine defining a swing high as a candle whose high is greater than the highs of the three candles before it and the three candles after it.&lt;/p&gt;

&lt;p&gt;That definition is perfectly reasonable for chart analysis.&lt;/p&gt;

&lt;p&gt;But the three candles after the candidate pivot do not exist when the candidate candle closes.&lt;/p&gt;

&lt;p&gt;If a backtest marks that swing high immediately, the strategy has learned something from the future.&lt;/p&gt;

&lt;p&gt;The correct interpretation is different: the swing high may have occurred at candle N, but it only becomes &lt;em&gt;confirmed&lt;/em&gt; after the required later candles have closed.&lt;/p&gt;

&lt;p&gt;That distinction between &lt;strong&gt;pivot time&lt;/strong&gt; and &lt;strong&gt;confirmation time&lt;/strong&gt; is what keeps historical results honest.&lt;/p&gt;

&lt;h2&gt;
  
  
  Start with closed candles
&lt;/h2&gt;

&lt;p&gt;The simplest boundary is to make closed candles the unit of computation.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="k"&gt;type&lt;/span&gt; &lt;span class="n"&gt;Candle&lt;/span&gt; &lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;OpenTime&lt;/span&gt;  &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Time&lt;/span&gt;
    &lt;span class="n"&gt;CloseTime&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Time&lt;/span&gt;
    &lt;span class="n"&gt;Open&lt;/span&gt;      &lt;span class="kt"&gt;float64&lt;/span&gt;
    &lt;span class="n"&gt;High&lt;/span&gt;      &lt;span class="kt"&gt;float64&lt;/span&gt;
    &lt;span class="n"&gt;Low&lt;/span&gt;       &lt;span class="kt"&gt;float64&lt;/span&gt;
    &lt;span class="n"&gt;Close&lt;/span&gt;     &lt;span class="kt"&gt;float64&lt;/span&gt;
    &lt;span class="n"&gt;Volume&lt;/span&gt;    &lt;span class="kt"&gt;float64&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A strategy should not continually reinterpret an unfinished candle as though it were final.&lt;/p&gt;

&lt;p&gt;Instead, a live system can append each newly closed candle and calculate support/resistance from the same type of input used by the backtest.&lt;/p&gt;

&lt;p&gt;That makes the live and historical paths much easier to reason about.&lt;/p&gt;

&lt;h2&gt;
  
  
  Make the candle prefix the contract
&lt;/h2&gt;

&lt;p&gt;Suppose the system has received 500 closed candles.&lt;/p&gt;

&lt;p&gt;Everything calculated at that point should depend only on those 500 candles and the supplied options.&lt;/p&gt;

&lt;p&gt;Given the same prefix again, the result should be deterministic.&lt;/p&gt;

&lt;p&gt;That is the model used by &lt;code&gt;go-sr&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="n"&gt;levels&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;sr&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Compute&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;sr&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Options&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;Timeframe&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;   &lt;span class="s"&gt;"5m"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;Lookback&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;    &lt;span class="m"&gt;120&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;Mode&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;        &lt;span class="n"&gt;sr&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ModeZones&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;MinStrength&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="m"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="no"&gt;nil&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;err&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The result includes the detected levels as well as strategy-oriented metadata:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="n"&gt;levels&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;NearestSupport&lt;/span&gt;
&lt;span class="n"&gt;levels&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;NearestResistance&lt;/span&gt;

&lt;span class="n"&gt;levels&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;NearestSupportDistance&lt;/span&gt;
&lt;span class="n"&gt;levels&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;NearestResistanceDistance&lt;/span&gt;

&lt;span class="n"&gt;levels&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;NearestSupportStrength&lt;/span&gt;
&lt;span class="n"&gt;levels&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;NearestResistanceStrength&lt;/span&gt;

&lt;span class="n"&gt;levels&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;NearSupport&lt;/span&gt;
&lt;span class="n"&gt;levels&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;NearResistance&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This lets strategy code consume support/resistance as data instead of treating it only as something to draw on a chart.&lt;/p&gt;

&lt;h2&gt;
  
  
  Confirmation does not mean immutability
&lt;/h2&gt;

&lt;p&gt;There is an important nuance here.&lt;/p&gt;

&lt;p&gt;“No lookahead” does &lt;strong&gt;not&lt;/strong&gt; mean support and resistance must remain identical forever.&lt;/p&gt;

&lt;p&gt;When new candles close:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;new pivots can become confirmed;&lt;/li&gt;
&lt;li&gt;existing zones can accumulate additional touches;&lt;/li&gt;
&lt;li&gt;the rolling lookback window can move;&lt;/li&gt;
&lt;li&gt;old structure can fall outside that window.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;All of those can legitimately change the current S/R result.&lt;/p&gt;

&lt;p&gt;The requirement is narrower and more useful:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The action you made at time T must have been computable using only information available at time T.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;A later candle is allowed to change tomorrow’s answer. It must not secretly improve yesterday’s answer.&lt;/p&gt;

&lt;h2&gt;
  
  
  Lines versus zones
&lt;/h2&gt;

&lt;p&gt;Support and resistance can also be represented in different ways.&lt;/p&gt;

&lt;p&gt;A line-based implementation might merge nearby pivot prices according to a fixed percentage tolerance.&lt;/p&gt;

&lt;p&gt;That is useful when you want simple, predictable horizontal levels.&lt;/p&gt;

&lt;p&gt;A zone-based implementation recognizes that market structure is rarely exact to the cent. Volatility can be used to determine a more natural region around clustered pivots.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;go-sr&lt;/code&gt; supports both approaches:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="c"&gt;// ATR-aware zones&lt;/span&gt;
&lt;span class="n"&gt;sr&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Options&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;Timeframe&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;   &lt;span class="s"&gt;"5m"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;Lookback&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;    &lt;span class="m"&gt;120&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;Mode&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;        &lt;span class="n"&gt;sr&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ModeZones&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;MinStrength&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="m"&gt;2&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;and:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="c"&gt;// Traditional lines&lt;/span&gt;
&lt;span class="n"&gt;sr&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Options&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;Timeframe&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"5m"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;Lookback&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;  &lt;span class="m"&gt;120&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;Mode&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;      &lt;span class="n"&gt;sr&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ModeLegacy&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;Tolerance&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="m"&gt;0.002&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;Neither representation solves lookahead by itself. The important part is &lt;em&gt;when&lt;/em&gt; the underlying pivots are allowed to influence the result.&lt;/p&gt;

&lt;h2&gt;
  
  
  Multi-timeframe data has the same problem
&lt;/h2&gt;

&lt;p&gt;The same rule applies when aggregating lower-timeframe candles.&lt;/p&gt;

&lt;p&gt;If you are computing hourly S/R from 5-minute data, the current unfinished hourly candle should not be treated as a completed historical candle.&lt;/p&gt;

&lt;p&gt;A useful architecture is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;exchange / historical data
        ↓
closed 5m candles
        ↓
completed higher-timeframe buckets
        ↓
support/resistance computation
        ↓
strategy
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This keeps the information boundary explicit.&lt;/p&gt;

&lt;p&gt;For example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="n"&gt;candles15m&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;sr&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;AggregateCandlesToTimeframe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;candles5m&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s"&gt;"5m"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s"&gt;"15m"&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;The strategy can then run the same S/R calculation on those completed higher-timeframe candles.&lt;/p&gt;

&lt;h2&gt;
  
  
  Backtest/live parity is the real goal
&lt;/h2&gt;

&lt;p&gt;Avoiding lookahead is not just a backtesting concern.&lt;/p&gt;

&lt;p&gt;A useful trading component should behave the same way when its inputs come from:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;a historical fixture;&lt;/li&gt;
&lt;li&gt;an exchange REST API;&lt;/li&gt;
&lt;li&gt;a WebSocket candle stream;&lt;/li&gt;
&lt;li&gt;a broker;&lt;/li&gt;
&lt;li&gt;a replay engine.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The market-data adapter may change, but the S/R algorithm should not need a special “backtest mode” that gets different information from the live version.&lt;/p&gt;

&lt;p&gt;That is a strong architectural test.&lt;/p&gt;

&lt;p&gt;If the historical implementation needs access to data that the live implementation cannot possibly have at the same moment, there is probably a timing problem.&lt;/p&gt;

&lt;h2&gt;
  
  
  Test the information boundary
&lt;/h2&gt;

&lt;p&gt;Tests for technical indicators should go beyond checking a few expected prices.&lt;/p&gt;

&lt;p&gt;Useful invariants include:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;identical candle prefixes produce identical output;&lt;/li&gt;
&lt;li&gt;only closed prefixes are passed to the calculation;&lt;/li&gt;
&lt;li&gt;pivots are not exposed before their confirmation point;&lt;/li&gt;
&lt;li&gt;adding future candles does not retroactively change decisions already recorded by the backtest;&lt;/li&gt;
&lt;li&gt;multi-timeframe aggregation does not leak partial future buckets.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Those tests are less visually impressive than a perfect chart.&lt;/p&gt;

&lt;p&gt;They are much more valuable.&lt;/p&gt;

&lt;h2&gt;
  
  
  A small Go implementation
&lt;/h2&gt;

&lt;p&gt;I extracted these ideas into &lt;code&gt;go-sr&lt;/code&gt;, an Apache-2.0 Go module with no external runtime dependencies:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;go get github.com/laclance/go-sr@v1.1.0
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Repository:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/laclance/go-sr" rel="noopener noreferrer"&gt;https://github.com/laclance/go-sr&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;It includes line and zone modes, nearest-level metadata, multi-timeframe helpers, and a standalone runnable example.&lt;/p&gt;

&lt;p&gt;I’m particularly interested in real integration feedback. If you already have a Go backtester or trading bot with its own OHLCV type, I’d like to know whether the API drops naturally into that pipeline.&lt;/p&gt;

&lt;p&gt;Because for support and resistance, the hardest question usually is not:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;“Did it identify the level?”&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;It is:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;“When was the algorithm actually allowed to know that level existed?”&lt;/strong&gt;&lt;/p&gt;

</description>
      <category>algorithms</category>
      <category>go</category>
      <category>programming</category>
      <category>softwareengineering</category>
    </item>
  </channel>
</rss>
