Notebooks/Multi-Indicator Score Strategy
Signals·TA Strategies·Intermediate

Multi-Indicator Score Strategy

Build a composite scoring system that ranks and combines signals from multiple indicators into a single entry/exit score.

compositescoringmulti-indicator

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

[1]
!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

[2]
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

[3]
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

[4]
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

[5]
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

[6]
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()
[6]