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.
backtester/core.pypd.DataFrame_compute_signals(self) -> pd.DataFrame | NoneEngine 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(...).
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 item | Requirement |
|---|---|
| Shape | dates x instruments DataFrame |
| Index | engine trading dates |
| Columns | strategy universe instruments |
| Values | commonly 1, 0, -1; continuous scores are allowed before sizing |
| NaN policy | no NaNs after signal generation; fill warmup values explicitly |
| Storage | stored under self.strategy_name in the strategy data store |
| Timing | values 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.
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.
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.
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.
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(...)?