Appearance

Personalize your experience

Mode
Accent Color
Direction
Coding for Trading — Fundamentals

Coding for Trading — Fundamentals

Coding

The concepts every strategy rests on, whether you write it in the condition builder or in code: series, bar indexing, the difference between a value and a state change, and why floating-point arithmetic bites.

Everything is a series

A trading program does not work on numbers, it works on series — one per bar, indexed by time. Close is not a price, it is a price per bar; and SMA(20) is not an average, it is an average per bar, each computed from the twenty bars ending there.

This is the single mental shift that makes code make sense. Every you write is evaluated once per bar, in order, with only the past available. A condition is not a question asked once; it is a question asked on every bar, and the answer changes.

a series, conceptually
// what "Close" means to the evaluatortype Series = number[];        // one value per bar, oldest firstconst close: Series = [100.2, 101.5, 100.8, 102.4, 103.1];// what "SMA(3)" means: a new series, same length, computed per barfunction sma(source: Series, period: number): Series {  return source.map((_, i) => {    if (i < period - 1) return NaN;          // warm-up: not enough history    const window = source.slice(i - period + 1, i + 1);    return window.reduce((a, b) => a + b, 0) / period;  });}sma(close, 3);  // [NaN, NaN, 100.83, 101.57, 102.10]
ℹ
Note: the NaN values. An indicator with N genuinely has no before bar N — there is not enough history to compute one. That warm-up region is real, it appears in every backtest, and comparisons against it are false rather than erroneous.

Bar indexing

Reaching backwards is how a compares now against then. The bracket notation is zero-based from the current bar: Close[0] is this bar, Close[1] the previous one.

ExpressionTypeDescription
ClosenumberThe current bar’s close. Equivalent to Close[0].
Close[1]numberThe previous bar’s close. The of every momentum and change calculation.
Close[N]numberN bars ago. Undefined when fewer than N bars have elapsed.
RSI(14)[1]numberAn indicator’s on the previous bar. Indicators are series too, so they index the same way.
!
Caution:Indexing past the start of the gives you an , not an error. A condition that references Close[20] is simply false for the first twenty bars, and if your test range is short that may be most of it. Always begin a test well before the you actually care about.

Values versus state changes

The most consequential distinction in logic, and the one most often got wrong. A condition describes a state that persists. A state change describes a transition that happens once.

the difference, in code
// a value condition: true on every bar the state holdsconst isAbove = (i: number) => rsi[i] > 30;// bars 99,100,101,102 → true, true, true, true// a state change: true only on the bar the transition occursconst crossedAbove = (i: number) =>  i > 0 && rsi[i - 1] <= 30 && rsi[i] > 30;// bars 99,100,101,102 → true, false, false, false

Used as an entry, the first fires on every bar the condition holds — which means an order on every bar until something stops it. The second fires once. Almost every “why did my take 4,000 ” question is a condition where a state change was meant.

★
Pro tip:A rule that always holds: one trigger, several filters. If you find yourself with two state changes joined by AND, one of them wants rewriting as a condition.

Floating point

Prices and indicator outputs are floating-point numbers, and floating-point arithmetic is not exact. This is not a curiosity — it changes which conditions are safe to write.

why == is dangerous
0.1 + 0.2 === 0.3;            // false0.1 + 0.2;                    // 0.30000000000000004// so this comparison can fail even when the values "are" equalsma20 === sma50;              // unreliable// compare within a tolerance insteadconst near = (a: number, b: number, eps = 1e-9) => Math.abs(a - b) < eps;// or, better, express the intent as an inequalitysma20 > sma50;                // no equality involved, no problem
!
Caution:Avoid == on any produced by arithmetic. It is safe on values — a bar index, a boolean state, an integer count — and unreliable on prices, averages and oscillator output. Most conditions that reach for equality actually want an inequality or a .

Causality

A may only use information that existed at the moment it acts. Violating this is look-ahead bias, and it is the one bug that makes a look better rather than worse.

  • A bar's close is only known once the bar has ended. Acting on the close of the bar you are evaluating is legitimate; acting on it during the bar is not.
  • A bar's high and low are only final at the close. A condition using High mid-bar is using a number that has not settled.
  • Anything computed over a centred uses bars by construction. A centred is not usable in a , however good it looks on a chart.
  • Fundamental and corporate carries a publication lag. Using it on its effective date rather than its announcement date is look-ahead.
!
Caution:The tell is quality, not error. Look-ahead does not throw, it just produces a that seems to know what happens next — because it does. If a backtest looks too good, assume a leak and go looking for it before you assume an edge.

A minimal strategy, in full

Every idea in this article, in one worked example: a moving-average with a stop, doing nothing that requires the and nothing that relies on exact equality.

crossover with a stop
interface Bar { open: number; high: number; low: number; close: number }function run(bars: Bar[]) {  const close = bars.map(b => b.close);  const fast  = sma(close, 20);  const slow  = sma(close, 50);  let position: { entry: number; stop: number } | null = null;  const trades: { entry: number; exit: number; reason: string }[] = [];  // start at 1: a crossover needs a previous bar to compare against  for (let i = 1; i < bars.length; i++) {    // warm-up guard — NaN comparisons are false, but being explicit is clearer    if (Number.isNaN(slow[i]) || Number.isNaN(slow[i - 1])) continue;    if (position) {      // stops are checked before signals: protection first      if (bars[i].low <= position.stop) {        trades.push({ entry: position.entry, exit: position.stop, reason: 'stop' });        position = null;        continue;      }      const crossedDown = fast[i - 1] >= slow[i - 1] && fast[i] < slow[i];      if (crossedDown) {        trades.push({ entry: position.entry, exit: close[i], reason: 'signal' });        position = null;      }      continue;    }    const crossedUp = fast[i - 1] <= slow[i - 1] && fast[i] > slow[i];    if (crossedUp) {      position = { entry: close[i], stop: close[i] * 0.98 };    }  }  return trades;}
  1. 1
    The loop starts at 1
    A state change needs a previous bar. Starting at 0 would against undefined and silently never fire.
  2. 2
    Warm-up is guarded explicitly
    NaN comparisons are already false, so the guard is not strictly required — but it documents the warm-up region instead of leaving it implicit.
  3. 3
    The stop is checked before the signal
    Protection takes . If both would trigger on one bar, the stop wins, which is the conservative choice and the one the engine also makes.
  4. 4
    Crossings use >= and < on either side
    No equality comparison on a computed average, and the boundary case — exactly equal on the previous bar — is handled deliberately rather than by accident.
  5. 5
    Only past and present are read
    Every index is i or i − 1. Nothing reaches forward, so the result is reproducible in real time.

Concepts to carry forward

Series

One per bar. Every is evaluated once per bar, in order, with only the past visible.

Warm-up

An indicator with N has no before bar N. That region is real and shows up in every backtest.

Trigger vs filter

A state change fires once; a condition holds. One trigger and several filters is the of a sound entry.

Tolerance

Never computed floats for equality. Use inequalities, or an explicit epsilon.

Causality

Use only what existed at the moment of action. Look-ahead is the bug that flatters.

Protection first

When a stop and a signal collide on one bar, the stop wins. Order your checks to reflect that.

See also

Related articles

  • Building Custom Indicators

    Coding / Custom Indicators

    Composing new indicators from existing ones, the recursive shapes that need care, warm-up handling, and how to check a custom indicator is actually correct.

  • Script API Reference

    Coding / IDE / Reference

    This reference covers all APIs, interfaces, and utilities available to indicator, overlay, and strategy scripts in Quatick IDE.

  • Script Best Practices

    Coding / IDE / Best Practices

    Follow these guidelines to write performant, reliable, and maintainable trading scripts.

  • Strategy DSL — Execution Model

    Coding / Strategy DSL

    What the engine does on every bar: protective exits, exit signals, entries, layers and re-entries, sizing and costs, and where every statistic in the result…

  • Condition Evaluator

    Automation / Strategy Builder / Reference

    How a condition becomes true on a bar: series computed up front, no value during warm-up, the exact cross and rising rules, and the order of work on each…

  • Built-in Indicators & Overlays

    Coding / IDE / Reference

    Quatick ships with a full library of pre-built indicators and overlays. You can use these out of the box, or study their implementations as reference for…

  • Condition Recipes

    Coding / Recipes

    Patterns worth knowing by heart: regime detection, confirmation, divergence, volatility gating, session control, and the idioms that express each one cleanly.

  • Debugging Strategies

    Coding / Debugging

    A method for diagnosing strategies that do nothing, do too much, or do the wrong thing — plus the specific symptoms that have specific causes.

  • Evaluation Performance

    Coding / Performance

    Why a backtest is slow and what actually fixes it: condition ordering, indicator reuse, incremental computation, and the difference between an O(n) and an…

Browse all knowledge base articles →