Combining conditions: AND / OR / threshold logic
Chapter 114's weighted score is one way to combine indicators. Often you want simpler, more interpretable logic: strict AND (all conditions must agree), OR (any one is enough), or a minimum-count threshold (at least N of M agree). This chapter builds all three as reusable primitives.
import pandas as pd
def combine_and(*conditions: pd.Series) -> pd.Series:
"""All conditions must be the SAME non-zero sign for a signal; otherwise 0."""
stacked = pd.concat(conditions, axis=1)
all_positive = (stacked > 0).all(axis=1)
all_negative = (stacked < 0).all(axis=1)
signal = pd.Series(0, index=conditions[0].index)
signal[all_positive] = 1
signal[all_negative] = -1
return signal
def combine_or(*conditions: pd.Series) -> pd.Series:
"""Any non-zero condition passes through; if conditions disagree in direction, net to 0."""
stacked = pd.concat(conditions, axis=1)
net = stacked.sum(axis=1)
signal = pd.Series(0, index=conditions[0].index)
signal[net > 0] = 1
signal[net < 0] = -1
return signal
def combine_min_count(conditions: list[pd.Series], min_agree: int) -> pd.Series:
"""At least min_agree conditions must agree on direction."""
stacked = pd.concat(conditions, axis=1)
positive_count = (stacked > 0).sum(axis=1)
negative_count = (stacked < 0).sum(axis=1)
signal = pd.Series(0, index=conditions[0].index)
signal[positive_count >= min_agree] = 1
signal[negative_count >= min_agree] = -1
return signal
Worked example — three different combination strategies on the same indicators
ema_trend = (indicators_df["ema_9"] > indicators_df["ema_21"]).apply(lambda x: 1 if x else -1)
macd_cross = detect_crossover(indicators_df["macd"], indicators_df["macd_signal"]) # ch 113
rsi_bias = indicators_df["rsi_14"].apply(lambda x: 1 if x > 50 else -1)
adx_gate = indicators_df["adx"].apply(lambda x: 1 if x > 20 else 0)
strict_signal = combine_and(ema_trend, macd_cross, rsi_bias)
# fires only when EMA trend, MACD crossover, AND RSI bias ALL point the same way — rare, high-conviction
loose_signal = combine_or(ema_trend, macd_cross, rsi_bias)
# fires when the net direction across all three leans one way — frequent, lower-conviction
majority_signal = combine_min_count([ema_trend, macd_cross, rsi_bias], min_agree=2)
# fires when at least 2 of 3 agree — a middle ground
AND is stricter and rarer — trades fewer times, typically higher per-trade conviction
OR is looser and more frequent — more trades, typically noisier, lower per-trade conviction
Neither is "correct" in the abstract — the right choice depends on your strategy's target trade frequency and how correlated the underlying indicators already are (chapter 132 covers measuring that correlation directly — if two of your three conditions are 90% correlated, an AND of all three barely differs from an AND of just one, giving a false sense of confirmation).
Applying a gate condition (like ADX) — different from voting conditions
def apply_gate(signal: pd.Series, gate: pd.Series) -> pd.Series:
"""gate: 1 = allow signal through, 0 = suppress regardless of signal's own direction."""
return signal.where(gate == 1, 0)
final_signal = apply_gate(strict_signal, adx_gate)
A gate (chapter 106's "is there a trend at all") is conceptually different from a voting condition — it doesn't contribute a direction, it only permits or blocks whatever direction the other conditions already produced. Mixing gates into a voting/AND-OR scheme without this distinction (e.g. accidentally treating ADX's 0/1 gate as if it voted -1/1) silently corrupts the combination logic.
Testing every combination mode before committing to one
def compare_combination_modes(conditions: list[pd.Series], price_series: pd.Series) -> pd.DataFrame:
modes = {
"and": combine_and(*conditions),
"or": combine_or(*conditions),
"majority": combine_min_count(conditions, min_agree=len(conditions)//2 + 1),
}
results = []
for name, signal in modes.items():
result = backtest_signal(signal, price_series) # ch 81-82
results.append({"mode": name, **result})
return pd.DataFrame(results)
Run this comparison rather than assuming AND is "safer" or OR is "better" — the right combination mode is itself an empirical question, subject to the same walk-forward validation (chapter 84) as any other strategy choice.