05. Optimization, tags, and sweeps
Take a working strategy from a single-day backtest to a 700-combo sweep without melting your CPU. You will use @day_start, required_columns, and tags to control the analyzer's parallelism story end-to-end.
Sweeps run the strategy once per parameter combination per day. A naive strategy that does heavy work inside calculate() pays for that work combos x days times. The fix is hoisting: anything that does not depend on a parameter goes in @day_start, runs once per day per worker, and is reused across every combo. This walkthrough takes the EMA crossover from tutorial 02 and rewrites it for sweep performance.
Step 1. The five rules of hoisting
Read this before writing optimization code. These rules are also documented inline in the bundled optimize.py template.
Convert columns to numpy ONCE per day. Bad:
np.asarray(df["close"])insidecalculate(). Good: do it inside@day_startand readself._day["close"].Run literal-period TA ONCE per day. Bad:
ta.atr(high, low, close, 14)insidecalculate(). Good: same call inside@day_start; reuseself._day["atr14"].Run swept-period TA inside
calculate()ONLY for the param being swept. Keep all other periods literal and hoisted.Cache day-level scalars (vol_mean, regime label, max_drawdown) in
@day_start. They are recomputed once per combo otherwise.Use
required_columns=[...]so SHM only ships what you read. On a 28-day sweep this saves 30-70% of inter-process bandwidth.
Step 2. The @day_start hook
Decorate a method with @day_start. The runner calls it once per day per worker BEFORE the param loop and stashes the return value on self._day.
Example
from quant_charts import day_start
class EMACrossOptimized:
@day_start
def prep(self, df):
close = np.asarray(df["close"], dtype=np.float64)
high = np.asarray(df["high"], dtype=np.float64)
low = np.asarray(df["low"], dtype=np.float64)
atr14 = ta.atr(high, low, close, 14)
median_atr = float(np.median(atr14[np.isfinite(atr14)]))
return {
"close": close,
"atr14": atr14,
"median_atr": median_atr,
}
Notes
- The method name does not matter (here
prep). Only the@day_startdecorator does. - Whatever you return becomes
self._dayincalculate(). Use a dict so multiple precomputed arrays/scalars travel together. - Keep heavy literal-period TA in here (
ta.atr(..., 14),ta.sma(..., 200)). Move only swept-period TA intocalculate().
Step 3. The full optimized strategy
EMA crossover with hoisted ATR-regime filter. Save as ema_cross_opt.py.
Example
import numpy as np
from quant_charts import (
strategy, day_start, input, ta, cross_above, cross_below,
define_tag, Timeframe,
)
@strategy(
name="EMA Cross Optimized",
description="EMA crossover with ATR-regime filter, hoisted for sweeps.",
overlay=True,
timeframe=Timeframe.M1,
data_mode="ohlc",
required_columns=["high", "low", "close"],
uses_sltp=False,
)
class EMACrossOptimized:
fast = input.int(9, "Fast EMA", min=2, max=200) # SWEPT
slow = input.int(21, "Slow EMA", min=2, max=400) # SWEPT
regime_mult = input.float(1.5, "ATR Regime Mult",
min=0.0, max=10.0, step=0.1) # SWEPT
@day_start
def prep(self, df):
close = np.asarray(df["close"], dtype=np.float64)
high = np.asarray(df["high"], dtype=np.float64)
low = np.asarray(df["low"], dtype=np.float64)
atr14 = ta.atr(high, low, close, 14)
finite = atr14[np.isfinite(atr14)]
median_atr = float(np.median(finite)) if finite.size else float("nan")
return {"close": close, "atr14": atr14, "median_atr": median_atr}
def calculate(self, df):
close = self._day["close"]
atr14 = self._day["atr14"]
median_atr = self._day["median_atr"]
# Only the swept-period TA runs here. Two EMA calls.
fast = ta.ema(close, self.fast)
slow = ta.ema(close, self.slow)
# Regime filter: trade only when current ATR is materially above the
# day's median (avoids sleepy chop). Vectorized.
in_regime = atr14 > median_atr * self.regime_mult
bull_cross = cross_above(fast, slow)
bear_cross = cross_below(fast, slow)
define_tag("in_regime", "ATR above day median * mult", color="#22c55e")
define_tag("out_regime", "ATR below regime threshold", color="#a1a1aa")
return {
"entry_long": bull_cross & in_regime,
"exit_long": bear_cross,
"entry_short": bear_cross & in_regime,
"exit_short": bull_cross,
"in_regime": in_regime,
"out_regime": ~in_regime,
}
Step 4. Set up the sweep in the analyzer
The analyzer runs the strategy across a parameter grid in parallel.
- Open the Analyzer tab from the chart top bar.
- Mode: Strategy. Strategy: pick "EMA Cross Optimized".
- Date range: pick the days you want to test (multi-select).
- Parameter form: each
input.*field shows up. For each one you want to sweep, switch from "single value" to "range" and set start/stop/step. The analyzer computes the cartesian product unless you switch to fixed-N mode. - Click Run. Watch the progress bar; the result table populates as days complete.
Notes
required_columns=["high", "low", "close"]saves shippingvolume,delta, etc. across the worker boundary on every combo.- The analyzer reuses the
@day_startoutput across every combo for that day, so a 700-combo grid pays the ATR cost ONCE per day.
Step 5. Read the results
Three views matter: the leaderboard, the per-tag stats, and the 2D scatter.
Leaderboard: ranked rows by total PnL (or Sharpe, sortino, profit factor — toggle in the column picker). Click any row to load that exact (param, day) combo onto the chart.
Per-tag stats panel: shows PnL, win rate, trade count for each tag separately. With this strategy you can see how trades tagged in_regime performed vs trades tagged out_regime (which by definition have zero entries here, but the panel layout is the same).
2D scatter: pick two parameters from the dropdowns; each dot is one combo. Hover for stats. The scatter exposes parameter-space topography that the leaderboard hides.
3D scatter: same idea with three axes. Useful when sweep dimensionality > 2.
Step 6. Preventive tag filtering
The analyzer can re-run a sweep with a tag filter applied at entry time, not just post-hoc. Same Rust engine, no Python re-execution.
Once you have a sweep with tags, click any tag chip in the per-tag panel. The analyzer rebuilds a trading_mask from that tag's pre-computed boolean arrays and re-runs through Rust. Result: a new column on the leaderboard showing what each combo would have done if it had ONLY traded inside that tag's True regions.
This differs from a post-hoc filter (which just hides trades on display) because the position constraint matters: with only one position open at a time, removing some entries cascades into different exits. Preventive filtering simulates that cascade correctly.
Notes
- See "Preventive Tag Filtering" in the Features tab for the full architecture details.
- Tag-filtered sweeps reuse the original signal bundles, so they cost milliseconds per combo, not seconds.