This repository contains a methodology and CLI tools for matching records that refer to the same real-world event across platforms such as Kalshi and Polymarket, even when names, tags, and IDs differ.
-
Canonicalize records Normalize every platform export into a common shape:
source,event_id,title,description,tags,category, and date-like fields. Preserve the raw record for audit/debugging. -
Normalize comparable text Lowercase, strip punctuation/accents, remove platform boilerplate words, and tokenize the title, tags, and description. Extract structured signals such as dates, years, percentages, prices, and other numeric thresholds.
-
Generate candidates with blocking Avoid all-pairs comparison at scale. Put records into blocks based on shared title tokens, tags, dates, years, entities, metrics, geography, and numeric thresholds. For large files, also add TF-IDF nearest-neighbor candidates when scikit-learn is installed. For small files, the tool scans all pairs.
-
Score multiple signals Pair scores combine:
- title token overlap
- full text token overlap
- character n-gram similarity
- fuzzy token-set/token-sort similarity via RapidFuzz when installed
- tag/category overlap
- date compatibility
- number/threshold compatibility
- structured entity/domain/metric/geography/comparator compatibility
- optional sentence-transformer semantic similarity
Date, numeric, entity, domain, metric, geography, and comparator contradictions apply penalties. The score remains explainable, which is useful for human review and threshold tuning.
-
Label and cluster Pairs above
--thresholdare labeledmatch; pairs above--review-thresholdare labeledreview. Matched pairs are connected into cluster IDs so the same event can span more than two sources. -
Evaluate and tune Build a labeled set of known matches and non-matches. Track precision for
matchrows and recall acrossmatch + reviewrows. Tune thresholds and feature weights with that labeled set before automating downstream merges. -
Train when labels exist Use labeled pairs to train a logistic regression scoring model. The trainer exports a small JSON model that the main matcher can load without depending on scikit-learn at runtime.
The matcher itself has fallbacks for missing optional packages. Install requirements for the better fuzzy scorer and model training:
python3 -m pip install -r requirements.txtInstall the heavier embedding dependency only if you plan to use --embeddings:
python3 -m pip install -r requirements-embeddings.txtpython3 event_matcher.py examples/events.jsonl --output matches.csvMultiple files are supported. If a record lacks a source, the filename stem is used.
python3 event_matcher.py kalshi_events.json polymarket_events.csv --output matches.jsonUse a review queue:
python3 event_matcher.py events.jsonl \
--threshold 0.80 \
--review-threshold 0.60 \
--output matches.csvEmit every scored pair, including non-matches:
python3 event_matcher.py events.jsonl --all-pairs --output scored_pairs.csvUse optional embeddings for semantic similarity:
python3 event_matcher.py events.jsonl --embeddings --output matches.csvCreate a labeled CSV with known matches and non-matches:
source_a,id_a,source_b,id_b,is_match
kalshi,KXPRESTRUMP-24,polymarket,0xtrump2024,1
kalshi,KXPRESTRUMP-24,polymarket,0xsenate2024,0Run the evaluator:
python3 evaluate.py examples/labels.csv examples/events.jsonl --errors errors.csvExample report:
pairs: 6
positive_labels: 2
negative_labels: 4
auto_match precision=1.000 recall=1.000 f1=1.000 tp=2 fp=0 tn=4 fn=0
match_or_review precision=1.000 recall=1.000 f1=1.000 tp=2 fp=0 tn=4 fn=0
auto_match measures only confident matches. match_or_review measures how many true matches would be caught by either automatic matching or the human review queue.
Train a lightweight logistic-regression model from labeled pairs:
python3 train_matcher.py examples/labels.csv examples/events.jsonl --output matcher_model.jsonInclude embeddings as a trainable feature after installing requirements-embeddings.txt:
python3 train_matcher.py examples/labels.csv examples/events.jsonl --embeddings --output matcher_model.jsonUse that model for scoring:
python3 event_matcher.py examples/events.jsonl --model matcher_model.json --output matches.csvThe loader accepts JSON, JSONL/NDJSON, and CSV. Field names are flexible:
- source:
source,platform,venue,exchange - ID:
event_id,id,ticker,slug,market_id,condition_id - title:
title,name,question,headline,event_title - text:
description,subtitle,details,rules,resolution_criteria - tags:
tags,tag,categories,category,topics,topic - dates:
date,event_date,end_date,close_time,expiration_time,resolution_date
Example JSONL:
{"source":"kalshi","event_id":"KXPRESTRUMP-24","title":"Will Donald Trump win the 2024 US presidential election?","tags":["Politics","Elections"],"end_date":"2024-11-05"}
{"source":"polymarket","event_id":"0xabc","question":"Donald Trump wins 2024 U.S. Presidential Election?","tags":["Politics"],"resolution_date":"2024-11-05"}CSV/JSON output includes:
label:match,review, orno_matchwhen--all-pairsis usedscore: normalized 0-1 similarity scorecluster_id: connected component ID for confident matches- source/ID/title for both records
explanation: top feature scores and shared signals
The highest-leverage improvement is more labeled data. Keep adding hard positives and hard negatives to the evaluation file, especially same-topic non-matches such as candidate vs party, month-specific CPI markets, different thresholds, different leagues, and different offices. Then use evaluate.py after every scoring change and train_matcher.py once the label set is large enough to support a learned model.