QuantJourney Backtester

QuantJourney Backtester

Share product feedback

✓

Thank you.

Your note is now in the QuantJourney inbox.

docs/engine/signals.mdx

Signals

How QuantJourney strategies express intent before sizing, rebalancing or order submission.

Signals are strategy intent. They say what the strategy wants to own, avoid, rank or short before capital is allocated.

Sourcebacktester/core.py
LayerStrategy intent / alpha state
ModeRequired in weight mode; optional helper state in order mode
Inputmarket data, indicators, factor features and strategy rules
Outputdates x instruments pd.DataFrame
Primary API_compute_signals(self) -> pd.DataFrame | None
Main caveatSignals are not trades and are not weights. They must be causal.

Engine Contract

A signal is an intermediate research object. It does not create a position, submit an order, charge cost or update NAV. In weight mode, signals are usually converted into target weights. In order mode, signal logic can live directly inside _compute_orders(...).

Timing rule

Even though the weight-performance path shifts target weights by one bar, signal computation must still be causal. Do not use future returns, next-day close, future fundamentals or revised data that was unavailable at the decision timestamp.

Data Contract

Contract itemRequirement
Shapedates x instruments DataFrame
Indexengine trading dates
Columnsstrategy universe instruments
Valuescommonly 1, 0, -1; continuous scores are allowed before sizing
NaN policyno NaNs after signal generation; fill warmup values explicitly
Storagestored under self.strategy_name in the strategy data store
Timingvalues must use only data available at or before the bar

Level 1: Binary Trend Signal

This is the minimal long/flat pattern. It is best for trend filters and allocation strategies.

SMA crossover signal
python
import pandas as pd


class SMASignalStrategy(Backtester):
    def _compute_signals(self) -> pd.DataFrame:
        fast = self.instruments_data.get_feature("SMA_50_close")
        slow = self.instruments_data.get_feature("SMA_200_close")

        valid = fast.notna() & slow.notna()
        signal = (fast > slow).astype(float)

        return signal.where(valid, 0.0).fillna(0.0)

Level 2: Directional Signal

Signals can be long, flat or short. The weight layer decides how much capital each state receives.

Long/short RSI regime
python
class RSIDirectionalSignal(Backtester):
    def _compute_signals(self) -> pd.DataFrame:
        rsi = self.instruments_data.get_feature("RSI_14_close")

        signal = pd.DataFrame(0.0, index=rsi.index, columns=rsi.columns)
        signal = signal.mask(rsi < 30, 1.0)    # long oversold names
        signal = signal.mask(rsi > 70, -1.0)   # short overbought names

        return signal.fillna(0.0)

Level 3: Continuous Alpha Score

Continuous signals are useful when the ranking strength matters. They should usually be transformed into weights by a separate sizing rule.

Cross-sectional momentum score
python
class MomentumScoreSignal(Backtester):
    def _compute_signals(self) -> pd.DataFrame:
        close = self.instruments_data.get_feature("adj_close")

        momentum_12m = close.pct_change(252)
        momentum_1m = close.pct_change(21)

        # Skip the most recent month to reduce short-term reversal effects.
        score = momentum_12m - momentum_1m

        # Convert each date into percentile ranks across instruments.
        ranked = score.rank(axis=1, pct=True)

        return ranked.fillna(0.0)

Level 4: Signal Helper In Order Mode

In order mode you do not need to implement _compute_signals(). You can calculate the same idea inside _compute_orders(...) and submit orders directly.

Order-mode signal as local logic
python
class OrderModeSignal(Backtester):
    def _compute_orders(self, date, bars, current_positions, nav):
        inst = "AAPL"
        pos = current_positions.get(inst, 0.0)

        fast = self.instruments_data.get_feature("SMA_20_close").loc[date, inst]
        slow = self.instruments_data.get_feature("SMA_50_close").loc[date, inst]

        buy_signal = fast > slow
        sell_signal = fast < slow

        if pos == 0 and buy_signal:
            ...
        elif pos > 0 and sell_signal:
            ...

Failure Modes

  • Returning prices instead of signals.
  • Returning a Series when the engine expects a dates x instruments DataFrame.
  • Forgetting to fill indicator warmup NaNs.
  • Using shift(-1), future returns or next-day close in signal logic.
  • Expecting signals to create trades without _compute_weights() or _compute_orders().
  • Treating signal strength as final portfolio weight without checking row sums, caps and cash.

Audit Checklist

  • Does the signal index match the price index?
  • Do signal columns match the engine universe?
  • Are all values zero after warmup?
  • Are there any NaNs?
  • Does the strategy run in execution_mode="weights" and implement _compute_weights()?
  • If running order mode, is the signal logic actually used inside _compute_orders(...)?