Most retail traders look at one indicator at a time. Professional quant desks look for confluence - multiple signals firing on the same ticker at the same moment. That's what separates a noisy RSI reading from a high-confidence trade setup.
This guide shows you how to build a Python stock scanner that finds those confluence moments using the SignalSage API - a quantitative signals API that scans for VWAP deviation, RSI exhaustion, Bollinger Band squeezes, and volume spikes happening simultaneously, then returns pre-calculated entry, target, and stop levels.
What We're Building
A Python scanner that:
- Hits the SignalSage API with a watchlist of tickers
- Filters signals by confidence score and risk level
- Prints a ranked alert table with entry/target/stop levels
- Optionally runs on a schedule to alert at market open
Prerequisites
- Python 3.8+
- Free RapidAPI account
- Basic Python knowledge (no finance background required)
Step 1: Subscribe to the API
- Go to RapidAPI and sign up for a free account
- Search for "SignalSage" by Circle of Wizards
- Subscribe to the free BASIC plan
- Copy your
X-RapidAPI-Keyfrom the API console
Step 2: Install Dependencies
pip install requests tabulate colorama
Step 3: Understanding the Signal Data
Before writing the scanner, let's look at what the API returns. A single signal looks like this:
{
"symbol": "NVDA",
"signalType": "Multi-Layer Mean Reversion",
"trigger": "1.6x volume, RSI 34, Near Bollinger",
"confidence": 85,
"position": "LONG",
"instrument": "CALL",
"entryLevel": 204.37,
"targetLevel": 208.46,
"stopLevel": 201.92,
"timeframe": "4d",
"riskLevel": "moderate",
"isActive": true
}
The trigger field tells you exactly which indicators fired. confidence is a 0-100 score - I filter for 70+ in production. isActive distinguishes live setups from historical context.
Step 4: The Scanner
import requests
from tabulate import tabulate
from colorama import Fore, Style, init
from datetime import datetime
init(autoreset=True)
RAPIDAPI_KEY = "YOUR_RAPIDAPI_KEY_HERE"
RAPIDAPI_HOST = "signalsage.p.rapidapi.com"
BASE_URL = f"https://{RAPIDAPI_HOST}"
HEADERS = {
"X-RapidAPI-Key": RAPIDAPI_KEY,
"X-RapidAPI-Host": RAPIDAPI_HOST,
}
# Your watchlist - up to 10 tickers per request
WATCHLIST = ["SPY", "QQQ", "AAPL", "NVDA", "TSLA", "META", "MSFT", "GOOGL", "AMZN"]
def get_signals(symbols: list[str]) -> dict:
"""Fetch signals for a list of symbols."""
response = requests.get(
f"{BASE_URL}/signals",
headers=HEADERS,
params={"symbols": ",".join(symbols)},
timeout=30,
)
response.raise_for_status()
return response.json()
def get_analysis(symbols: list[str]) -> dict:
"""Fetch hero analysis - plain-English trade thesis for each signal."""
response = requests.get(
f"{BASE_URL}/analysis",
headers=HEADERS,
params={"symbols": ",".join(symbols)},
timeout=30,
)
response.raise_for_status()
return response.json()
def risk_color(risk_level: str) -> str:
colors = {"low": Fore.GREEN, "moderate": Fore.YELLOW, "high": Fore.RED}
return colors.get(risk_level.lower(), Fore.WHITE)
def confidence_bar(score: int) -> str:
filled = round(score / 10)
return "?" * filled + "?" * (10 - filled)
def display_signals(data: dict, min_confidence: int = 65):
signals = [s for s in data.get("signals", []) if s.get("isActive")]
if not signals:
status = data.get("market_status", "UNKNOWN")
print(f"\n{Fore.YELLOW}No active signals. Market status: {status}{Style.RESET_ALL}")
print("SignalSage returns signals during market hours (9:30am-4pm ET).")
return
# Filter and sort by confidence
filtered = sorted(
[s for s in signals if s.get("confidence", 0) >= min_confidence],
key=lambda x: x.get("confidence", 0),
reverse=True,
)
if not filtered:
print(f"\n{Fore.YELLOW}No signals above {min_confidence}% confidence threshold.{Style.RESET_ALL}")
return
print(f"\n{Fore.CYAN}{'?' * 70}")
print(f" SIGNALSAGE - {len(filtered)} ACTIVE SIGNAL(S) | {datetime.now().strftime('%H:%M:%S ET')}")
print(f"{'?' * 70}{Style.RESET_ALL}\n")
rows = []
for s in filtered:
conf = s.get("confidence", 0)
risk = s.get("riskLevel", "?")
pos = s.get("position", "?")
instr = s.get("instrument", "?")
pos_color = Fore.GREEN if pos == "LONG" else Fore.RED
rc = risk_color(risk)
entry = s.get("entryLevel", 0)
target = s.get("targetLevel", 0)
stop = s.get("stopLevel", 0)
r_multiple = round((target - entry) / (entry - stop), 1) if entry > stop else "-"
rows.append([
f"{s.get('emoji', '')} {s.get('symbol', '?')}",
s.get("signalType", "?")[:28],
f"{conf}% {confidence_bar(conf)}",
f"{pos_color}{pos} {instr}{Style.RESET_ALL}",
f"${entry:.2f}",
f"{Fore.GREEN}${target:.2f}{Style.RESET_ALL}",
f"{Fore.RED}${stop:.2f}{Style.RESET_ALL}",
f"{rc}{r_multiple}R {risk}{Style.RESET_ALL}",
s.get("timeframe", "?"),
])
headers = ["Ticker", "Signal Type", "Confidence", "Play", "Entry", "Target", "Stop", "R:R / Risk", "TF"]
print(tabulate(rows, headers=headers, tablefmt="rounded_outline"))
print(f"\n{Fore.CYAN}Triggers:{Style.RESET_ALL}")
for s in filtered:
print(f" {s.get('emoji', '.')} {s['symbol']}: {s.get('trigger', '-')}")
def display_analysis(data: dict):
analysis = data.get("analysis")
if not analysis:
return
print(f"\n{Fore.CYAN}?? Market Analysis ??{Style.RESET_ALL}")
if isinstance(analysis, str):
words = analysis.split()
line, lines = [], []
for word in words:
if sum(len(w) + 1 for w in line) + len(word) > 80:
lines.append(" ".join(line))
line = [word]
else:
line.append(word)
if line:
lines.append(" ".join(line))
for l in lines:
print(f" {l}")
elif isinstance(analysis, list):
for item in analysis:
print(f" . {item}")
def main():
print(f"{Fore.MAGENTA}????????????????????????????????????")
print(f"? SIGNALSAGE SCANNER v1.0 ?")
print(f"????????????????????????????????????{Style.RESET_ALL}")
print(f"Watchlist: {', '.join(WATCHLIST)}")
print("Scanning...\n")
try:
signals_data = get_signals(WATCHLIST)
analysis_data = get_analysis(WATCHLIST[:5])
display_signals(signals_data, min_confidence=65)
display_analysis(analysis_data)
except requests.exceptions.HTTPError as e:
status = e.response.status_code
if status == 401:
print(f"{Fore.RED}Invalid API key - check your RapidAPI key.{Style.RESET_ALL}")
elif status == 403:
print(f"{Fore.RED}Access denied - subscribe at rapidapi.com/rapidSuper/api/signalsage{Style.RESET_ALL}")
elif status == 429:
print(f"{Fore.RED}Rate limit hit - upgrade to PRO for higher limits.{Style.RESET_ALL}")
else:
print(f"{Fore.RED}API error {status}: {e}{Style.RESET_ALL}")
except requests.exceptions.Timeout:
print(f"{Fore.RED}Request timed out - the API scans live market data, try again.{Style.RESET_ALL}")
if __name__ == "__main__":
main()
Sample Output
????????????????????????????????????
? SIGNALSAGE SCANNER v1.0 ?
????????????????????????????????????
Watchlist: SPY, QQQ, AAPL, NVDA, TSLA, META, MSFT, GOOGL, AMZN
Scanning...
??????????????????????????????????????????????????????????????????????
SIGNALSAGE - 3 ACTIVE SIGNAL(S) | 10:34:22 ET
??????????????????????????????????????????????????????????????????????
????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????
? Ticker ? Signal Type ? Confidence ? Play ? Entry ? Target ? Stop ? R:R / Risk ? TF ?
????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????
? ??? NVDA ? Multi-Layer Mean Reversion ? 85% ?????????? ? LONG CALL ? $204.37 ? $208.46 ? $201.92 ? 1.7R moderate ? 4d ?
? ?? SPY ? Momentum Breakout ? 78% ?????????? ? LONG CALL ? $734.47 ? $749.16 ? $725.66 ? 1.6R moderate ? 2d ?
? ?? AAPL ? VWAP Reversion ? 71% ?????????? ? LONG CALL ? $307.23 ? $313.38 ? $303.64 ? 1.7R low ? 3d ?
????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????
Triggers:
??? NVDA: 1.6x volume, RSI 34, Near Bollinger
?? SPY: 2.0x volume, Near Bollinger
?? AAPL: RSI 38, VWAP -0.8%
How the Signals Work
SignalSage checks four conditions simultaneously for each ticker:
| Indicator | What it's checking |
|---|---|
| VWAP deviation | Price meaningfully below intraday average (mean reversion setup) |
| RSI | Oversold reading below 40 (momentum exhaustion) |
| Bollinger Band | Price near or below lower band (statistical extreme) |
| Volume | Current bar volume ? 1.5x average (institutional interest) |
The confidence score reflects how many of these are firing together. A single RSI reading is noise. RSI + volume spike + Bollinger touch at VWAP is a setup worth watching.
Automating It
Run at market open every weekday with cron:
# crontab -e
30 9 * * 1-5 /usr/bin/python3 /path/to/scanner.py >> ~/signals.log 2>&1
Or add a Discord webhook for phone alerts:
def send_alert(signal: dict):
webhook_url = "YOUR_DISCORD_WEBHOOK"
msg = (
f"**{signal['emoji']} {signal['symbol']} - {signal['signalType']}**\n"
f"Confidence: {signal['confidence']}% | {signal['position']} {signal['instrument']}\n"
f"Entry: ${signal['entryLevel']:.2f} ? Target: ${signal['targetLevel']:.2f} | Stop: ${signal['stopLevel']:.2f}\n"
f"Trigger: {signal['trigger']}"
)
requests.post(webhook_url, json={"content": msg})
Call send_alert(s) for each signal in filtered and you'll get a Discord ping whenever a high-confidence setup fires at open.
A Note on Market Hours
The API scans live price data from Alpaca's market feed. Outside 9:30am-4pm ET it returns "market_status": "NO_SIGNALS" - that's expected, not an error. Schedule your scanner to run after open.
BASIC plan covers personal use and a daily open scan. PRO ($9.99/mo) unlocks higher rate limits for strategies running throughout the session.
The API is live on RapidAPI - search "SignalSage" by Circle of Wizards. Free to try. If you build something interesting on top of it, drop a comment below.
Top comments (0)