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