MACD Momentum
Momentum strategy with MACD. The entry occurs on histogram turns. When the histogram crosses zero upward, there is buying strength and the strategy opens long. When it crosses downward, there is selling strength and the strategy opens short.
Standard implementation of the MACD Histogram crossover. Handles EMA calculations and zero-line crossing detection.
# No import needed: `Indicator` is a pre-injected global (native indicator kernel).
# A fixed lookback window keeps every bar O(1) in history length — see the note below.
LOOKBACK = 300 # window >> slow+signal → O(1) per bar; converges to the full-history MACD
DECLARATION = {
"type": "strategy",
"inputs": [
{"name": "fast", "type": "int", "default": 12},
{"name": "slow", "type": "int", "default": 26},
{"name": "signal", "type": "int", "default": 9},
],
"plots": [
{"name": "macd", "title": "MACD", "source": "macd",
"type": "line", "color": "#22D3EE", "width": 2},
{"name": "signal_line", "title": "Signal", "source": "signal_line",
"type": "line", "color": "#F59E0B", "width": 2},
{"name": "hist", "title": "Histogram", "source": "hist",
"type": "histogram", "color": "#94A3B8"},
],
"pane": "new",
"scale": "right",
"levels": [
{"name": "Zero", "value": 0, "color": "#64748B", "width": 1, "style": "dotted"},
],
}
def main(df=None, sdk=None, params={}):
# Strict Mode requires an accepted entrypoint (main / on_bar / on_*_tick /
# a Strategy class). This 3-branch dispatcher wires the per-bar backtest,
# the chart (study) call, and the declaration probe. on_bar_strategy on its
# own is NOT an entrypoint — without this main() the runner rejects the script.
params = params or {}
if sdk is not None:
return on_bar_strategy(sdk, params)
if df is not None:
return _build_chart(df, params)
return DECLARATION
def _build_chart(df, params):
# df= branch runs once for the chart, so a whole-series call is fine here.
# Indicator.macd accepts the DataFrame directly and reads source='close'.
p = params or {}
fast, slow, sig = int(p.get("fast", 12)), int(p.get("slow", 26)), int(p.get("signal", 9))
macd_line, signal_line, hist = Indicator.macd(df, fast, slow, sig)
return {**DECLARATION, "series": {"macd": macd_line, "signal_line": signal_line, "hist": hist}}
def on_bar_strategy(sdk, params):
fast, slow, sig = int(params.get("fast", 12)), int(params.get("slow", 26)), int(params.get("signal", 9))
if len(sdk.candles) < max(fast, slow) + sig + 2: return
# Bounded window → O(1) in history length. Indicator.macd (native, no import)
# returns (macd_line, signal_line, hist); the last two hist values give the cross.
_, _, hist = Indicator.macd(sdk.candles[-LOOKBACK:], fast, slow, sig)
hist_prev, hist_curr = hist[-2], hist[-1]
if hist_prev is None or hist_curr is None: return # still in warmup
crossed_up = hist_prev <= 0 and hist_curr > 0
crossed_down = hist_prev >= 0 and hist_curr < 0
if sdk.position == 0:
if crossed_up: sdk.buy(action="buy_to_open", qty=1, order_type="market")
elif crossed_down: sdk.sell(action="sell_short_to_open", qty=1, order_type="market")
elif sdk.position > 0 and crossed_down:
sdk.sell(action="sell_to_close", qty=abs(sdk.position), order_type="market")
elif sdk.position < 0 and crossed_up:
sdk.buy(action="buy_to_cover", qty=abs(sdk.position), order_type="market")Keep every bar O(1). The
Indicator.macd(sdk.candles[-LOOKBACK:], …)call above reads a bounded slice, so each frame is constant-time in history length and needs no manual state — the last two histogram values come straight out of the window. Do not pass the whole history —Indicator.macd([c["close"] for c in sdk.candles], fast, slow, sig)is O(n) per bar and O(n²) over the backtest. As history grows, a late frame overruns the ~800 ms per-bar budget, which can desync the request/response protocol into a fatalProtocolErrorthat aborts the whole run — see Compute indicators incrementally.A window comfortably larger than
slow + signal(≈10×;LOOKBACK = 300here) converges to the full-history MACD. If you need values bit-identical to a full-history recompute, keep the three EMAs as exact accumulators insdk.stateand update each from the newest close — see Compute indicators incrementally.Indicatoris a pre-injected global; there is nothing to import.
When to use
- Trending markets with pullbacks. The histogram captures the moment when the correction ends.
- Intermediate timeframes (1h, 4h).
What to expect
- Earlier signals than SMA crossover, since the histogram reverses before the moving-average crossover.
- More sensitive to noise than SMA in sideways markets.