Skip to content

Commit cba5426

Browse files
Initial scaffold: seed inventory, kepler build script, docs output
19 seed records across three tiers (execution/network/research) in data/sites_seed.csv; build_map.py exports a standalone kepler.gl map to docs/index.html. - Basemap: CARTO Dark Matter (token-free); the Mapbox demo token keplergl bundles is stripped from the export (GitHub push protection rejects it, and we don't need Mapbox). - Fullscreen fix: kepler's Jupyter wrapper only measures its container on window resize, so the export pins the container to the viewport and fires a resize on load. - Pin setuptools<81 (keplergl 0.3.x imports pkg_resources). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
0 parents  commit cba5426

8 files changed

Lines changed: 847 additions & 0 deletions

File tree

.gitignore

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
# Python
2+
.venv/
3+
venv/
4+
__pycache__/
5+
*.py[cod]
6+
.ipynb_checkpoints/
7+
8+
# Raw source pulls (can be large; cache locally, don't commit)
9+
data/raw/
10+
11+
# Build outputs at repo root (the ~11MB kepler export); the published copy
12+
# lives in docs/ and IS tracked
13+
/*.html
14+
!docs/*.html
15+
16+
# macOS
17+
.DS_Store
18+
19+
# Local tooling
20+
.claude/

CLAUDE.md

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
# CLAUDE.md — Quant Infrastructure Map
2+
3+
## What this project is
4+
5+
Research + interactive map for a published essay: **"Mapping the Physical
6+
Footprint of Quantitative Finance"** — where hedge funds and quant/HFT firms
7+
actually put their machines. Author: Daniel Roy. Output: a kepler.gl (or
8+
MapLibre) interactive map embedded in a blog post, backed by a rigorously
9+
sourced CSV inventory.
10+
11+
The thesis: the latency arms race consumed microseconds; the ML arms race
12+
consumes megawatts — and megawatts leave a public paper trail.
13+
14+
## Core structure: three tiers
15+
16+
1. **execution** — exchange colocation sites (NYSE Mahwah, Nasdaq Carteret,
17+
Equinix NY4/5 Secaucus, CME/CyrusOne Aurora, LD4 Slough, Equinix FR2
18+
Frankfurt, Aruba IT3 Bergamo, HKEX TKO, JPX Tokyo...).
19+
2. **network** — microwave/millimeter/shortwave relay infrastructure
20+
(Chicago–NJ corridor, Channel crossing masts at Richborough/Houtem,
21+
London–Frankfurt paths). Operators: McKay Brothers, New Line Networks
22+
(Jump/Virtu), Vigilant Global (DRW), Anova.
23+
3. **research** — ML compute campuses. Anchor case: XTX Kajaani (€1bn+,
24+
22.5MW first building, ~250MW reported at full build). Contrast: Citadel
25+
Securities on Google Cloud, Renaissance's undisclosed on-prem setup.
26+
27+
## Non-negotiable rules
28+
29+
1. **Every pin must carry a public source.** Nothing gets mapped that is not
30+
already in the public record (spectrum license, planning filing, corporate
31+
announcement, credible press). This is both the intellectual standard and
32+
the legal safety margin of the piece.
33+
2. **Confidence grading is mandatory**: `confirmed` (primary source) /
34+
`reported` (credible press) / `inferred` (triangulated). Never upgrade a
35+
record's confidence without a new source URL in `evidence_url`.
36+
3. **Coordinates**: all seed coordinates are `approximate` (from memory) and
37+
MUST be verified by geocoding against a documented address before any
38+
publication build. Track status in `coord_precision`
39+
(exact/approximate/city_level/symbolic).
40+
4. Do not state or imply non-public information about any firm's
41+
infrastructure. When evidence is absent, absence is the finding (e.g.
42+
Renaissance Technologies: campus known, compute inferred — say so).
43+
44+
## Data schema
45+
46+
`data/sites_seed.csv` columns: site_id, site_name, tier, operator_or_venue,
47+
firms_linked, lat, lon, coord_precision, city, country, capacity_note,
48+
evidence_type, evidence_note, confidence, verify_url_hint (to be replaced by
49+
`evidence_url` with real URLs).
50+
51+
Planned second file: `data/paths.csv` for Tier-2 arc pairs
52+
(origin_site_id, dest_site_id, operator, medium, evidence_url) — feeds a
53+
kepler arc layer.
54+
55+
## Evidence pipeline (strength order)
56+
57+
1. Spectrum licenses: FCC ULS (Part 101 + experimental), Ofcom fixed-links
58+
register, ANFR Cartoradio (FR), BIPT (BE). Licensee shells trace back to
59+
firms via Companies House / state registries.
60+
2. Planning applications (councils/municipalities).
61+
3. Corporate + contractor disclosure (e.g. YIT/Granlund pages on XTX Kajaani).
62+
4. Job postings / LinkedIn location signals.
63+
5. Network forensics: PeeringDB, ASN registrations, exchange connectivity docs.
64+
6. Industry databases: DataCenterMap, DCD archives.
65+
66+
Key references: Alexandre Laumonier (*Sniper in Mahwah*), Donald MacKenzie
67+
(*Trading at the Speed of Light*), Shkilko & Sokolov (microwave outages paper),
68+
Zuckerman (*The Man Who Solved the Market*).
69+
70+
## Tech conventions
71+
72+
- Python 3.11+, pandas; keep dependencies minimal (see requirements.txt).
73+
- `build_map.py` is the single build entry point: CSV → standalone kepler HTML.
74+
- The kepler HTML export is ~11MB (bundled library). If embed weight becomes a
75+
problem for the blog, build a lean MapLibre page reading the same CSV — the
76+
dataset is the asset, the renderer is swappable.
77+
- kepler tooltip URLs may not be clickable depending on version — test before
78+
publication; MapLibre popup fallback if needed.
79+
- Every scraper/extractor goes in `pipeline/` with a docstring stating source,
80+
terms-of-use considerations, and rate limiting. Cache raw pulls in
81+
`data/raw/` (gitignored if large).
82+
83+
## Working style
84+
85+
- Small, reviewable commits; one data source or feature per branch.
86+
- When adding records, prefer fewer well-sourced pins over many weak ones.
87+
- Update PLAN.md checkboxes as milestones complete.

PLAN.md

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
# PLAN.md — phased milestones
2+
3+
## Phase 1 — Foundation hygiene
4+
- [ ] Geocode-verify all seed coordinates against documented addresses; set
5+
coord_precision=exact where verified
6+
- [ ] Replace verify_url_hint with real evidence_url for all 19 seed records
7+
- [ ] Add pytest data-validation suite (schema, coordinate bounds, confidence
8+
enum, URL presence for confirmed records) — run in CI
9+
10+
## Phase 2 — Tier 2 extraction (the towers)
11+
- [ ] pipeline/fcc_uls.py: pull FCC ULS Part 101 licenses for known
12+
licensee shells on the Chicago–NJ corridor; output tower coords +
13+
paths.csv arc pairs
14+
- [ ] FCC experimental license pull (shortwave sites)
15+
- [ ] Ofcom fixed-links register extraction (London–Slough–coast paths)
16+
- [ ] ANFR Cartoradio: French relay sites on the London–Frankfurt corridor
17+
- [ ] Add kepler arc layer for paths.csv
18+
19+
## Phase 3 — Tier 3 expansion (the compute story)
20+
- [ ] XTX: pin Kajaani precisely (Sokajärventie site), add second building,
21+
Iceland (Verne Global) with sources
22+
- [ ] Job-ad triangulation pass: Jane Street, HRT, Two Sigma, DE Shaw, Jump —
23+
infrastructure roles with locations
24+
- [ ] Renaissance: Brookhaven planning/permit search; document the null result
25+
if null
26+
- [ ] Power-capacity column normalization (MW where public)
27+
28+
## Phase 4 — Publication build
29+
- [ ] Decide renderer: kepler standalone vs MapLibre page (embed weight, link
30+
behavior in tooltips)
31+
- [ ] Static fallback image (matplotlib/plotly) for social cards + RSS
32+
- [ ] Final source audit: every pin → working evidence_url
33+
- [ ] Essay draft sections keyed to map tiers

README.md

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
# Mapping the Physical Footprint of Quantitative Finance
2+
3+
Working repo for the essay + interactive map. Three-tier inventory of publicly
4+
documented quant/HFT infrastructure, with an evidence grade on every record.
5+
6+
## Structure
7+
8+
- `data/sites_seed.csv` — the inventory (19 seed records; target ~80-120)
9+
- `build_map.py` — CSV → standalone kepler.gl HTML
10+
(`python build_map.py --out docs/index.html`)
11+
- `docs/index.html` — output (tracked; servable via GitHub Pages); embeddable
12+
in Ghost via iframe or direct upload
13+
14+
## Schema
15+
16+
| column | meaning |
17+
|---|---|
18+
| `tier` | `execution` (exchange colo) / `network` (microwave, shortwave) / `research` (ML compute) |
19+
| `coord_precision` | `exact` (verified address/geocode) / `approximate` (needs verification) / `city_level` / `symbolic` (no single site exists, e.g. cloud usage) |
20+
| `confidence` | `confirmed` (primary source: license, filing, corporate announcement) / `reported` (credible press) / `inferred` (triangulated from job ads, LinkedIn, network data) |
21+
| `evidence_type` | `spectrum_license` / `planning_docs` / `exchange_docs` / `corporate_announcement` / `operator_docs` / `press` |
22+
| `verify_url_hint` | where to find the primary source (replace with real URLs before publication) |
23+
24+
**Publication rule: every pin must carry a public primary or credible secondary
25+
source. Nothing gets mapped that isn't already in the public record.**
26+
27+
## Evidence pipeline (in order of strength)
28+
29+
1. **Spectrum licenses** — FCC ULS (Part 101 + experimental), Ofcom fixed-links
30+
register, ANFR Cartoradio (FR), BIPT (BE). Exact tower coordinates + licensee
31+
shells traceable via Companies House / state registries.
32+
2. **Planning applications** — councils/municipalities for towers and DC builds.
33+
3. **Corporate + contractor disclosure** — press releases; contractor project pages.
34+
4. **Job postings / LinkedIn** — location signals for unannounced presence.
35+
5. **Network forensics** — PeeringDB, ASN registrations, exchange connectivity docs.
36+
6. **Industry databases** — DataCenterMap, DCD archives, colo operator case studies.
37+
38+
## Key references
39+
40+
- Alexandre Laumonier, *Sniper in Mahwah* (blog + books "6"/"5") — the Channel
41+
towers, shortwave series (with Bob Van Valzah)
42+
- Donald MacKenzie, *Trading at the Speed of Light* (2021)
43+
- Shkilko & Sokolov, "Every Cloud Has a Silver Lining" (microwave outages as
44+
natural experiment)
45+
- Exchange colocation service documents (NYSE, Nasdaq, CME, DB, HKEX, JPX)
46+
47+
## TODO
48+
49+
- [ ] Verify/refine all `approximate` coordinates (geocode against addresses)
50+
- [ ] Replace `verify_url_hint` with real evidence URLs
51+
- [ ] FCC ULS pull: script the Part 101 licensee extraction for known shells
52+
- [ ] Add arc layer: tower-pair paths for Tier 2 (kepler arc layer, needs
53+
origin/destination pairs in a second CSV)
54+
- [ ] Ofcom fixed-links + ANFR Cartoradio extraction for the London-Frankfurt corridor
55+
- [ ] Expand Tier 3: Jane Street, HRT, Two Sigma, DE Shaw compute footprints
56+
(job-ad triangulation)
57+
- [ ] Test kepler tooltip URL behavior in target blog; fall back to MapLibre
58+
popup page if links aren't clickable

build_map.py

Lines changed: 182 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,182 @@
1+
"""
2+
Quant/HFT infrastructure map — build script.
3+
4+
Reads data/sites_seed.csv, enriches it (Google Maps deep links, tier labels),
5+
and exports a standalone kepler.gl HTML map suitable for embedding in a blog post.
6+
7+
Usage:
8+
python build_map.py [--csv data/sites_seed.csv] [--out quant_dc_map.html]
9+
"""
10+
11+
from __future__ import annotations
12+
13+
import argparse
14+
import re
15+
from pathlib import Path
16+
17+
import pandas as pd
18+
from keplergl import KeplerGl
19+
20+
# keplergl's bundled JS ships kepler.gl's demo Mapbox token, which GitHub
21+
# push protection rejects. We don't need Mapbox at all: use CARTO's free
22+
# Dark Matter basemap (no access token) and strip the bundled token from
23+
# the exported HTML.
24+
CARTO_DARK = {
25+
"id": "carto_dark",
26+
"label": "CARTO Dark Matter (no token)",
27+
"url": "https://basemaps.cartocdn.com/gl/dark-matter-gl-style/style.json",
28+
"icon": "",
29+
"custom": True,
30+
"accessToken": None,
31+
}
32+
MAPBOX_TOKEN_RE = re.compile(r"pk\.eyJ[0-9A-Za-z_-]+\.[0-9A-Za-z_-]+")
33+
34+
# kepler's Jupyter wrapper only measures its container on window resize
35+
# events (initial render falls back to 800x400), so pin the container to the
36+
# viewport and fire a resize once the page loads.
37+
FULLSCREEN_SNIPPET = """<style>
38+
html, body { width: 100%; height: 100%; }
39+
.keplergl-widget-container { width: 100vw !important; height: 100vh !important; }
40+
</style>
41+
<script>
42+
window.addEventListener('load', function () {
43+
setTimeout(function () { window.dispatchEvent(new Event('resize')); }, 100);
44+
});
45+
</script>
46+
"""
47+
48+
TIER_LABELS = {
49+
"execution": "Tier 1 — Execution (exchange colo)",
50+
"network": "Tier 2 — Network (microwave/shortwave)",
51+
"research": "Tier 3 — Research (ML compute)",
52+
}
53+
54+
# Hex colors per tier, referenced in the kepler config below.
55+
TIER_COLORS = {
56+
"execution": [230, 80, 60], # red — the latency layer
57+
"network": [250, 190, 60], # amber — the towers
58+
"research": [70, 130, 240], # blue — the compute layer
59+
}
60+
61+
62+
def load_sites(csv_path: Path) -> pd.DataFrame:
63+
df = pd.read_csv(csv_path)
64+
65+
required = {"site_id", "site_name", "tier", "lat", "lon", "confidence"}
66+
missing = required - set(df.columns)
67+
if missing:
68+
raise ValueError(f"CSV missing required columns: {missing}")
69+
70+
# Google Maps deep link per pin — the "zoom in for detail" handoff.
71+
df["gmaps_url"] = (
72+
"https://www.google.com/maps/search/?api=1&query="
73+
+ df["lat"].round(6).astype(str)
74+
+ ","
75+
+ df["lon"].round(6).astype(str)
76+
)
77+
df["tier_label"] = df["tier"].map(TIER_LABELS).fillna(df["tier"])
78+
79+
# Fail loudly on bad coordinates rather than silently dropping pins.
80+
bad = df[(df["lat"].abs() > 90) | (df["lon"].abs() > 180) | df["lat"].isna()]
81+
if not bad.empty:
82+
raise ValueError(f"Bad coordinates for: {bad['site_id'].tolist()}")
83+
84+
return df
85+
86+
87+
def kepler_config(df: pd.DataFrame) -> dict:
88+
"""Minimal kepler config: one point layer per tier, tooltip with evidence fields."""
89+
center_lat = float(df["lat"].mean())
90+
center_lon = float(df["lon"].mean())
91+
92+
layers = []
93+
for tier, color in TIER_COLORS.items():
94+
layers.append(
95+
{
96+
"id": f"layer_{tier}",
97+
"type": "point",
98+
"config": {
99+
"dataId": tier,
100+
"label": TIER_LABELS[tier],
101+
"columns": {"lat": "lat", "lng": "lon"},
102+
"isVisible": True,
103+
"color": color,
104+
"visConfig": {
105+
"radius": 14,
106+
"opacity": 0.85,
107+
"outline": True,
108+
"thickness": 2,
109+
},
110+
},
111+
}
112+
)
113+
114+
tooltip_fields = [
115+
{"name": f}
116+
for f in [
117+
"site_name",
118+
"operator_or_venue",
119+
"firms_linked",
120+
"capacity_note",
121+
"confidence",
122+
"evidence_note",
123+
"gmaps_url",
124+
]
125+
]
126+
127+
return {
128+
"version": "v1",
129+
"config": {
130+
"visState": {
131+
"layers": layers,
132+
"interactionConfig": {
133+
"tooltip": {
134+
"fieldsToShow": {t: tooltip_fields for t in TIER_COLORS},
135+
"enabled": True,
136+
}
137+
},
138+
},
139+
"mapState": {
140+
"latitude": center_lat,
141+
"longitude": center_lon,
142+
"zoom": 2.4,
143+
},
144+
"mapStyle": {
145+
"styleType": CARTO_DARK["id"],
146+
"mapStyles": {CARTO_DARK["id"]: CARTO_DARK},
147+
},
148+
},
149+
}
150+
151+
152+
def build(csv_path: Path, out_path: Path) -> None:
153+
df = load_sites(csv_path)
154+
155+
m = KeplerGl(config=kepler_config(df))
156+
# One dataset per tier so each gets its own layer + legend entry + toggle.
157+
for tier in TIER_COLORS:
158+
subset = df[df["tier"] == tier].reset_index(drop=True)
159+
if not subset.empty:
160+
m.add_data(data=subset, name=tier)
161+
162+
m.save_to_html(file_name=str(out_path), read_only=False)
163+
164+
html = out_path.read_text(encoding="utf-8")
165+
html, n_stripped = MAPBOX_TOKEN_RE.subn("", html)
166+
# rsplit: "</body>" also appears inside the bundled JS; only the last
167+
# occurrence is the real closing tag.
168+
head, _, tail = html.rpartition("</body>")
169+
html = head + FULLSCREEN_SNIPPET + "</body>" + tail
170+
out_path.write_text(html, encoding="utf-8")
171+
172+
print(f"Wrote {out_path}{len(df)} sites "
173+
f"({df['tier'].value_counts().to_dict()}); "
174+
f"stripped {n_stripped} bundled Mapbox token(s)")
175+
176+
177+
if __name__ == "__main__":
178+
parser = argparse.ArgumentParser()
179+
parser.add_argument("--csv", default="data/sites_seed.csv", type=Path)
180+
parser.add_argument("--out", default="quant_dc_map.html", type=Path)
181+
args = parser.parse_args()
182+
build(args.csv, args.out)

0 commit comments

Comments
 (0)