Signals·Signal Confluence Systems·Intermediate

TA Signal Confluence Engine

Build a signal confluence engine that aggregates multiple independent technical analysis signals into a unified directional confidence score, weighting each signal by its historical predictive accuracy and current market context appropriateness.

signal-generationtechnical-analysistrading-signals

Signals — TA Signal Confluence Engine


1. Dependency Installation

[ ]
!pip install pandas numpy plotly
Requirement already satisfied: pandas in /usr/local/lib/python3.12/dist-packages (2.2.2)
Requirement already satisfied: numpy in /usr/local/lib/python3.12/dist-packages (2.0.2)
Requirement already satisfied: plotly in /usr/local/lib/python3.12/dist-packages (5.24.1)
Requirement already satisfied: python-dateutil>=2.8.2 in /usr/local/lib/python3.12/dist-packages (from pandas) (2.9.0.post0)
Requirement already satisfied: pytz>=2020.1 in /usr/local/lib/python3.12/dist-packages (from pandas) (2025.2)
Requirement already satisfied: tzdata>=2022.7 in /usr/local/lib/python3.12/dist-packages (from pandas) (2026.1)
Requirement already satisfied: tenacity>=6.2.0 in /usr/local/lib/python3.12/dist-packages (from plotly) (9.1.4)
Requirement already satisfied: packaging in /usr/local/lib/python3.12/dist-packages (from plotly) (26.1)
Requirement already satisfied: six>=1.5 in /usr/local/lib/python3.12/dist-packages (from python-dateutil>=2.8.2->pandas) (1.17.0)

2. Library Imports

[ ]
import warnings; warnings.filterwarnings("ignore")
import pandas as pd
import numpy as np
import plotly.graph_objects as go
from plotly.subplots import make_subplots

3. Strategy Overview

The TA Signal Confluence Engine aggregates directional signals from multiple independent technical indicators into a single composite score. The premise is that when several unrelated indicators agree, the probability of a successful trade is higher than when acting on any single signal.

Indicators combined:

IndicatorBullish ConditionBearish Condition
EMA CrossFast EMA > Slow EMAFast EMA < Slow EMA
RSIRSI < 40 (oversold)RSI > 60 (overbought)
MACDMACD line > Signal lineMACD line < Signal line
Bollinger BandsClose < Lower BandClose > Upper Band
VolumeVolume > 1.5 × Avg Volume

Confluence score: Sum of individual signals (each ±1 or 0). Final signal:

  • Score ≥ thresholdBuy (+1)
  • Score ≤ −thresholdSell (−1)
  • Otherwise → No signal (0)

Limitation: All indicators in this engine are trend-following or momentum-based; in ranging markets they tend to produce contradictory signals that cancel each other out, naturally reducing false positives.

4. Data Generation

[ ]
def generate_data(periods: int) -> pd.DataFrame:
    """
    Generate synthetic OHLCV price data using a geometric random walk.

    Parameters
    ----------
    periods : int
        Number of 1-minute bars to generate.

    Returns
    -------
    pd.DataFrame
        DataFrame with columns: open, high, low, close, volume, datetime.
    """
    start_date     = pd.to_datetime("2024-01-01 00:00:00+00:00")
    datetime_index = pd.date_range(start_date, periods=periods, freq="1min", tz="UTC")
    price_data = []
    last_close = 42000
    for i in range(periods):
        open_price  = last_close + np.random.normal(0, last_close * 0.0005)
        close_price = open_price + np.random.normal(0, last_close * 0.005)
        body_high   = max(open_price, close_price)
        body_low    = min(open_price, close_price)
        high_price  = max(body_high + abs(np.random.normal(0, last_close * 0.002)), open_price, close_price)
        low_price   = min(body_low  - abs(np.random.normal(0, last_close * 0.002)), open_price, close_price)
        if high_price < low_price:
            high_price, low_price = low_price, high_price
        price_data.append({
            "open":  max(1, int(open_price)),
            "high":  max(1, int(high_price)),
            "low":   max(1, int(low_price)),
            "close": max(1, int(close_price)),
        })
        last_close = close_price
    df = pd.DataFrame(price_data, index=datetime_index)
    df.index.name = "datetime"
    df["volume"]   = np.random.uniform(100.0, 500.0, periods)
    df["datetime"] = df.index.to_series()
    return df.reset_index(drop=True)

df = generate_data(500)
display(df.head())
open high low close volume datetime
0 42004 42628 41911 42558 361.438946 2024-01-01 00:00:00+00:00
1 42550 42625 42409 42413 264.763772 2024-01-01 00:01:00+00:00
2 42430 42567 42099 42225 108.519221 2024-01-01 00:02:00+00:00
3 42255 42307 42129 42195 178.541421 2024-01-01 00:03:00+00:00
4 42205 42302 42185 42293 143.237693 2024-01-01 00:04:00+00:00

5. Strategy Function

[ ]
import pandas as pd
def ta_signal_confluence_engine(
    df: pd.DataFrame,
    ema_fast: int = 12,
    ema_slow: int = 26,
    rsi_period: int = 14,
    bb_period: int = 20,
    bb_std: float = 2.0,
    volume_factor: float = 1.5,
    threshold: int = 3,
) -> pd.DataFrame:
    """
    Compute a multi-indicator confluence score and derive a final trading signal.

    Core logic
    ----------
    1. Compute five independent indicator signals (EMA cross, RSI, MACD,
       Bollinger Bands, volume).
    2. Sum all signals into a raw confluence score (-5 to +5).
    3. Apply a threshold filter: emit +1 or -1 only when the absolute score
       meets or exceeds the threshold; otherwise emit 0.

    Parameters
    ----------
    df : pd.DataFrame   OHLCV DataFrame.
    ema_fast : int      Fast EMA period.
    ema_slow : int      Slow EMA period.
    rsi_period : int    RSI calculation period.
    bb_period : int     Bollinger Band rolling window.
    bb_std : float      Bollinger Band standard deviation multiplier.
    volume_factor : float  Volume ratio threshold for volume signal.
    threshold : int     Minimum absolute confluence score to emit a signal.

    Returns
    -------
    pd.DataFrame
        Original DataFrame extended with indicator signals, confluence_score, signal.
    """
    df = df.copy().sort_values("datetime", ignore_index=True)

    # ── EMA Cross signal ─────────────────────────────────────────────────────
    df["ema_fast"] = df["close"].ewm(span=ema_fast, adjust=False).mean()
    df["ema_slow"] = df["close"].ewm(span=ema_slow, adjust=False).mean()
    df["sig_ema"]  = np.where(df["ema_fast"] > df["ema_slow"], 1, -1)

    # ── RSI signal ───────────────────────────────────────────────────────────
    delta = df["close"].diff()
    gain  = delta.clip(lower=0).rolling(rsi_period).mean()
    loss  = (-delta.clip(upper=0)).rolling(rsi_period).mean()
    df["rsi"]     = 100 - 100 / (1 + gain / loss.replace(0, np.nan))
    df["sig_rsi"] = np.where(df["rsi"] < 40, 1, np.where(df["rsi"] > 60, -1, 0))

    # ── MACD signal ──────────────────────────────────────────────────────────
    macd_line      = df["close"].ewm(span=12, adjust=False).mean() - \
                     df["close"].ewm(span=26, adjust=False).mean()
    signal_line    = macd_line.ewm(span=9, adjust=False).mean()
    df["sig_macd"] = np.where(macd_line > signal_line, 1, -1)

    # ── Bollinger Band signal ─────────────────────────────────────────────────
    sma       = df["close"].rolling(bb_period).mean()
    std       = df["close"].rolling(bb_period).std()
    df["bb_upper"] = sma + bb_std * std
    df["bb_lower"] = sma - bb_std * std
    df["sig_bb"] = np.where(df["close"] < df["bb_lower"], 1,
                   np.where(df["close"] > df["bb_upper"], -1, 0))

    # ── Volume signal ────────────────────────────────────────────────────────
    df["avg_vol"]  = df["volume"].rolling(20).mean()
    # Volume alone is non-directional; only amplify the EMA direction
    df["sig_vol"]  = np.where(df["volume"] > volume_factor * df["avg_vol"],
                              df["sig_ema"], 0)

    # ── Confluence score and final signal ────────────────────────────────────
    df["confluence_score"] = (df["sig_ema"] + df["sig_rsi"] +
                               df["sig_macd"] + df["sig_bb"] + df["sig_vol"])

    df["signal"] = np.where(df["confluence_score"] >=  threshold,  1,
                   np.where(df["confluence_score"] <= -threshold, -1, 0))

    return df

df_signals = ta_signal_confluence_engine(df, threshold=3)

print("--- Signal Distribution ---")
print(df_signals["signal"].value_counts())
print("\n--- Confluence Score Distribution ---")
print(df_signals["confluence_score"].value_counts().sort_index())
--- Signal Distribution ---
signal
 0    494
 1      4
-1      2
Name: count, dtype: int64

--- Confluence Score Distribution ---
confluence_score
-3      2
-2     51
-1    116
 0    136
 1    137
 2     54
 3      4
Name: count, dtype: int64

Explanation:

  • Each indicator contributes ±1 or 0 to the score independently; the maximum possible score is ±5.
  • threshold=3 requires at least 3 of 5 indicators to agree before a signal is emitted, balancing signal frequency against quality.
  • sig_vol amplifies the EMA signal direction rather than providing a directional view of its own, as volume is non-directional.

6. Visualization

[ ]
buy_signals  = df_signals[df_signals["signal"] ==  1]
sell_signals = df_signals[df_signals["signal"] == -1]

fig = make_subplots(rows=3, cols=1, shared_xaxes=True,
    subplot_titles=["Price + Signals", "RSI", "Confluence Score"],
    row_heights=[0.5, 0.25, 0.25])

fig.add_trace(go.Candlestick(
    x=df_signals["datetime"],
    open=df_signals["open"], high=df_signals["high"],
    low=df_signals["low"],   close=df_signals["close"],
    name="Price"), row=1, col=1)

fig.add_trace(go.Scatter(x=df_signals["datetime"], y=df_signals["ema_fast"],
    mode="lines", name="EMA Fast", line=dict(color="blue", width=1)),   row=1, col=1)
fig.add_trace(go.Scatter(x=df_signals["datetime"], y=df_signals["ema_slow"],
    mode="lines", name="EMA Slow", line=dict(color="orange", width=1)), row=1, col=1)

fig.add_trace(go.Scatter(x=buy_signals["datetime"],  y=buy_signals["low"]   * 0.999,
    mode="markers", marker=dict(symbol="triangle-up",   size=10, color="green"), name="Buy (+1)"),  row=1, col=1)
fig.add_trace(go.Scatter(x=sell_signals["datetime"], y=sell_signals["high"] * 1.001,
    mode="markers", marker=dict(symbol="triangle-down", size=10, color="red"),   name="Sell (-1)"), row=1, col=1)

fig.add_trace(go.Scatter(x=df_signals["datetime"], y=df_signals["rsi"],
    mode="lines", name="RSI", line=dict(color="purple", width=1)), row=2, col=1)
fig.add_hline(y=40, line_dash="dot", line_color="green", row=2, col=1)
fig.add_hline(y=60, line_dash="dot", line_color="red",   row=2, col=1)

fig.add_trace(go.Bar(x=df_signals["datetime"], y=df_signals["confluence_score"],
    name="Confluence", marker_color=["green" if s > 0 else "red" if s < 0 else "gray"
    for s in df_signals["confluence_score"]]), row=3, col=1)
fig.add_hline(y=0, line_dash="dot", line_color="gray", row=3, col=1)

fig.update_layout(title_text="TA Signal Confluence Engine",
                  xaxis_rangeslider_visible=False, height=800,
                  xaxis3_title="Datetime")
fig.show()

Conclusion

This notebook demonstrated a multi-indicator TA Signal Confluence Engine. Key takeaways include:

  • Aggregation of Signals: The engine effectively combines directional signals from multiple technical indicators to generate a composite score.
  • Threshold-based Filtering: A configurable threshold ensures that signals are only emitted when there's a strong agreement among indicators, aiming to reduce false positives.
  • Visualization: The visualizations clearly illustrate price action, indicator values (like RSI), and the confluence score, along with generated buy/sell signals.

Further enhancements could include incorporating more diverse indicators, optimizing parameters using backtesting, or integrating with a trading execution platform.