Most algo trading tools focus on one type of market. Either you're scanning stocks with RSI and MACD, or you're looking at prediction markets. I wanted both — and I built TradeSight to do exactly that.
Here's what I learned combining two very different data sources into a single opportunity scanner.
Why prediction markets + stocks?
Prediction markets like Polymarket price crowd wisdom on future events — elections, economic indicators, crypto outcomes. The interesting thing: they're largely uncorrelated with stock market moves. A high-confidence Polymarket opportunity (say, 85% yes on a Fed rate decision) doesn't necessarily move with the S&P 500.
That orthogonality is valuable. If you have signals firing in both markets simultaneously, they're more likely to be independent — not just correlated noise from the same underlying macro move.
The second reason: Polymarket mispricings are often exploitable. When a market is at 70/30 but the underlying evidence points to 85/15, that's an edge. Prediction markets are still relatively thin compared to equities, so opportunities exist.
The Polymarket side: scanning for edges
Polymarket has a public API at gamma-api.polymarket.com. No auth required for reads:
import requests
def get_active_markets(min_volume=10000, min_liquidity=5000):
"""Fetch Polymarket markets with meaningful activity."""
url = "https://gamma-api.polymarket.com/markets"
params = {
"active": True,
"closed": False,
"limit": 100,
"order": "volume24hr",
"ascending": False
}
resp = requests.get(url, params=params, timeout=10)
markets = resp.json()
filtered = []
for market in markets:
volume = float(market.get("volume24hr", 0) or 0)
liquidity = float(market.get("liquidity", 0) or 0)
if volume >= min_volume and liquidity >= min_liquidity:
filtered.append({
"question": market["question"],
"yes_price": float(market.get("lastTradePrice", 0.5)),
"volume_24h": volume,
"liquidity": liquidity,
"end_date": market.get("endDate"),
})
return filtered
From there, I apply simple opportunity scoring: markets where yes price is between 0.15 and 0.85 (not already decided), volume is meaningful, and liquidity allows entry/exit without slipping.
The key insight: a market at 50/50 with $500K volume is interesting. A market at 98/2 with $50K volume is not — you can't meaningfully profit from the final 2%.
The stock side: multi-indicator scanning
For stocks, I use the Alpaca API to pull OHLCV bars and run a standard technical stack:
from alpaca.data.historical import StockHistoricalDataClient
from alpaca.data.requests import StockBarsRequest
from alpaca.data.timeframe import TimeFrame
import pandas as pd
def calculate_signals(df):
"""Calculate RSI, MACD, and Bollinger Bands."""
# RSI
delta = df['close'].diff()
gain = delta.where(delta > 0, 0).rolling(14).mean()
loss = (-delta.where(delta < 0, 0)).rolling(14).mean()
rs = gain / loss.replace(0, 1e-10)
df['rsi'] = 100 - (100 / (1 + rs))
# MACD
ema12 = df['close'].ewm(span=12).mean()
ema26 = df['close'].ewm(span=26).mean()
df['macd'] = ema12 - ema26
df['macd_signal'] = df['macd'].ewm(span=9).mean()
df['macd_hist'] = df['macd'] - df['macd_signal']
# Bollinger Bands
sma20 = df['close'].rolling(20).mean()
std20 = df['close'].rolling(20).std()
df['bb_upper'] = sma20 + (2 * std20)
df['bb_lower'] = sma20 - (2 * std20)
df['bb_pct'] = (df['close'] - df['bb_lower']) / (df['bb_upper'] - df['bb_lower'])
return df
def score_stock_opportunity(df):
"""Score stock setup quality on 0-100."""
latest = df.iloc[-1]
score = 50 # neutral baseline
# RSI signals
if latest['rsi'] < 30:
score += 20 # oversold
elif latest['rsi'] > 70:
score -= 20 # overbought
# MACD momentum
if latest['macd_hist'] > 0 and df['macd_hist'].iloc[-2] <= 0:
score += 15 # fresh crossover up
elif latest['macd_hist'] < 0 and df['macd_hist'].iloc[-2] >= 0:
score -= 15 # fresh crossover down
# Bollinger position
if latest['bb_pct'] < 0.1:
score += 10 # near lower band (potential bounce)
elif latest['bb_pct'] > 0.9:
score -= 10 # near upper band (extended)
return min(100, max(0, score))
I run this across the full S&P 500 universe every morning before open. Tickers with score > 70 get flagged as potential entries.
Combining both: the unified opportunity feed
The tricky part is normalizing confidence across two completely different market types. Polymarket gives you probabilities. Stocks give you technical scores. How do you put them on the same dashboard?
My approach: convert everything to a confidence-weighted expected value.
For Polymarket:
- An opportunity score = how far the market is from "certain" × volume depth factor
- A market at 65/35 with $200K volume scores higher than 65/35 with $5K volume
For stocks:
- Use the 0-100 technical score directly
- Weight by relative volume (1.5x average volume = higher conviction)
Then show them side by side in a single feed, sorted by score. The trader (or the tournament system) evaluates which opportunities to act on.
def get_unified_opportunities(poly_markets, stock_signals):
"""Normalize and combine opportunities from both sources."""
opportunities = []
for market in poly_markets:
yes = market['yes_price']
# Distance from certainty, weighted by volume
uncertainty_score = (1 - abs(yes - 0.5) * 2) # 1.0 at 50%, 0.0 at 0% or 100%
vol_factor = min(1.5, market['volume_24h'] / 100000)
score = uncertainty_score * vol_factor * 100
opportunities.append({
"type": "polymarket",
"label": market['question'][:60],
"score": round(score),
"yes_price": yes,
"context": f"${market['volume_24h']:,.0f} 24h volume"
})
for ticker, data in stock_signals.items():
opportunities.append({
"type": "stock",
"label": ticker,
"score": data['score'],
"price": data['price'],
"context": f"RSI {data['rsi']:.1f} | MACD {'▲' if data['macd_hist'] > 0 else '▼'}"
})
return sorted(opportunities, key=lambda x: x['score'], reverse=True)
What the combined feed actually looks like
On a typical day, TradeSight surfaces 8-15 stock opportunities and 3-5 Polymarket markets worth watching. The interesting moments are when they're correlated: a Polymarket market on Fed rate cuts at 72% YES correlating with oversold bank stocks is a more compelling thesis than either signal alone.
I haven't built automated cross-market position sizing yet — that's on the roadmap. Right now it's a unified dashboard that lets me see both sources together and run overnight tournaments to test strategies against them.
What I'd do differently
The Polymarket API changes. The gamma-api endpoint occasionally returns inconsistent field names. I've had to add fallbacks for lastTradePrice vs outcomePrices[0] vs clobTokenIds. Build in resilience.
Volume ≠ liquidity. High 24h volume on Polymarket doesn't always mean tight spreads. Check the spread field if it's available, or calculate it from the order book. Getting slipped on entry because you looked at volume instead of liquidity is a beginner mistake I made.
Recency bias in technical signals. If you run your scanner during extended hours, you'll get signals from low-volume periods that look stronger than they are. I now only run the stock scanner 30 minutes after open.
Where to see this in action
TradeSight is open source on GitHub — the scanner module and opportunity scoring logic are all there. It runs fully local with Alpaca paper trading (free tier), so you can explore it without real money.
If prediction markets + algorithmic trading sounds interesting, the combo is worth exploring. The markets are different enough to be complementary, and the technical implementation is straightforward once you have a clean abstraction layer over both data sources.
TradeSight is a paper trading project — not financial advice. Always use paper trading before live capital.
Top comments (0)