Crypto Derivatives Edge Scanner
Build a Python scanner that detects trading signals from live crypto derivatives data.
Introduction
30 Second Summary
Professional crypto traders don't just watch price charts. They watch the data behind the price: how much leverage is in the market, which direction traders are positioned, and when those positions become fragile enough to snap.
In this project, you will build a command-line derivatives edge scanner that pulls live open interest, funding rates, and cross-exchange data from free APIs, calculates statistical signals, and alerts you when multiple indicators converge simultaneously.
What You'll Build
A Python script that aggregates derivatives data from Binance's free futures API and Xoomar's cross-exchange feed, calculates quantitative signals, and detects when multiple indicators align.
By the end of this project, you'll have:
- A working Python script (edge_scanner.py) that fetches live derivatives data from 3 exchanges with no API keys required.
- Professional-grade signal calculations including funding rate z-score, cross-exchange divergence, and OI concentration.
- A confluence detector that identifies when multiple signals fire at the same time.
- Secret Mission: Extend the scanner to monitor multiple assets and integrate on-chain signals from Glassnode.
Are there any prerequisites?
You should have Python, Cursor, and Claude Code already installed on your machine. A basic understanding of crypto futures and perpetual contracts will help you interpret the signals, but you'll learn the quantitative concepts as you go.
Before We Start
Derivatives data reveals how traders are positioned before price moves. Most retail traders never look at this data programmatically.
In this step, you'll set up a Python workspace with the tools needed to pull live derivatives data and run quantitative analysis on it.
In this step, get ready to:
- Create a project folder with a Python virtual environment.
- Install the requests and pandas packages for data fetching and analysis.
- Configure Glassnode MCP in Claude Code for on-chain research.
Create your project folder and virtual environment
A virtual environment keeps your project's packages isolated from the rest of your system. This prevents version conflicts between different Python projects.
- Press Cmd+Space (macOS) or the Windows key (Windows) to open your search bar.
- Type Terminal and press Enter to open it.
- Move to your Desktop by running this command:
cd ~/Desktop
- Create your project folder and move into it by running these commands:
mkdir edge-scanner
cd edge-scanner
- Create and activate your virtual environment by running these commands:
python3 -m venv venv
source venv/bin/activate
- Verify your environment is active by running:
python --version
You should see a Python 3.x version printed. Your terminal prompt should also show (venv) at the beginning, confirming the virtual environment is active.
Not seeing (venv) in your prompt?
Make sure you ran source venv/bin/activate after creating the environment. If you closed and reopened your terminal, you need to navigate back to edge-scanner and run the activate command again.
help me activate my Python virtual environment
Install the required packages
You need two packages for this project. requests handles HTTP calls to fetch live market data. pandas provides data analysis tools for calculating statistical signals.
- Install both packages by running:
pip install requests pandas
- Verify the installation worked by running:
python -c "import requests, pandas; print('Ready')"
You should see Ready printed in your terminal.
Seeing an import error?
Check that (venv) still appears in your terminal prompt. If it disappeared, reactivate with source venv/bin/activate and run the install command again.
help me fix my pip install for requests and pandas
Set up Glassnode MCP in Claude Code
Glassnode provides on-chain blockchain data like exchange flows, SOPR, and MVRV ratios. By connecting its MCP server to Claude Code, you can query on-chain metrics directly from your AI coding environment.
What is MCP?
MCP (Model Context Protocol) is an open protocol that connects AI models to external data sources. It lets Claude Code access live data from services like Glassnode without writing API integration code yourself.
The public access tier gives you 30 days of on-chain metric history at no cost.
- Open a new terminal window (keep your virtual environment terminal open).
- Add the Glassnode MCP server to Claude Code by running:
claude mcp add --transport http glassnode https://mcp.glassnode.com
- Open Claude Code by running:
claude
- Test the connection by typing this prompt in Claude Code:
What assets does Glassnode support?
You should see Claude respond with a list of supported assets (BTC, ETH, and others). This confirms the Glassnode MCP server is connected and working.
Not getting a response about assets?
Make sure you ran the claude mcp add command before opening Claude Code. If Claude Code was already open, close it and reopen it so it picks up the new MCP configuration.
help me connect Glassnode MCP to Claude Code
Your workspace is ready. Next up, you'll write your first functions to pull live open interest and funding rate data from Binance.
Fetch Derivatives Data from Binance
Your environment is ready. Now it is time to pull real data from the market.
Derivatives exchanges like Binance publish open interest and funding rate data for free. These two metrics reveal how traders are positioned before price moves. In this step, you will create a Python script that fetches both from Binance's USDT-margined futures API.
In this step, get ready to:
- Write a function that fetches current BTC open interest from Binance.
- Write a function that fetches funding rate history.
- Test both functions by running the script and inspecting the output.
Write the open interest function
Open interest is the total number of outstanding futures contracts that have not been settled. A rising OI alongside rising price means new money is entering the market with conviction.
- In Cursor, create a new file called edge_scanner.py inside your edge-scanner folder.
- Add the following imports, configuration constants, and the open interest function:
import requests
import pandas as pd
from datetime import datetime
# --- Configuration ---
BINANCE_FUTURES_BASE = "https://fapi.binance.com"
XOOMAR_BASE = "https://xoomar.com"
SYMBOL = "BTCUSDT"
# --- Binance Data Fetching ---
def get_current_oi(symbol):
"""Fetch current open interest from Binance USDT-M futures."""
url = f"{BINANCE_FUTURES_BASE}/fapi/v1/openInterest"
response = requests.get(url, params={"symbol": symbol})
response.raise_for_status()
return response.json()
What does this code do?
The configuration section sets up base URLs so you can swap exchanges later without hunting through function bodies. SYMBOL is set to BTCUSDT because BTC perpetuals have the deepest liquidity and most reliable signal.
The get_current_oi function hits Binance's /fapi/v1/openInterest endpoint. No API key is needed. The response returns openInterest as a string (e.g. "10659.509"), symbol, and a time timestamp.
- Save edge_scanner.py.
- You will not see output yet. The next chunk adds the second function, and you will test both together afterward.
Add the funding rate history function
Funding rates are periodic payments between longs and shorts on perpetual futures. When funding is positive, longs pay shorts. When it is negative, shorts pay longs. Extreme funding signals crowded positioning.
- Below the get_current_oi function, add the following funding rate function:
def get_funding_history(symbol, limit=100):
"""Fetch funding rate history from Binance."""
url = f"{BINANCE_FUTURES_BASE}/fapi/v1/fundingRate"
response = requests.get(url, params={"symbol": symbol, "limit": limit})
response.raise_for_status()
return response.json()
What does this code do?
This function calls /fapi/v1/fundingRate with a limit of 100. Binance settles funding every 8 hours, so 100 periods gives you roughly 33 days of history.
Each entry in the response contains fundingRate (a decimal like 0.0001 meaning 0.01% per 8h), fundingTime, symbol, and markPrice. You will use this history in the next step to calculate a z-score.
- Save edge_scanner.py.
- You will not see output yet. The next chunk adds a test block to run both functions.
Test both functions by running the script
- At the bottom of edge_scanner.py, add this test block to call both functions and print results:
if __name__ == "__main__":
oi_data = get_current_oi(SYMBOL)
print("Open Interest:", oi_data)
funding_data = get_funding_history(SYMBOL, limit=100)
print("Funding History (first 3):", funding_data[:3])
What does this code do?
The if __name__ == "__main__": guard ensures this code only runs when you execute the file directly. It calls both functions and prints the raw results so you can verify the API responses look correct.
Printing funding_data[:3] shows only the first 3 entries. This keeps your terminal readable while confirming data is flowing.
- Save edge_scanner.py.
- Run the script by executing this command in your terminal:
python edge_scanner.py
You should see a dictionary with an openInterest value (a string of numbers), followed by a list of 3 funding rate entries each containing fundingRate, fundingTime, and markPrice.
Seeing a connection error?
- Make sure you are connected to the internet. These are live API calls to Binance's servers.
- Check that BINANCE_FUTURES_BASE is set to https://fapi.binance.com with no trailing slash.
- If you see ModuleNotFoundError, confirm your virtual environment is activated (you should see (venv) in your terminal prompt).
Still stuck? Help me debug my Binance API connection in edge_scanner.py
✔️ Awesome, I've got everything!
Great. Double check you have saved your file before moving on.
ⓧ I'd like to double check the full code
Here is the complete edge_scanner.py so far:
import requests
import pandas as pd
from datetime import datetime
# --- Configuration ---
BINANCE_FUTURES_BASE = "https://fapi.binance.com"
XOOMAR_BASE = "https://xoomar.com"
SYMBOL = "BTCUSDT"
# --- Binance Data Fetching ---
def get_current_oi(symbol):
"""Fetch current open interest from Binance USDT-M futures."""
url = f"{BINANCE_FUTURES_BASE}/fapi/v1/openInterest"
response = requests.get(url, params={"symbol": symbol})
response.raise_for_status()
return response.json()
def get_funding_history(symbol, limit=100):
"""Fetch funding rate history from Binance."""
url = f"{BINANCE_FUTURES_BASE}/fapi/v1/fundingRate"
response = requests.get(url, params={"symbol": symbol, "limit": limit})
response.raise_for_status()
return response.json()
if __name__ == "__main__":
oi_data = get_current_oi(SYMBOL)
print("Open Interest:", oi_data)
funding_data = get_funding_history(SYMBOL, limit=100)
print("Funding History (first 3):", funding_data[:3])
Why no API key?
Binance's market data endpoints are public. Open interest and funding rates are published data that any trader can access without authentication. Rate limits are IP-based (weight system), not auth-based. You can make these calls freely without signing up or managing credentials.
Your script is pulling live derivatives data from Binance with zero cost and zero authentication. Next up, you will add cross-exchange data from Xoomar to see how positioning differs across venues.
Add Cross-Exchange Data from Xoomar
Your scanner can pull data from Binance, but that only gives you one exchange's perspective. Traders on Bybit or OKX might be positioned completely differently.
In this step, you will add a second data source that gives you the same asset's funding rate across three major exchanges in a single API call. This cross-exchange view is what reveals divergent positioning and potential arbitrage opportunities.
In this step, get ready to:
- Write a function to fetch cross-exchange funding rates from Xoomar.
- Parse and filter the response for BTC entries across all exchanges.
Write a function to fetch cross-exchange funding rates
Xoomar Pulse is a free market data aggregator that pulls funding rates from Binance, Bybit, and OKX into one feed. No API key, no signup, no credit card required.
- In edge_scanner.py, find the if __name__ == "__main__": block at the bottom of your file.
- Add the following function directly above that block, leaving a blank line between the new function and the if __name__ line:
# --- Xoomar Data Fetching ---
def get_cross_exchange_funding():
"""Fetch current funding rates across Binance, Bybit, OKX from Xoomar."""
url = f"{XOOMAR_BASE}/api/markets/funding-rates"
response = requests.get(url)
response.raise_for_status()
return response.json()
What does this code do?
This function hits Xoomar's /api/markets/funding-rates endpoint, which returns current funding rates for 30+ symbols across all three exchanges in one response.
The XOOMAR_BASE constant is already defined at the top of your file. No parameters are needed because the endpoint returns all symbols at once.
- Save edge_scanner.py.
You won't see a change yet when running the script because the main block hasn't been updated to call this function.
Parse and filter the response for BTC entries
The Xoomar response contains one row per symbol per exchange. You need to filter for BTC entries and handle null values, since some fields may not be populated for every exchange.
- In edge_scanner.py, find the if __name__ == "__main__": block at the bottom of your file.
- Replace the entire if __name__ == "__main__": block with the following code:
if __name__ == "__main__":
oi_data = get_current_oi(SYMBOL)
print("Open Interest:", oi_data)
funding_data = get_funding_history(SYMBOL, limit=100)
print("Funding History (first 3):", funding_data[:3])
cross_data = get_cross_exchange_funding()
btc_entries = [e for e in cross_data.get("data", []) if e.get("baseAsset", "").upper() == "BTC"]
print("BTC cross-exchange entries:")
for e in btc_entries:
rate = float(e["fundingRate"]) if e.get("fundingRate") else None
print(f" {e['exchange']}: {rate}")
What does this code do?
- cross_data.get("data", []) safely accesses the response envelope. Xoomar wraps all responses in { data, updatedAt, source } format.
- The list comprehension filters for entries where baseAsset equals "BTC". The .upper() call ensures case-insensitive matching.
- e.get("fundingRate") checks for null values before converting to float. Some exchanges may not report a rate at every interval.
- The result prints each exchange's BTC funding rate side by side so you can spot divergences.
- Save edge_scanner.py.
- Run the script by executing this command in your terminal:
python edge_scanner.py
You should see BTC funding rates from Binance, Bybit, and OKX printed below the Binance data you already had.
Not seeing cross-exchange data?
- If you see a ConnectionError, check your internet connection. Xoomar's API is publicly available with no authentication.
- If you see an empty list for BTC entries, the filter may not be matching. Check that your list comprehension uses .upper() == "BTC" exactly.
- If you get a 429 status code, you have hit the fair-use rate limit. Wait a minute and try again.
help me debug my Xoomar API call in edge_scanner.py
✔️ Awesome, I've got everything!
Great. Double check you have saved edge_scanner.py before moving on.
ⓧ I'd like to double check the full code
import requests
import pandas as pd
from datetime import datetime
# --- Configuration ---
BINANCE_FUTURES_BASE = "https://fapi.binance.com"
XOOMAR_BASE = "https://xoomar.com"
SYMBOL = "BTCUSDT"
# --- Binance Data Fetching ---
def get_current_oi(symbol):
"""Fetch current open interest from Binance USDT-M futures."""
url = f"{BINANCE_FUTURES_BASE}/fapi/v1/openInterest"
response = requests.get(url, params={"symbol": symbol})
response.raise_for_status()
return response.json()
def get_funding_history(symbol, limit=100):
"""Fetch funding rate history from Binance."""
url = f"{BINANCE_FUTURES_BASE}/fapi/v1/fundingRate"
response = requests.get(url, params={"symbol": symbol, "limit": limit})
response.raise_for_status()
return response.json()
# --- Xoomar Data Fetching ---
def get_cross_exchange_funding():
"""Fetch current funding rates across Binance, Bybit, OKX from Xoomar."""
url = f"{XOOMAR_BASE}/api/markets/funding-rates"
response = requests.get(url)
response.raise_for_status()
return response.json()
if __name__ == "__main__":
oi_data = get_current_oi(SYMBOL)
print("Open Interest:", oi_data)
funding_data = get_funding_history(SYMBOL, limit=100)
print("Funding History (first 3):", funding_data[:3])
cross_data = get_cross_exchange_funding()
btc_entries = [e for e in cross_data.get("data", []) if e.get("baseAsset", "").upper() == "BTC"]
print("BTC cross-exchange entries:")
for e in btc_entries:
rate = float(e["fundingRate"]) if e.get("fundingRate") else None
print(f" {e['exchange']}: {rate}")
Why use Xoomar instead of calling each exchange directly?
Binance, Bybit, and OKX each have their own API with different response formats and rate limits. Xoomar normalizes all three into a single schema, updated every 60 seconds.
One call gives you the same asset across all venues. This is the foundation for the cross-exchange spread calculation you will build in the next step.
Your scanner now pulls data from both Binance directly and a cross-exchange aggregator. Next up, you will turn this raw data into quantitative signals that flag when market conditions are extreme.
Calculate Edge-Finding Metrics
You now have live derivatives data flowing in from Binance and Xoomar. Raw data alone does not give you edge. Professional quant traders turn raw data into statistical signals that reveal when market positioning is stretched to an extreme.
In this step, you will build three signal calculations that quantify how crowded, divergent, and concentrated the derivatives market is right now. These are the same types of metrics used at crypto quant funds to identify mean-reversion and arbitrage setups.
In this step, get ready to:
- Calculate a funding rate z-score to detect crowded positioning.
- Calculate the cross-exchange funding spread to find divergence across venues.
- Calculate OI concentration using the Herfindahl Index to measure fragility risk.
Calculate the funding rate z-score
A z-score measures how many standard deviations the current value is from its historical average. When funding rate exceeds 2 standard deviations, it means one side of the trade is massively crowded and historically tends to snap back.
- In edge_scanner.py, find the line that says # --- Xoomar Data Fetching --- and scroll below the get_cross_exchange_funding() function.
- Add the following section comment and function below get_cross_exchange_funding(), above the existing if __name__ block:
# --- Signal Calculations ---
def calculate_funding_zscore(funding_data):
"""
Calculate how extreme the current funding rate is
compared to recent history using a z-score.
Z-score > 2: Funding is extremely high (crowded longs)
Z-score < -2: Funding is extremely low (crowded shorts)
"""
df = pd.DataFrame(funding_data)
df["fundingRate"] = df["fundingRate"].astype(float)
current_rate = df["fundingRate"].iloc[-1]
mean_rate = df["fundingRate"].mean()
std_rate = df["fundingRate"].std()
if std_rate == 0:
return 0.0, current_rate, mean_rate
zscore = (current_rate - mean_rate) / std_rate
return zscore, current_rate, mean_rate
What does this code do?
This function takes the list of 100 funding rate entries you fetched from Binance and converts them into a pandas DataFrame for statistical analysis.
- It calculates the mean and standard deviation of all 100 historical funding rates.
- It compares the most recent rate to that history using the z-score formula: (current - mean) / std.
- A z-score above 2 means longs are paying extreme premium (squeeze risk for longs).
- A z-score below -2 means shorts are paying extreme premium (squeeze risk for shorts).
You won't see a change yet. The next function builds on the same pattern.
Calculate the cross-exchange funding spread
When the same asset has different funding rates on different exchanges, it reveals that traders on those venues are positioned differently. A large spread is the basis of funding rate arbitrage strategies.
- Add this function below calculate_funding_zscore, still above the if __name__ block:
def calculate_cross_exchange_spread(cross_data, target_asset="BTC"):
"""
Find the funding rate spread between exchanges for an asset.
Large spreads indicate divergent positioning across venues.
"""
asset_rates = []
for entry in cross_data.get("data", []):
base = entry.get("baseAsset", "").upper()
if base == target_asset and entry.get("fundingRate"):
asset_rates.append({
"exchange": entry["exchange"],
"funding_rate": float(entry["fundingRate"]),
})
if len(asset_rates) < 2:
return 0.0, asset_rates
rates = [r["funding_rate"] for r in asset_rates]
spread = max(rates) - min(rates)
return spread, asset_rates
What does this code do?
This function filters the Xoomar cross-exchange data for BTC entries and extracts the funding rate from each exchange (Binance, Bybit, OKX).
- It calculates the spread as the difference between the highest and lowest funding rate across venues.
- A spread above 0.05% signals that exchanges have divergent positioning.
- This divergence is exploitable: go long on the negative-funding venue and short on the positive one.
You won't see a change yet. One more signal calculation to go.
Calculate OI concentration with the Herfindahl Index
The Herfindahl Index measures how concentrated market share is across participants. In crypto derivatives, if one exchange holds the majority of open interest, cascading liquidations on that exchange can trigger market-wide moves.
- Add this function below calculate_cross_exchange_spread, still above the if __name__ block:
def calculate_oi_concentration(cross_data, target_asset="BTC"):
"""
Calculate how concentrated open interest is across exchanges
using the Herfindahl Index.
High concentration (>0.5) means one exchange dominates,
creating cascading liquidation risk.
"""
oi_by_exchange = []
for entry in cross_data.get("data", []):
base = entry.get("baseAsset", "").upper()
oi_value = entry.get("openInterestValue")
if base == target_asset and oi_value:
oi_by_exchange.append({
"exchange": entry["exchange"],
"oi_value": float(oi_value),
})
if not oi_by_exchange:
return 0.0, []
total_oi = sum(e["oi_value"] for e in oi_by_exchange)
for e in oi_by_exchange:
e["share_pct"] = (e["oi_value"] / total_oi) * 100 if total_oi > 0 else 0
herfindahl = sum((e["share_pct"] / 100) ** 2 for e in oi_by_exchange)
return herfindahl, oi_by_exchange
What does this code do?
This function pulls openInterestValue from the Xoomar data for each exchange and calculates how concentrated that OI is.
- It calculates each exchange's percentage share of total BTC open interest.
- It squares each share and sums them to produce the Herfindahl Index.
- With 3 equal exchanges the index would be 0.33. A value above 0.5 means one exchange dominates.
Now that all three signal functions are in place, you need to update the test code at the bottom of the file to exercise them.
- Find the existing if __name__ == "__main__": block at the bottom of your file.
- Replace the entire block with this updated version that calls all three signal functions:
if __name__ == "__main__":
funding_data = get_funding_history(SYMBOL, limit=100)
cross_data = get_cross_exchange_funding()
zscore, current_rate, mean_rate = calculate_funding_zscore(funding_data)
print(f"Z-Score: {zscore:.2f}, Current Rate: {current_rate*100:.4f}%, Mean: {mean_rate*100:.4f}%")
spread, exchange_rates = calculate_cross_exchange_spread(cross_data, "BTC")
print(f"Cross-exchange spread: {spread*100:.4f}%")
herfindahl, oi_breakdown = calculate_oi_concentration(cross_data, "BTC")
print(f"Herfindahl Index: {herfindahl:.3f}")
What does this test block do?
This fetches live data from both APIs, then runs each of your three signal calculations and prints the results. You will see the z-score with its interpretive context, the cross-exchange spread as a percentage, and the Herfindahl Index as a decimal.
- Save edge_scanner.py.
- Run the scanner by executing this command in your terminal:
python edge_scanner.py
- You should see three lines of output: a z-score value (likely between -1 and 1 in calm markets), a cross-exchange spread percentage, and a Herfindahl Index (likely around 0.33-0.50 for BTC).
Seeing a KeyError or TypeError?
- If you see KeyError: 'fundingRate', make sure the get_funding_history function is returning data correctly. Run the old test code from the previous step to confirm the API is responding.
- If you see TypeError: unsupported operand type, check that you are converting string values to float with astype(float) and float() before doing math.
- If the Herfindahl Index shows 0.0, the Xoomar response may have null in the openInterestValue field. Confirm your filter checks for if base == target_asset and oi_value to skip null entries.
Help me debug my edge scanner output.
✔️ Awesome, I've got everything!
Great. Make sure you have saved edge_scanner.py with all three signal functions plus the updated test block.
ⓧ I'd like to double check the full code
import requests
import pandas as pd
from datetime import datetime
# --- Configuration ---
BINANCE_FUTURES_BASE = "https://fapi.binance.com"
XOOMAR_BASE = "https://xoomar.com"
SYMBOL = "BTCUSDT"
# --- Binance Data Fetching ---
def get_current_oi(symbol):
"""Fetch current open interest from Binance USDT-M futures."""
url = f"{BINANCE_FUTURES_BASE}/fapi/v1/openInterest"
response = requests.get(url, params={"symbol": symbol})
response.raise_for_status()
return response.json()
def get_funding_history(symbol, limit=100):
"""Fetch funding rate history from Binance."""
url = f"{BINANCE_FUTURES_BASE}/fapi/v1/fundingRate"
response = requests.get(url, params={"symbol": symbol, "limit": limit})
response.raise_for_status()
return response.json()
# --- Xoomar Data Fetching ---
def get_cross_exchange_funding():
"""Fetch current funding rates across Binance, Bybit, OKX from Xoomar."""
url = f"{XOOMAR_BASE}/api/markets/funding-rates"
response = requests.get(url)
response.raise_for_status()
return response.json()
# --- Signal Calculations ---
def calculate_funding_zscore(funding_data):
"""
Calculate how extreme the current funding rate is
compared to recent history using a z-score.
Z-score > 2: Funding is extremely high (crowded longs)
Z-score < -2: Funding is extremely low (crowded shorts)
"""
df = pd.DataFrame(funding_data)
df["fundingRate"] = df["fundingRate"].astype(float)
current_rate = df["fundingRate"].iloc[-1]
mean_rate = df["fundingRate"].mean()
std_rate = df["fundingRate"].std()
if std_rate == 0:
return 0.0, current_rate, mean_rate
zscore = (current_rate - mean_rate) / std_rate
return zscore, current_rate, mean_rate
def calculate_cross_exchange_spread(cross_data, target_asset="BTC"):
"""
Find the funding rate spread between exchanges for an asset.
Large spreads indicate divergent positioning across venues.
"""
asset_rates = []
for entry in cross_data.get("data", []):
base = entry.get("baseAsset", "").upper()
if base == target_asset and entry.get("fundingRate"):
asset_rates.append({
"exchange": entry["exchange"],
"funding_rate": float(entry["fundingRate"]),
})
if len(asset_rates) < 2:
return 0.0, asset_rates
rates = [r["funding_rate"] for r in asset_rates]
spread = max(rates) - min(rates)
return spread, asset_rates
def calculate_oi_concentration(cross_data, target_asset="BTC"):
"""
Calculate how concentrated open interest is across exchanges
using the Herfindahl Index.
High concentration (>0.5) means one exchange dominates,
creating cascading liquidation risk.
"""
oi_by_exchange = []
for entry in cross_data.get("data", []):
base = entry.get("baseAsset", "").upper()
oi_value = entry.get("openInterestValue")
if base == target_asset and oi_value:
oi_by_exchange.append({
"exchange": entry["exchange"],
"oi_value": float(oi_value),
})
if not oi_by_exchange:
return 0.0, []
total_oi = sum(e["oi_value"] for e in oi_by_exchange)
for e in oi_by_exchange:
e["share_pct"] = (e["oi_value"] / total_oi) * 100 if total_oi > 0 else 0
herfindahl = sum((e["share_pct"] / 100) ** 2 for e in oi_by_exchange)
return herfindahl, oi_by_exchange
if __name__ == "__main__":
funding_data = get_funding_history(SYMBOL, limit=100)
cross_data = get_cross_exchange_funding()
zscore, current_rate, mean_rate = calculate_funding_zscore(funding_data)
print(f"Z-Score: {zscore:.2f}, Current Rate: {current_rate*100:.4f}%, Mean: {mean_rate*100:.4f}%")
spread, exchange_rates = calculate_cross_exchange_spread(cross_data, "BTC")
print(f"Cross-exchange spread: {spread*100:.4f}%")
herfindahl, oi_breakdown = calculate_oi_concentration(cross_data, "BTC")
print(f"Herfindahl Index: {herfindahl:.3f}")
Your scanner now has three quantitative signals that measure different dimensions of derivatives market stress. Next up, you will build a confluence detector that fires when multiple signals align at the same time.
Build the Signal Confluence Detector
You now have three independent metrics: funding z-score, cross-exchange spread, and OI concentration. Each one tells you something about market positioning, but any single metric can generate false signals.
Professional quant traders solve this by requiring confluence. Multiple indicators must align before a signal becomes actionable. In this step, you will build a scanner that classifies each metric, detects when two or more signals fire simultaneously, and prints a formatted dashboard.
In this step, get ready to:
- Define signal thresholds that classify each metric as bullish, bearish, or cautionary.
- Build confluence logic that flags when multiple signals align.
- Create a formatted dashboard that displays all metrics and active signals in one run.
Define signal thresholds and classify each metric
Each of your three metrics needs a threshold that separates noise from a meaningful signal. When a metric crosses its threshold, the scanner classifies it with a direction label.
Here is how the classification works inside the scanner function you will build shortly:
signals = []
if zscore > 2:
signals.append(("BEARISH", "Funding extreme HIGH - crowded longs, squeeze risk"))
elif zscore < -2:
signals.append(("BULLISH", "Funding extreme LOW - crowded shorts, squeeze risk"))
if spread > 0.0005:
signals.append(("NEUTRAL", f"Funding divergence ({spread * 100:.4f}% spread)"))
if herfindahl > 0.5:
signals.append(("CAUTION", "OI highly concentrated on one exchange"))
What do these thresholds mean?
- Z-score > 2 (BEARISH): Funding is more than 2 standard deviations above the mean. Longs are crowded and paying a premium, creating squeeze risk to the downside.
- Z-score < -2 (BULLISH): Shorts are crowded. A short squeeze becomes likely when this many traders are paying to stay short.
- Spread > 0.05% (NEUTRAL): Different exchanges disagree on positioning. This is an arbitrage opportunity rather than a directional signal.
- Herfindahl > 0.5 (CAUTION): One exchange holds more than half the open interest. If that exchange's liquidation engine fires, cascading liquidations follow.
Build the confluence logic that fires when multiple signals align
A single signal is noise. Two or more signals firing at the same time is confluence. This is the core principle: aligned signals are actionable.
The confluence detection logic collects all active signals into a list and checks the count:
if signals:
print(f"\n Active Signals: {len(signals)}")
for direction, description in signals:
print(f" [{direction:>7}] {description}")
if len(signals) >= 2:
print("\n *** CONFLUENCE DETECTED ***")
print(" Multiple signals aligned - investigate further")
else:
print("\n No significant signals. Market positioning is neutral.")
Why require 2 or more signals?
Any single metric can trigger on random market noise. But when funding is extreme AND open interest is concentrated on one exchange, you have a structural setup. The probability of a squeeze event rises significantly when multiple independent indicators point the same direction.
Add a formatted output that shows the full scanner dashboard
Now you will combine everything into a single scan_for_edge() function. This function fetches all data, prints each metric in a formatted section, classifies signals, and detects confluence.
- In edge_scanner.py, find the existing if __name__ == "__main__": block at the bottom of your file.
- Delete the entire if __name__ == "__main__": block and everything below it.
- Replace it with the following code:
# --- Main Scanner ---
def scan_for_edge():
"""
Main edge scanner. Pulls data from multiple sources,
calculates quantitative signals, and detects confluence.
"""
print("=" * 60)
print(" CRYPTO DERIVATIVES EDGE SCANNER")
print(f" {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}")
print("=" * 60)
# --- 1. Current Open Interest (Binance) ---
print("\n[1] OPEN INTEREST (Binance USDT-M)")
print("-" * 40)
oi_data = get_current_oi(SYMBOL)
oi_value = float(oi_data["openInterest"])
print(f" BTC Open Interest: {oi_value:,.2f} contracts")
# --- 2. Funding Rate Analysis (Binance) ---
print("\n[2] FUNDING RATE ANALYSIS (Binance)")
print("-" * 40)
funding_data = get_funding_history(SYMBOL, limit=100)
zscore, current_rate, mean_rate = calculate_funding_zscore(funding_data)
print(f" Current Rate: {current_rate * 100:.4f}%")
print(f" Mean Rate (last 100 periods): {mean_rate * 100:.4f}%")
print(f" Z-Score: {zscore:.2f}")
if abs(zscore) > 2:
print(f" ** EXTREME: {abs(zscore):.1f} std devs from mean **")
# --- 3. Cross-Exchange Funding Spread (Xoomar) ---
print("\n[3] CROSS-EXCHANGE FUNDING SPREAD")
print("-" * 40)
cross_data = get_cross_exchange_funding()
spread, exchange_rates = calculate_cross_exchange_spread(cross_data, "BTC")
for r in sorted(exchange_rates, key=lambda x: x["funding_rate"], reverse=True):
print(f" {r['exchange']:>8}: {r['funding_rate'] * 100:>8.4f}%")
print(f" Spread: {spread * 100:.4f}%")
# --- 4. OI Concentration ---
print("\n[4] OI CONCENTRATION ANALYSIS")
print("-" * 40)
herfindahl, oi_breakdown = calculate_oi_concentration(cross_data, "BTC")
for e in sorted(oi_breakdown, key=lambda x: x["oi_value"], reverse=True):
print(f" {e['exchange']:>8}: ${e['oi_value']/1e9:.2f}B ({e['share_pct']:.1f}%)")
print(f" Herfindahl Index: {herfindahl:.3f} (1.0=monopoly, 0.33=equal)")
# --- 5. Signal Confluence ---
print("\n" + "=" * 60)
print(" SIGNAL CONFLUENCE")
print("=" * 60)
signals = []
if zscore > 2:
signals.append(("BEARISH", "Funding extreme HIGH - crowded longs, squeeze risk"))
elif zscore < -2:
signals.append(("BULLISH", "Funding extreme LOW - crowded shorts, squeeze risk"))
if spread > 0.0005:
signals.append(("NEUTRAL", f"Funding divergence ({spread * 100:.4f}% spread)"))
if herfindahl > 0.5:
signals.append(("CAUTION", "OI highly concentrated on one exchange"))
if signals:
print(f"\n Active Signals: {len(signals)}")
for direction, description in signals:
print(f" [{direction:>7}] {description}")
if len(signals) >= 2:
print("\n *** CONFLUENCE DETECTED ***")
print(" Multiple signals aligned - investigate further")
else:
print("\n No significant signals. Market positioning is neutral.")
print("\n" + "=" * 60)
return signals
if __name__ == "__main__":
scan_for_edge()
What does this code do?
The scan_for_edge() function is your complete scanner. It performs five jobs in sequence:
- Section 1 fetches current open interest from Binance using the function you wrote in Step 2.
- Section 2 pulls 100 funding rate periods and calculates the z-score using your calculation function from Step 4.
- Section 3 fetches cross-exchange data from Xoomar and calculates the funding spread.
- Section 4 calculates OI concentration across exchanges using the Herfindahl Index.
- Section 5 classifies each metric against its threshold and checks for confluence.
- Save edge_scanner.py.
- Run the complete scanner by executing this command in your terminal:
python edge_scanner.py
- You should see a formatted dashboard with five sections: Open Interest, Funding Rate Analysis, Cross-Exchange Funding Spread, OI Concentration Analysis, and Signal Confluence.
- The dashboard header includes a timestamp showing when the scan ran.
- At the bottom, you will see either active signals listed with their directions or a message saying market positioning is neutral.
Seeing an error when you run the scanner?
- If you see an indentation error, make sure the entire scan_for_edge() function body is indented with 4 spaces consistently. Python is sensitive to mixed tabs and spaces.
- If you see a NameError for scan_for_edge, check that you deleted the old if __name__ block completely before pasting the new code.
- If you get a network timeout, your internet connection may have dropped. Wait a few seconds and run it again.
Why is my edge scanner throwing an error?
How do I interpret the output?
If funding is currently near the mean (z-score between -2 and 2), the market is in a neutral state. Most of the time, you will see no active signals. That is normal and expected.
When you do see CONFLUENCE DETECTED, it means the market has an unusual structural setup. This is the kind of moment quant funds pay attention to. It does not guarantee a price move, but it means the probability of a squeeze or sharp repositioning is elevated.
✔️ Awesome, I've got everything!
Great. Double check you have saved your file.
ⓧ I'd like to double check the full code
Here is the complete edge_scanner.py file as it should look now:
import requests
import pandas as pd
from datetime import datetime
# --- Configuration ---
BINANCE_FUTURES_BASE = "https://fapi.binance.com"
XOOMAR_BASE = "https://xoomar.com"
SYMBOL = "BTCUSDT"
# --- Binance Data Fetching ---
def get_current_oi(symbol):
"""Fetch current open interest from Binance USDT-M futures."""
url = f"{BINANCE_FUTURES_BASE}/fapi/v1/openInterest"
response = requests.get(url, params={"symbol": symbol})
response.raise_for_status()
return response.json()
def get_funding_history(symbol, limit=100):
"""Fetch funding rate history from Binance."""
url = f"{BINANCE_FUTURES_BASE}/fapi/v1/fundingRate"
response = requests.get(url, params={"symbol": symbol, "limit": limit})
response.raise_for_status()
return response.json()
# --- Xoomar Data Fetching ---
def get_cross_exchange_funding():
"""Fetch current funding rates across Binance, Bybit, OKX from Xoomar."""
url = f"{XOOMAR_BASE}/api/markets/funding-rates"
response = requests.get(url)
response.raise_for_status()
return response.json()
# --- Signal Calculations ---
def calculate_funding_zscore(funding_data):
"""
Calculate how extreme the current funding rate is
compared to recent history using a z-score.
Z-score > 2: Funding is extremely high (crowded longs)
Z-score < -2: Funding is extremely low (crowded shorts)
"""
df = pd.DataFrame(funding_data)
df["fundingRate"] = df["fundingRate"].astype(float)
current_rate = df["fundingRate"].iloc[-1]
mean_rate = df["fundingRate"].mean()
std_rate = df["fundingRate"].std()
if std_rate == 0:
return 0.0, current_rate, mean_rate
zscore = (current_rate - mean_rate) / std_rate
return zscore, current_rate, mean_rate
def calculate_cross_exchange_spread(cross_data, target_asset="BTC"):
"""
Find the funding rate spread between exchanges for an asset.
Large spreads indicate divergent positioning across venues.
"""
asset_rates = []
for entry in cross_data.get("data", []):
base = entry.get("baseAsset", "").upper()
if base == target_asset and entry.get("fundingRate"):
asset_rates.append({
"exchange": entry["exchange"],
"funding_rate": float(entry["fundingRate"]),
})
if len(asset_rates) < 2:
return 0.0, asset_rates
rates = [r["funding_rate"] for r in asset_rates]
spread = max(rates) - min(rates)
return spread, asset_rates
def calculate_oi_concentration(cross_data, target_asset="BTC"):
"""
Calculate how concentrated open interest is across exchanges
using the Herfindahl Index.
High concentration (>0.5) means one exchange dominates,
creating cascading liquidation risk.
"""
oi_by_exchange = []
for entry in cross_data.get("data", []):
base = entry.get("baseAsset", "").upper()
oi_value = entry.get("openInterestValue")
if base == target_asset and oi_value:
oi_by_exchange.append({
"exchange": entry["exchange"],
"oi_value": float(oi_value),
})
if not oi_by_exchange:
return 0.0, []
total_oi = sum(e["oi_value"] for e in oi_by_exchange)
for e in oi_by_exchange:
e["share_pct"] = (e["oi_value"] / total_oi) * 100 if total_oi > 0 else 0
herfindahl = sum((e["share_pct"] / 100) ** 2 for e in oi_by_exchange)
return herfindahl, oi_by_exchange
# --- Main Scanner ---
def scan_for_edge():
"""
Main edge scanner. Pulls data from multiple sources,
calculates quantitative signals, and detects confluence.
"""
print("=" * 60)
print(" CRYPTO DERIVATIVES EDGE SCANNER")
print(f" {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}")
print("=" * 60)
# --- 1. Current Open Interest (Binance) ---
print("\n[1] OPEN INTEREST (Binance USDT-M)")
print("-" * 40)
oi_data = get_current_oi(SYMBOL)
oi_value = float(oi_data["openInterest"])
print(f" BTC Open Interest: {oi_value:,.2f} contracts")
# --- 2. Funding Rate Analysis (Binance) ---
print("\n[2] FUNDING RATE ANALYSIS (Binance)")
print("-" * 40)
funding_data = get_funding_history(SYMBOL, limit=100)
zscore, current_rate, mean_rate = calculate_funding_zscore(funding_data)
print(f" Current Rate: {current_rate * 100:.4f}%")
print(f" Mean Rate (last 100 periods): {mean_rate * 100:.4f}%")
print(f" Z-Score: {zscore:.2f}")
if abs(zscore) > 2:
print(f" ** EXTREME: {abs(zscore):.1f} std devs from mean **")
# --- 3. Cross-Exchange Funding Spread (Xoomar) ---
print("\n[3] CROSS-EXCHANGE FUNDING SPREAD")
print("-" * 40)
cross_data = get_cross_exchange_funding()
spread, exchange_rates = calculate_cross_exchange_spread(cross_data, "BTC")
for r in sorted(exchange_rates, key=lambda x: x["funding_rate"], reverse=True):
print(f" {r['exchange']:>8}: {r['funding_rate'] * 100:>8.4f}%")
print(f" Spread: {spread * 100:.4f}%")
# --- 4. OI Concentration ---
print("\n[4] OI CONCENTRATION ANALYSIS")
print("-" * 40)
herfindahl, oi_breakdown = calculate_oi_concentration(cross_data, "BTC")
for e in sorted(oi_breakdown, key=lambda x: x["oi_value"], reverse=True):
print(f" {e['exchange']:>8}: ${e['oi_value']/1e9:.2f}B ({e['share_pct']:.1f}%)")
print(f" Herfindahl Index: {herfindahl:.3f} (1.0=monopoly, 0.33=equal)")
# --- 5. Signal Confluence ---
print("\n" + "=" * 60)
print(" SIGNAL CONFLUENCE")
print("=" * 60)
signals = []
if zscore > 2:
signals.append(("BEARISH", "Funding extreme HIGH - crowded longs, squeeze risk"))
elif zscore < -2:
signals.append(("BULLISH", "Funding extreme LOW - crowded shorts, squeeze risk"))
if spread > 0.0005:
signals.append(("NEUTRAL", f"Funding divergence ({spread * 100:.4f}% spread)"))
if herfindahl > 0.5:
signals.append(("CAUTION", "OI highly concentrated on one exchange"))
if signals:
print(f"\n Active Signals: {len(signals)}")
for direction, description in signals:
print(f" [{direction:>7}] {description}")
if len(signals) >= 2:
print("\n *** CONFLUENCE DETECTED ***")
print(" Multiple signals aligned - investigate further")
else:
print("\n No significant signals. Market positioning is neutral.")
print("\n" + "=" * 60)
return signals
if __name__ == "__main__":
scan_for_edge()
Your edge scanner is complete. You have a working tool that pulls live derivatives data from multiple sources, calculates professional-grade metrics, and flags structural setups where multiple indicators converge. Next up, the Secret Mission extends this to scan multiple assets simultaneously.
Secret mission
Multi-Asset Scanner with On-Chain Context
Your scanner currently watches BTC alone. Real quant desks monitor multiple assets simultaneously to find which one is flashing the strongest signal right now. Extend your scanner to cover BTC, ETH, and SOL, rank them by signal strength, and validate signals with on-chain data from Glassnode.
Clean Up Your Resources
Clean Up Your Resources
This project runs entirely locally and uses only free APIs. There are no ongoing costs, no running servers, and no paid subscriptions to worry about.
Decide whether to keep your resources in place or delete them entirely.
Resources you used:
- A Python virtual environment in edge-scanner/venv.
- A Glassnode MCP server configured in Claude Code.
Keep everything running
No action needed. Choose this if you plan to keep running and extending the scanner.
- All APIs used (Binance market data, Xoomar) are free with unlimited access. No costs accumulate.
- The Glassnode MCP server provides 30 days of on-chain data on public access indefinitely. No trial expiration.
- Your virtual environment stays on disk but does not run in the background.
Pause - I'll come back to this later
Since there are no running processes or paid services, pausing is the same as keeping. Your files stay on disk until you return.
- No servers to stop. No subscriptions to pause.
- When you come back, activate the virtual environment again by running source venv/bin/activate from inside the edge-scanner folder.
Delete - I don't want to use this again
Remove all project files and the MCP server configuration to start fresh.
- Delete the project folder by running this command from your Desktop:
- On macOS/Linux:
rm -rf ~/Desktop/edge-scanner
- On Windows:
Remove-Item -Recurse -Force ~\Desktop\edge-scanner
- Remove the Glassnode MCP server from Claude Code by running:
claude mcp remove glassnode
- That removes all project resources. The requests and pandas packages were installed inside the virtual environment, so deleting the folder removes them too.
That's a Wrap!
That's a Wrap!
Nice work! You just built a quantitative derivatives edge scanner from scratch using only free data sources.
You've learned how to:
- Pull live open interest and funding rate data from Binance's futures API and aggregate cross-exchange data from Xoomar covering Binance, Bybit, and OKX in a single call.
- Calculate professional-grade metrics including a funding z-score, cross-exchange spread, and OI concentration (Herfindahl Index) to quantify positioning extremes.
- Build a signal confluence detector that identifies when multiple derivatives indicators align simultaneously, and configured Glassnode MCP in Claude Code for on-chain research.
- Secret Mission: Extended the scanner to monitor BTC, ETH, and SOL simultaneously with a ranked summary table and integrated on-chain context from Glassnode when confluence fires.
Ready to quiz yourself?