Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 24 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,27 @@
# Cross-Platform Market Matcher

This module allows you to compare and log markets, events, and profiles between Kalshi and Polymarket using their public APIs.

### How to Run

**One-time scan:**
```bash
python cross_platform_runner.py --once --category Politics
```

**Continuous monitoring (every 60 seconds):**
```bash
python cross_platform_runner.py --watch 60 --category Politics
```

You can change the category (e.g., "Economy", "Crypto", "Sports") as needed.

**What it does:**
- Fetches all Kalshi series for the given category
- For each series, searches Polymarket for related markets, events, and profiles using the series title
- Logs all checked markets, events, and profiles, including reasons for non-matches

This is useful for research, debugging, and monitoring cross-platform market overlap.
# Arbitrage Bot

Automated arbitrage detection bot for prediction markets (Polymarket & Kalshi) made by Jasper Buffet, Mark Burry and Duco Munger.
Expand Down
6 changes: 3 additions & 3 deletions config.yaml
Original file line number Diff line number Diff line change
@@ -1,17 +1,17 @@
apis:
polymarket:
base_url: "https://gamma-api.polymarket.com"
base_url: "https://matic-poly.market"
timeout: 30

kalshi:
base_url: "https://api.elections.kalshi.com"
base_url: "https://trading-api.kalshi.com"
api_key_id: "${KALSHI_API_KEY_ID}"
private_key: "${KALSHI_PRIVATE_KEY}"
timeout: 30

matching:
min_confidence: 0.65
use_semantic: true
use_semantic: false
semantic_model: "all-MiniLM-L6-v2"

arbitrage:
Expand Down
137 changes: 137 additions & 0 deletions cross_platform_runner.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
import argparse
import asyncio
import time
import aiohttp
from src.matching.cross_platform_matcher import cross_platform_match

KALSHI_URL = "https://api.elections.kalshi.com"
POLYMARKET_URL = "https://gamma-api.polymarket.com"

async def run_once(category):
print(f"Fetching Kalshi series for category '{category}'...")
from src.matching.cross_platform_matcher import fetch_kalshi_series, search_polymarket
kalshi_series = await fetch_kalshi_series(KALSHI_URL)
checked = 0
matched = 0
cross_reference = []
for series in kalshi_series:
if category and series.get("category", "").lower() != category.lower():
continue
title = series.get("title", "")
if not title:
print(f"Skipping Kalshi series (no title): {series}")
continue
print(f"Checking Kalshi series: {series.get('ticker','')} | {title} | {series.get('category','')}")
async def full_search(base_url, query):
url = f"{base_url}/public-search?q={query}"
async with aiohttp.ClientSession() as session:
async with session.get(url) as resp:
resp.raise_for_status()
return await resp.json()

try:
search_results = await full_search(POLYMARKET_URL, title)
except aiohttp.ClientResponseError as e:
print(f" Polymarket search failed for title '{title}': {e.status} {e.message}")
checked += 1
print(f" No Polymarket markets, events, or profiles for title '{title}' (API error)")
continue
except Exception as e:
print(f" Polymarket search failed for title '{title}': {e}")
checked += 1
print(f" No Polymarket markets, events, or profiles for title '{title}' (API error)")
continue
checked += 1
found_any = False
for key in ["markets", "events", "profiles"]:
items = search_results.get(key, [])
if items:
found_any = True
print(f" Found {len(items)} Polymarket {key} for title '{title}':")
for item in items:
if key == "markets":
print(f" Market: {item.get('id','')} | {item.get('question','')}" )
elif key == "events":
event_id = item.get('id','')
event_title = item.get('name','')
ticker = None
event_data = None
market_list = []
if event_id:
event_url = f"https://gamma-api.polymarket.com/events/{event_id}"
async with aiohttp.ClientSession() as session:
try:
async with session.get(event_url) as event_resp:
event_resp.raise_for_status()
event_data = await event_resp.json()
ticker = event_data.get('ticker') or event_data.get('name')
market_list = event_data.get('markets', [])
except Exception as e:
print(f" Error fetching event details for {event_id}: {e}")
print(f" Event: {event_id} | {ticker if ticker else event_title}")
open_markets = [m for m in market_list if m.get('active', True) and not m.get('closed', False)]
print(f" Open markets for event {event_id}: {len(open_markets)}")
for market in open_markets:
market_id = market.get('id', '')
question = market.get('question', '')
volume = market.get('volume', market.get('volumeNum', 0))
outcomes = market.get('outcomes', [])
outcome_prices = market.get('outcomePrices', [])
last_volume = market.get('volume24hr', 0)
# Parse outcomes and prices if they are JSON strings
import json as _json
if isinstance(outcomes, str):
try:
outcomes = _json.loads(outcomes)
except:
pass
if isinstance(outcome_prices, str):
try:
outcome_prices = _json.loads(outcome_prices)
except:
pass
print(f" Market: {market_id} | {question}")
print(f" Volume: {volume}")
print(f" Last 24hr Volume: {last_volume}")
print(f" Outcomes: {outcomes}")
print(f" Prices: {outcome_prices}")
# Save standardized info for cross referencing
cross_reference.append({
"kalshi_series": series,
"polymarket_event": event_data if event_data else item,
"polymarket_markets": open_markets
})
elif key == "profiles":
print(f" Profile: {item.get('id','')} | {item.get('name','')}")
if found_any:
matched += 1
else:
print(f" No Polymarket markets, events, or profiles for title '{title}'")
# Output standardized cross-reference data
import json
print("\nStandardized cross-reference data:")
print(json.dumps(cross_reference, indent=2, default=str))
print(f"Checked {checked} Kalshi series, {matched} had Polymarket matches.")

async def run_watch(category, interval):
while True:
await run_once(category)
print(f"Waiting {interval} seconds before next check...")
await asyncio.sleep(interval)

def main():
parser = argparse.ArgumentParser(description="Cross-platform market matcher")
parser.add_argument("--category", type=str, default="Politics", help="Market category to check")
parser.add_argument("--once", action="store_true", help="Run one-time market scan")
parser.add_argument("--watch", type=int, metavar="SECONDS", help="Continuously check every N seconds")
args = parser.parse_args()

if args.once:
asyncio.run(run_once(args.category))
elif args.watch:
asyncio.run(run_watch(args.category, args.watch))
else:
print("Specify --once for one-time scan or --watch N for continuous mode.")

if __name__ == "__main__":
main()
148 changes: 108 additions & 40 deletions src/api/kalshi.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,24 @@
from typing import List, Optional
from typing import List, Optional, Dict
from datetime import datetime
from .base import BaseAPIClient, Market
from ..utils.logger import logger
import aiohttp
import asyncio
import time
import jwt
from cryptography.hazmat.primitives import serialization
from cryptography.hazmat.backends import default_backend
from .base import BaseAPIClient, Market
from ..utils.logger import logger
from ..core import cache

class KalshiClient(BaseAPIClient):
# Map our standard categories to Kalshi series tags
CATEGORY_MAP = {
"politics": ["POLITICS", "ELECTORAL"],
"economy": ["ECONOMIC", "FED", "MACRO"],
"crypto": ["CRYPTO"],
"sports": ["SPORTS", "NFL", "NBA", "MLB", "NHL"],
}

def __init__(self, base_url: str, timeout: int = 30,
api_key_id: Optional[str] = None,
private_key_str: Optional[str] = None):
Expand All @@ -17,12 +28,21 @@ def __init__(self, base_url: str, timeout: int = 30,
self.token = None
self.token_expiry = 0

async def _make_async_request(self, session: aiohttp.ClientSession,
method: str, endpoint: str, **kwargs) -> Dict:
"""Make an async HTTP request."""
url = f"{self.base_url}{endpoint}"

try:
async with session.request(method, url, **kwargs) as response:
response.raise_for_status()
return await response.json()
except Exception as e:
logger.error(f"Async request failed for {url}: {e}")
raise

def _ensure_authenticated(self) -> None:
"""
Attempt to authenticate with Kalshi API.
Note: Authentication is NOT required for read-only market data access.
It's only needed for trading operations (placing orders, etc).
"""
"""Ensure we have a valid JWT token."""
if self.token and time.time() < self.token_expiry:
return

Expand All @@ -31,68 +51,116 @@ def _ensure_authenticated(self) -> None:
return

try:
# Generate JWT token signed with private key
current_time = int(time.time())
payload = {
'iss': self.api_key_id,
'exp': current_time + 1800, # 30 minutes expiry
'exp': current_time + 1800,
'iat': current_time
}

# Load the private key (replace literal \n with actual newlines)
private_key_pem = self.private_key_str.replace('\\n', '\n')
private_key = serialization.load_pem_private_key(
private_key_pem.encode(),
password=None,
backend=default_backend()
)

# Sign the JWT
signed_jwt = jwt.encode(payload, private_key, algorithm='RS256')

# Set JWT as authorization header for authenticated requests
# Note: Kalshi may not have a login endpoint, JWT is used directly
self.session.headers.update({"Authorization": f"Bearer {signed_jwt}"})
self.token = signed_jwt
self.token_expiry = current_time + 1800
logger.info("Kalshi API authentication configured (JWT Bearer token)")

logger.info("Kalshi API authentication configured")

except Exception as e:
logger.debug(f"Kalshi authentication setup failed (not required for read-only): {e}")
logger.error(f"Kalshi authentication failed: {e}")
raise

def get_markets(self, limit: int = 200, status: str = "open") -> List[Market]:
cache_key = f"kalshi_markets_{limit}_{status}"
cached = self._get_cached(cache_key)
async def get_series(self) -> List[Dict]:
"""Fetch all market series from Kalshi."""
cache_key = "kalshi_series"
cached = cache.load_data(cache_key, max_age_hours=24)
if cached:
return cached

self._ensure_authenticated()
headers = {"Authorization": f"Bearer {self.token}"} if self.token else {}

async with aiohttp.ClientSession(headers=headers) as session:
data = await self._make_async_request(
session, "GET", "/trading-api/v2/series"
)
series = data.get("series", [])
cache.save_data(cache_key, series)
return series

async def get_markets_by_category(self, category: str,
limit: int = 200) -> List[Market]:
"""
Fetch markets for a specific category.

Args:
category: Category to fetch markets for (politics, economy, crypto, sports)
limit: Maximum number of markets to fetch
"""
cache_key = f"kalshi_markets_{category}_{limit}"
cached = cache.load_data(cache_key, max_age_hours=1)
if cached:
return [Market(**m) for m in cached]

try:
self._ensure_authenticated()
headers = {"Authorization": f"Bearer {self.token}"} if self.token else {}

endpoint = "/trade-api/v2/markets"
params = {
"limit": limit,
"status": status
}
# Get relevant series tags for category
category_tags = self.CATEGORY_MAP.get(category.lower(), [])
if not category_tags:
logger.warning(f"Unknown category: {category}")
return []

data = self._make_request("GET", endpoint, params=params)
market_data = data.get("markets", [])
# Get all series first
series = await self.get_series()
relevant_series = [
s for s in series
if any(tag in s.get("tags", []) for tag in category_tags)
]

markets = []
for item in market_data:
try:
market = self._parse_market(item)
if market:
markets.append(market)
except Exception as e:
logger.warning(f"Failed to parse Kalshi market: {e}")
continue

self._set_cache(cache_key, markets)
logger.info(f"Fetched {len(markets)} markets from Kalshi")
async with aiohttp.ClientSession(headers=headers) as session:
for series_data in relevant_series:
series_id = series_data["id"]

# Fetch markets in this series
endpoint = f"/trading-api/v2/markets"
params = {
"series_id": series_id,
"status": "active",
"limit": limit
}

try:
data = await self._make_async_request(
session, "GET", endpoint, params=params
)

for item in data.get("markets", []):
market = self._parse_market(item)
if market:
markets.append(market)

if len(markets) >= limit:
break

except Exception as e:
logger.error(f"Failed to fetch markets for series {series_id}: {e}")
continue

# Cache the results
cache.save_data(cache_key, [m.__dict__ for m in markets])
logger.info(f"Fetched {len(markets)} {category} markets from Kalshi")
return markets

except Exception as e:
logger.error(f"Failed to fetch Kalshi markets: {e}")
logger.error(f"Failed to fetch {category} markets from Kalshi: {e}")
return []

def _parse_market(self, data: dict) -> Optional[Market]:
Expand Down
Loading