Signals·TA Strategy Implementations·Intermediate

Multi Indicator Score Strategy

Build a multi-indicator composite scoring system that aggregates RSI, MACD, ADX, Bollinger Bands, and volume indicators into a unified bullish and bearish confidence score for systematic trade entry and exit decisions.

technical-analysistrading-signalstrading-strategies

Multi-Indicator Scoring Strategy for Financial Signal Generation

1. Strategy Definition

A multi-indicator scoring system aggregates signals from independent technical indicators into a single composite score. Each indicator contributes a directional vote: +1 (bullish), -1 (bearish), or 0 (neutral).

Signal Generation Logic:

The total score determines the overall trading signal:

Total ScoreSignal DescriptionTrading Signal
≥ +2Strong bullish consensusBuy (+1)
≤ -2Strong bearish consensusSell (-1)
-1 to +1Mixed or no consensusNo signal (0)

Constituent Indicators:

The strategy employs the following four independent indicators:

  1. Relative Strength Index (RSI) Score: Generated as +1 if the asset is oversold (RSI < 40), -1 if overbought (RSI > 60), and 0 otherwise.
  2. Moving Average (MA) Crossover Score: Generated as +1 if the fast moving average is above the slow moving average, and -1 if the fast moving average is below the slow moving average.
  3. Volume Score: Generated as +1 if the current trading volume exceeds its rolling average, and 0 otherwise. This indicator is bounded to 0/+1; the absence of high volume is considered neutral, not bearish, serving as an additive confirmation.
  4. Rate of Change (ROC) Score: Generated as +1 if the 5-period Rate of Change is positive, and -1 if negative.

Rationale for Multi-Indicator Approach:

Individual technical indicators often exhibit high rates of false signals. By requiring consensus across multiple independent indicators, each measuring different aspects of market behavior (e.g., momentum, trend, volume, rate of change), the probability of generating a genuine and robust trading signal is significantly enhanced compared to reliance on any single indicator in isolation. A consensus threshold of two or more indicators (out of a maximum of four) serves as a majority-rule filter, ensuring that signals reflect multi-dimensional market conditions rather than isolated indicator triggers.

2. Setup and Dependencies

2.1. Install Required Libraries

[ ]
!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.2. Import Libraries

[ ]
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. Data Generation

3.1. Generate Synthetic OHLCV Data

[ ]
def generate_data(periods):
    """
    Generates synthetic Open-High-Low-Close-Volume (OHLCV) financial data.

    Args:
        periods (int): The number of time periods (e.g., minutes) for which to generate data.

    Returns:
        pd.DataFrame: A DataFrame containing synthetic OHLCV data with a datetime index.
    """
    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)

4. Multi-Indicator Scoring Strategy Implementation

[ ]
def multi_indicator_score_strategy(df: pd.DataFrame) -> pd.DataFrame:
    """
    Applies the multi-indicator scoring strategy to a DataFrame of OHLCV data.

    Args:
        df (pd.DataFrame): Input DataFrame containing 'open', 'high', 'low', 'close', 'volume', and 'datetime' columns.

    Returns:
        pd.DataFrame: The input DataFrame augmented with individual indicator scores, total score, and final signal.
    """
    df = df.copy().sort_values("datetime", ignore_index=True)

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

    # 2. Moving Average Crossover Score Calculation
    df["score_ma"]  = np.where(df["close"].rolling(5).mean() > df["close"].rolling(20).mean(), 1, -1)

    # 3. Volume Score Calculation
    df["score_vol"] = np.where(df["volume"] > df["volume"].rolling(20).mean(), 1, 0)

    # 4. Rate of Change (ROC) Score Calculation
    roc = df["close"].pct_change(5)
    df["score_roc"] = np.where(roc > 0, 1, -1)

    # Aggregate Scores
    score_cols = ["score_rsi","score_ma","score_vol","score_roc"]
    df["total_score"] = df[score_cols].sum(axis=1)

    # Generate Final Signal
    df["signal"]      = np.where(df["total_score"] >=  2,  1,
                        np.where(df["total_score"] <= -2, -1, 0))
    return df

5. Apply Strategy and Analyze Signal Distribution

[ ]
df_signals = multi_indicator_score_strategy(df)

print("Signal Distribution:")
print(df_signals["signal"].value_counts())

print("\nScore Distribution:")
print(df_signals["total_score"].value_counts().sort_index())
Signal Distribution:
signal
 0    360
 1     82
-1     58
Name: count, dtype: int64

Score Distribution:
total_score
-2     58
-1    134
 0    126
 1    100
 2     57
 3     25
Name: count, dtype: int64

6. Visualization of Strategy Signals

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

fig = make_subplots(rows=2, cols=1, shared_xaxes=True,
    subplot_titles=["Price and Trading Signals", "Composite Score"],
    row_heights=[0.65, 0.35])

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=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.Bar(x=df_signals["datetime"], y=df_signals["total_score"],
    name="Total Score",
    marker_color=["green" if v >= 2 else "red" if v <= -2 else "gray" for v in df_signals["total_score"]]),
    row=2, col=1)

fig.add_hline(y= 2, line_dash="dash", line_color="green", row=2, col=1, annotation_text="Buy threshold")
fig.add_hline(y=-2, line_dash="dash", line_color="red",   row=2, col=1, annotation_text="Sell threshold")

fig.update_layout(title_text="Multi-Indicator Scoring Strategy Performance",
    xaxis_rangeslider_visible=False, height=700, yaxis=dict(autorange=True))

fig.show()

7. Conclusion

This notebook demonstrates a multi-indicator scoring strategy for generating financial signals. By combining four independent technical indicators (RSI, Moving Average Crossover, Volume, and Rate of Change), the strategy aims to produce more robust trading signals by requiring a consensus among these indicators.

Key takeaways:

  • The multi_indicator_score_strategy function successfully calculates individual indicator scores, aggregates them into a total_score, and generates a final signal based on predefined thresholds.
  • The signal distribution and score distribution provide insight into how often buy, sell, and no-signal conditions are met, and how the individual scores contribute to the aggregate total.
  • Visualizations clearly illustrate the synthetic price action, the generated buy/sell signals, and the composite score over time, helping to understand the strategy's performance.

This approach helps to mitigate false signals that can arise from relying on a single indicator, offering a more comprehensive view of market conditions before making a trading decision.