Weighted confluence scoring across indicators

"Confluence" means multiple independent indicators agreeing — the core idea behind combining chapters 101-111's indicators into one decision rather than trading any single one in isolation. This chapter builds a scoring system rather than a binary AND/OR gate (chapter 115 covers that simpler version).

import pandas as pd

def build_confluence_score(indicators_df: pd.DataFrame, rules: list[dict]) -> pd.Series:
    """
    rules: list of {"name": str, "condition_fn": callable(df) -> pd.Series[-1,0,1], "weight": float}
    Returns a continuous score per bar: sum of (condition_output * weight).
    """
    score = pd.Series(0.0, index=indicators_df.index)
    for rule in rules:
        condition_output = rule["condition_fn"](indicators_df)
        score += condition_output * rule["weight"]
    return score

Defining a rule set from chapters 101-111's indicators

rules = [
    {"name": "ema_trend", "weight": 1.0,
     "condition_fn": lambda df: (df["ema_9"] > df["ema_21"]).apply(lambda x: 1 if x else -1)},

    {"name": "macd_cross", "weight": 1.5,
     "condition_fn": lambda df: detect_crossover(df["macd"], df["macd_signal"])},

    {"name": "rsi_not_extreme", "weight": 0.5,
     "condition_fn": lambda df: df["rsi_14"].apply(lambda x: 1 if 40 < x < 70 else (-1 if 30 < x < 60 else 0))},

    {"name": "adx_trending", "weight": 1.0,
     "condition_fn": lambda df: df["adx"].apply(lambda x: 1 if x > 20 else 0)},   # ch 106 — gates strength, not direction

    {"name": "above_vwap", "weight": 0.75,
     "condition_fn": lambda df: (df["close"] > df["vwap"]).apply(lambda x: 1 if x else -1)},
]

score = build_confluence_score(indicators_df, rules)

Converting a continuous score into a discrete trade decision

def score_to_signal(score: pd.Series, entry_threshold: float = 3.0) -> pd.Series:
    signal = pd.Series(0, index=score.index)
    signal[score >= entry_threshold] = 1
    signal[score <= -entry_threshold] = -1
    return signal

Why weights should come from validation, not gut feeling

def grid_search_weights(indicators_df, rules, price_series, weight_options: list[float]):
    """Simplified sketch — real version needs proper walk-forward split, ch 84."""
    best = None
    for weight_combo in itertools.product(weight_options, repeat=len(rules)):
        for rule, w in zip(rules, weight_combo):
            rule["weight"] = w
        score = build_confluence_score(indicators_df, rules)
        signal = score_to_signal(score)
        result = backtest_signal(signal, price_series)   # your event-driven/vectorized backtest, ch 81-82
        if best is None or result["sharpe"] > best["sharpe"]:
            best = {"weights": weight_combo, "sharpe": result["sharpe"]}
    return best

Picking weights by intuition ("MACD feels more important than RSI so I'll give it 1.5x") is exactly the kind of untested assumption chapter 85 warns about — validate weight choices the same way you'd validate any other strategy parameter, including checking for the overfitting red flags from that chapter (too many free weight parameters relative to trade count is a real risk here, since a 5-rule weighted system has 5 free parameters).

The tradeoff of adding more indicators to a confluence score

Each additional rule is another parameter, another chance to overfit (chapter 85), and often redundant information (chapter 105 already noted RSI and Stochastic can be highly correlated) — more indicators isn't automatically more robust. A smaller set of indicators measuring genuinely different things (trend direction, trend strength, volume confirmation) usually beats a large pile of similar oscillators voting together.

Next: 115 — Combining conditions: AND / OR / threshold logic