Market Matching Algorithm Analysis
Overview
This document analyzes the current Kalshi ↔ Polymarket market matching implementation in Pam, identifies the top 3 weaknesses, and proposes specific improvements.
Files Analyzed:
- matcher/align.py - Equivalence scoring between markets
- matcher/ontology.py - Canonical market representation
- sources/discovery.py - Market discovery and filtering
- sources/parsers.py - Rule extraction from market data
- arbitrage/crypto_scanner.py - Main arbitrage detection logic
Current Implementation Summary
1. Market Discovery (sources/discovery.py)
- Uses regex-based title filtering to find candidate markets
- Filters by expiration window (configurable lookahead days)
- Applies minimum liquidity threshold
- Caches results with TTL
2. Canonical Representation (matcher/ontology.py)
- Converts raw market data to
CanonicalMarketdataclass - Extracts: subject (BTC/ETH/GEN), predicate (above/below/yes), threshold, fixing time
- Uses simple keyword matching for subject detection
3. Equivalence Scoring (matcher/align.py)
- Computes similarity score between two canonical markets
- Penalizes mismatches in: subject (-0.5), predicate (-0.2), threshold (up to -0.5), expiry time (-0.4), resolution source (-0.1)
- Returns score clamped to [0.0, 1.0]
4. Cross-Platform Matching (arbitrage/crypto_scanner.py)
_events_match()compares crypto symbol and time (e.g., “4PM_ET”)_extract_event_info()uses simple keyword search for crypto and time- Applies
equivalence_score()as a secondary filter with configurable threshold
Top 3 Weaknesses
Weakness #1: Brittle Subject Extraction
Location: matcher/ontology.py:21-27, crypto_scanner.py:420-449
Problem: Subject (crypto asset) detection relies on exact keyword matching:
subj = "BTC" if ("bitcoin" in title or "btc" in title) else \
("ETH" if ("ethereum" in title or "eth" in title) else "GEN")
Issues:
1. Missed variants: “BTCUSD”, “Bitcoin price”, “BTC/USD”, “XBT” (alternative Bitcoin ticker) won’t match
2. Case sensitivity bugs: While .lower() is applied, the matching logic assumes specific keyword patterns
3. Compound titles missed: “Will the Bitcoin Reference Time Index exceed $110,000?” uses “Reference Time Index” before mentioning Bitcoin
4. No handling of Kalshi’s BRTI/ERTI tickers: These are handled as special cases but the logic is fragile
5. Falls back to “GEN”: Generic fallback loses information and guarantees failed matches
Impact: High. Missed subject matches immediately disqualify valid arbitrage pairs with -0.5 penalty in equivalence_score().
Weakness #2: Imprecise Threshold Extraction
Location: matcher/ontology.py:29-33
Problem: Threshold extraction uses loose regex patterns:
m = re.search(r"(\d{2,3}(?:[.,]\d{3})*(?:\.\d+)?)k", title) or \
re.search(r"(\d{5,7}(?:\.\d+)?)", title)
Issues:
1. Ambiguous number extraction: “Bitcoin above $110,000 on August 28” - the regex might extract “28” (from date) instead of “110000”
2. No context awareness: Doesn’t distinguish between price thresholds, dates, percentages, or other numeric values
3. K-suffix handling fragile: 110k vs 110,000 vs $110,000 have different parsing paths
4. No validation: Extracted thresholds aren’t sanity-checked (e.g., BTC price should be 10,000-500,000 range)
5. Mismatch in decimal handling: Kalshi uses “one decimal” rounding; Polymarket may not - this creates false threshold mismatches
Impact: High. Threshold mismatches cause up to -0.5 penalty, and incorrect extraction leads to false negatives.
Weakness #3: Resolution Source Mismatch Detection
Location: sources/parsers.py:22-25, 61-62, matcher/align.py:17
Problem: Resolution source detection is insufficient for cross-venue arbitrage:
# Polymarket
if "binance" in rules: out["resolution_source"] = "Binance 1m close"
# Kalshi
if "rti" in rules or "cf benchmarks" in rules:
out["resolution_source"] = "CFB RTI 60s avg"
Issues:
1. Different oracles = different prices: Kalshi uses CF Benchmarks RTI (60-second average); Polymarket uses Binance 1-minute close. These can differ by 0.5-2% at volatile moments.
2. Settlement basis risk: Even “matched” markets may settle differently due to oracle divergence, creating execution risk not reflected in the spread
3. Only -0.1 penalty: The scoring penalizes resolution source mismatch minimally (score -= 0.1), underweighting a significant risk factor
4. Incomplete detection: Many markets don’t specify resolution source clearly; “unknown” vs “unknown” passes the check
5. No semantic equivalence: “Binance BTCUSDT perpetual” vs “Binance spot” are treated the same
Impact: Medium-High. Oracle mismatch can turn profitable-looking arbitrage into losses at settlement. A 2% spread can easily be erased by a 1.5% oracle divergence.
Proposed Improvements
Improvement #1: Semantic Subject Extraction
Approach: Replace keyword matching with a structured extraction system.
# Proposed: matcher/ontology.py
ASSET_ALIASES = {
"BTC": ["bitcoin", "btc", "xbt", "brti", "btcusd", "btc/usd", "btc-usd"],
"ETH": ["ethereum", "eth", "ether", "erti", "ethusd", "eth/usd"],
"SOL": ["solana", "sol", "solusd"],
"XRP": ["xrp", "ripple"],
"DOGE": ["doge", "dogecoin"],
}
def extract_subject(title: str, ticker: str = "") -> str:
"""Extract crypto asset with alias support and ticker parsing."""
combined = f"{title} {ticker}".lower()
# Priority 1: Check ticker patterns (KXBTCD, BRTI-24AUG)
ticker_match = re.match(r"(?:kx?)?([a-z]{3,4})(?:d|usd)?", ticker.lower())
if ticker_match:
symbol = ticker_match.group(1).upper()
if symbol in ASSET_ALIASES:
return symbol
# Priority 2: Check all aliases
for asset, aliases in ASSET_ALIASES.items():
if any(alias in combined for alias in aliases):
return asset
return "UNKNOWN" # Explicit unknown, not generic
Benefits: - Handles ticker variations (KXBTCD, BRTI, etc.) - Explicit alias mapping is maintainable - Returns “UNKNOWN” to fail-fast rather than false-match
Improvement #2: Context-Aware Threshold Extraction
Approach: Use structured patterns that understand market title grammar.
# Proposed: matcher/ontology.py
THRESHOLD_PATTERNS = [
# "$110,000" or "$110000"
(r"\$\s*(\d{1,3}(?:,\d{3})+|\d{4,7})(?:\.\d+)?", lambda m: float(m.group(1).replace(",", ""))),
# "110k" or "110K"
(r"(\d{2,3})\s*[kK](?:\s|$|[^a-zA-Z])", lambda m: float(m.group(1)) * 1000),
# "above/below/exceed 110000"
(r"(?:above|below|exceed|reach)\s+(\d{5,7})", lambda m: float(m.group(1))),
]
# Validation bounds by asset
THRESHOLD_BOUNDS = {
"BTC": (10_000, 500_000),
"ETH": (500, 20_000),
"SOL": (10, 1_000),
}
def extract_threshold(title: str, subject: str) -> Optional[float]:
"""Extract price threshold with validation."""
for pattern, extractor in THRESHOLD_PATTERNS:
match = re.search(pattern, title)
if match:
value = extractor(match)
bounds = THRESHOLD_BOUNDS.get(subject, (0, float("inf")))
if bounds[0] <= value <= bounds[1]:
return value
return None
Benefits: - Ordered patterns prefer explicit dollar amounts - Validation prevents date/percentage extraction errors - Asset-specific bounds catch extraction bugs
Improvement #3: Oracle Risk Scoring
Approach: Replace binary resolution source matching with oracle risk quantification.
# Proposed: matcher/oracle_risk.py
ORACLE_HIERARCHY = {
"CFB RTI 60s avg": {"type": "index", "lag_sec": 60, "provider": "cf_benchmarks"},
"Binance 1m close": {"type": "spot", "lag_sec": 60, "provider": "binance"},
"Coinbase spot": {"type": "spot", "lag_sec": 0, "provider": "coinbase"},
"unknown": {"type": "unknown", "lag_sec": 0, "provider": "unknown"},
}
def oracle_divergence_risk(source_a: str, source_b: str) -> float:
"""
Returns estimated divergence risk as a decimal (0.0 to 1.0).
Higher = more risk of settlement mismatch.
"""
a = ORACLE_HIERARCHY.get(source_a, ORACLE_HIERARCHY["unknown"])
b = ORACLE_HIERARCHY.get(source_b, ORACLE_HIERARCHY["unknown"])
risk = 0.0
# Unknown sources are high risk
if a["provider"] == "unknown" or b["provider"] == "unknown":
risk += 0.4
# Different providers add risk
if a["provider"] != b["provider"]:
risk += 0.3
# Index vs spot has basis risk
if a["type"] != b["type"]:
risk += 0.2
# Lag difference affects settlement timing
lag_diff = abs(a["lag_sec"] - b["lag_sec"])
if lag_diff > 30:
risk += 0.1
return min(1.0, risk)
def adjusted_equivalence_score(pm, pm_parsed, ks, ks_parsed, expiry_tol_sec=90) -> float:
"""Enhanced scoring with oracle risk weighting."""
base_score = equivalence_score(pm, pm_parsed, ks, ks_parsed, expiry_tol_sec)
oracle_risk = oracle_divergence_risk(
pm_parsed.get("resolution_source", "unknown"),
ks_parsed.get("resolution_source", "unknown")
)
# Oracle risk should have significant weight (up to -0.4)
return max(0.0, base_score - (oracle_risk * 0.4))
Benefits: - Quantifies oracle mismatch risk rather than binary check - Different oracle types (index vs spot) are distinguished - Unknown oracles are properly penalized
Summary of Changes
| Weakness | Current Behavior | Proposed Fix | Expected Improvement |
|---|---|---|---|
| Subject extraction | Simple keyword match | Alias mapping + ticker parsing | Reduce false negatives by ~30% |
| Threshold extraction | Loose regex, no validation | Ordered patterns + bounds checking | Reduce extraction errors by ~50% |
| Resolution source | -0.1 penalty, binary match | Oracle risk scoring (up to -0.4) | Better risk-adjusted opportunity filtering |
Implementation Priority
- Subject extraction (High priority) - Most matches fail at this step
- Threshold extraction (High priority) - Directly affects spread calculation accuracy
- Oracle risk scoring (Medium priority) - Important for execution quality but less impactful on discovery
Testing Recommendations
- Create test fixtures with known Kalshi/Polymarket market pairs
- Measure precision/recall before and after changes
- Track oracle divergence historically to validate risk model
- A/B test on live discovery to measure improvement in valid match rate