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]andentry_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.M5runs the strategy on 5-minute bars regardless of the chart's viewing timeframe.uses_sltp=Falseskips the SL/TP bracket bundle. Set toTrueand emitsl/tparrays 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/maxso 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_belowonly 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.
- From the strategy top-bar pill, pick "EMA Cross". The chart re-runs immediately and renders trade brackets (entry, SL/TP if any, exit).
- Hover a trade marker for entry/exit timestamps and PnL.
- Open the Analyzer tab, choose Strategy mode, click Run; the per-tag panel will show separate stats for
bull_setupvsbear_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/maxon the params and pick a step size in the Analyzer's parameter form.