Multi-Indicator Score Strategy
Build a composite scoring system that ranks and combines signals from multiple indicators into a single entry/exit score.
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 Score | Signal Description | Trading Signal |
|---|---|---|
| ≥ +2 | Strong bullish consensus | Buy (+1) |
| ≤ -2 | Strong bearish consensus | Sell (-1) |
| -1 to +1 | Mixed or no consensus | No signal (0) |
Constituent Indicators:
The strategy employs the following four independent indicators:
- Relative Strength Index (RSI) Score: Generated as +1 if the asset is oversold (RSI < 40), -1 if overbought (RSI > 60), and 0 otherwise.
- 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.
- 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.
- 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 plotlyRequirement 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_subplots3. 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 df5. 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()