pam ▸ ANALYSIS_matching_algorithm.md
updated 2026-03-15

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)

2. Canonical Representation (matcher/ontology.py)

3. Equivalence Scoring (matcher/align.py)

4. Cross-Platform Matching (arbitrage/crypto_scanner.py)


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

  1. Subject extraction (High priority) - Most matches fail at this step
  2. Threshold extraction (High priority) - Directly affects spread calculation accuracy
  3. Oracle risk scoring (Medium priority) - Important for execution quality but less impactful on discovery

Testing Recommendations

  1. Create test fixtures with known Kalshi/Polymarket market pairs
  2. Measure precision/recall before and after changes
  3. Track oracle divergence historically to validate risk model
  4. A/B test on live discovery to measure improvement in valid match rate