02. Your first Python strategy

A guided walkthrough that ends with ema_cross.py: a long-and-short EMA crossover strategy, with tags so the analyzer can split breakout-style entries from pullback-style entries in the per-tag stats panel.

You will use @strategy, define entry_long / exit_long / entry_short / exit_short boolean arrays, and attach tags via define_tag plus return-dict keys. Steps 1-5 build the file. Step 6 is the full strategy you can paste; step 7 walks the run cycle.

Step 1. The mental model

A Python OHLC strategy returns four boolean arrays the same length as the bar series. The Rust backtest engine reads them and simulates trades.

entry_long[i] = True means: enter long at bar i if no long is currently open. exit_long[i] = True closes any open long at bar i. The same for short. Anything else you return in the dict (any non-canonical key) is treated as a tag boolean array; tags label trades in the analyzer for slicing PnL by setup or regime.

Notes

  • Conflict resolution: if entry_long[i] and entry_short[i] are both True on the same bar, the engine takes neither (logged as a conflict).
  • Microstructure: long entry fills at ask, long exit fills at bid (and short reversed). The engine handles this; you only emit signals.

Step 2. Imports and decorator

Pull in the strategy decorator, signal helpers, and the timeframe enum.

Example

import numpy as np
from quant_charts import (
    strategy, input, ta, cross_above, cross_below,
    define_tag, Timeframe,
)


@strategy(
    name="EMA Cross",
    description="Long+short EMA crossover with directional tags.",
    overlay=True,
    timeframe=Timeframe.M5,
    data_mode="ohlc",
    required_columns=["close"],
    uses_sltp=False,
)
class EMACross:
    pass

Notes

  • timeframe=Timeframe.M5 runs the strategy on 5-minute bars regardless of the chart's viewing timeframe.
  • uses_sltp=False skips the SL/TP bracket bundle. Set to True and emit sl/tp arrays only when you want stops/targets.
  • required_columns=["close"] only ships the close column to the worker; cuts sweep memory.

Step 3. Parameters

Two integer params for the periods, two color params for tag display in the analyzer.

Example

class EMACross:
    fast_period = input.int(9, "Fast EMA", min=2, max=200)
    slow_period = input.int(21, "Slow EMA", min=2, max=400)

Notes

  • These show up as both panel inputs and analyzer sweep ranges. Set sensible min/max so the analyzer's grid mode does not generate nonsense.

Step 4. Compute crossovers

cross_above(a, b) returns a boolean array, True on the bar where a crosses up through b (False elsewhere). cross_below is the reverse.

Example

    def calculate(self, df):
        close = np.asarray(df["close"], dtype=np.float64)
        fast = ta.ema(close, self.fast_period)
        slow = ta.ema(close, self.slow_period)

        bull_cross = cross_above(fast, slow)
        bear_cross = cross_below(fast, slow)

Notes

  • cross_above / cross_below only fire on the exact bar of the cross. They do NOT stay True after.
  • For "fast above slow" (a continuous condition, not a single-bar cross), use above(fast, slow).

Step 5. Tags and return dict

Declare two tags for analyzer styling, then return the full signal/tag dict.

Example

        define_tag("bull_setup", "Long entry on bull cross", color="#22c55e")
        define_tag("bear_setup", "Short entry on bear cross", color="#ef4444")

        return {
            "entry_long":  bull_cross,
            "exit_long":   bear_cross,
            "entry_short": bear_cross,
            "exit_short":  bull_cross,
            # any non-canonical key is auto-classified as a tag bool array
            "bull_setup":  bull_cross,
            "bear_setup":  bear_cross,
        }

Notes

  • define_tag(name, label, color) is metadata for the UI. The actual tag boolean comes from the return dict key.
  • Tag arrays must be the same length as the signal arrays.
  • In the analyzer, click "Bull Setup" or "Bear Setup" in the per-tag stats panel to see PnL filtered to just those entries.

Step 6. The full file

Save this as ema_cross.py under your workspace/strategies/.

Example

import numpy as np
from quant_charts import (
    strategy, input, ta, cross_above, cross_below,
    define_tag, Timeframe,
)


@strategy(
    name="EMA Cross",
    description="Long+short EMA crossover with directional tags.",
    overlay=True,
    timeframe=Timeframe.M5,
    data_mode="ohlc",
    required_columns=["close"],
    uses_sltp=False,
)
class EMACross:
    fast_period = input.int(9, "Fast EMA", min=2, max=200)
    slow_period = input.int(21, "Slow EMA", min=2, max=400)

    def calculate(self, df):
        close = np.asarray(df["close"], dtype=np.float64)
        fast = ta.ema(close, self.fast_period)
        slow = ta.ema(close, self.slow_period)

        bull_cross = cross_above(fast, slow)
        bear_cross = cross_below(fast, slow)

        define_tag("bull_setup", "Long entry on bull cross", color="#22c55e")
        define_tag("bear_setup", "Short entry on bear cross", color="#ef4444")

        return {
            "entry_long":  bull_cross,
            "exit_long":   bear_cross,
            "entry_short": bear_cross,
            "exit_short":  bull_cross,
            "bull_setup":  bull_cross,
            "bear_setup":  bear_cross,
        }

Step 7. Run it

Attach the strategy to a chart and inspect trades.

  1. From the strategy top-bar pill, pick "EMA Cross". The chart re-runs immediately and renders trade brackets (entry, SL/TP if any, exit).
  2. Hover a trade marker for entry/exit timestamps and PnL.
  3. Open the Analyzer tab, choose Strategy mode, click Run; the per-tag panel will show separate stats for bull_setup vs bear_setup.

Notes

  • If trades do not appear, the strategy panel "Status" line shows the most recent error or zero-signal warning.
  • For sweeps, set min/max on the params and pick a step size in the Analyzer's parameter form.