Bloomberg-style pair-trade comparison tool for the 2026 UBS Finance Challenge. Built as a quantitative complement to a separate AI research module — the two tools are intentionally decoupled.
Pair Trade
- LONG Siemens Energy (
ENR.DE) — focus on the Grid Technologies segment - SHORT Sieyuan Electric (
002028.SZ) — Chinese private-sector grid equipment maker
Direction is committed; the tool itself is data-only. The narrative emerges from how spreads are framed (premium / discount to peer median, trajectory slopes, valuation-reset sensitivity), never from tool-generated opinions.
A single self-contained HTML file with three Bloomberg-style panels:
| Panel | Content |
|---|---|
| Left | Sieyuan + China T&D peer basket (NARI, XJ, Pinggao, TBEA, China XD) |
| Middle | Siemens Energy + Global Power peer basket (GE Vernova, Schneider, Hitachi, Eaton, MHI) — with a separate Grid Technologies segment row populated from manually-transcribed IR data |
| Right | Cross-basket spread analysis: direct pair, basket-vs-basket median spread, relative positioning (RICH / NEUTRAL / CHEAP), valuation-reset sensitivity, policy exposure overlay, AI-drafted commentary |
Plus a structured cross_market_caveats block surfacing A-share vs European reporting / accounting / fiscal-year-end / liquidity asymmetries — never papered over.
Live data, fetched 2026-04-28:
| Metric | Sieyuan (002028.SZ) | Siemens Energy (ENR.DE) |
|---|---|---|
| Price | CNY 195.25 | EUR 172.98 |
| Mcap (USD bn) | 22.3 | 173.4 |
| YTD % | +29.2% | +40.9% |
| Forward P/E | 30.3x | 30.3x |
| vs peer median | +59% (RICH) | −17% (NEUTRAL) |
| EBIT margin | 18.0% | 4.0% |
| EBIT margin slope (3Y) | +1.3 pp/yr | +2.95 pp/yr |
Cross-basket spread highlights:
- Sieyuan sits at the 100th percentile of its China T&D basket on EV/Sales, EV/EBITDA, revenue growth, EBIT margin, and ROE — labelled
RICHon every metric. - Siemens Energy sits broadly NEUTRAL within Global Power despite a materially lower margin base; consolidated forward P/E is below basket median (Global Power median is being pulled up by GE Vernova post-spinoff multiples).
- Valuation-reset sensitivity: if Sieyuan's forward P/E reverts to its China T&D basket median, implied move is −37%. If Siemens Energy's reverts to its Global Power median, implied move is +20%. The spread is the trade.
- Trajectory asymmetry: Siemens Energy's EBIT margin is expanding at +2.95pp/yr — roughly 2.2× the Global Power basket median slope. Sieyuan is at +1.3pp/yr against a flatter China T&D basket.
These observations are produced by the tool from yfinance + manually curated segment data — no qualitative judgement is applied. The asymmetry is structural in the data.
ubs-valuation-tool/
├── src/
│ ├── data_sources/
│ │ ├── yfinance_adapter.py # Stage 1a: live fundamentals
│ │ ├── siemens_energy_segments.py # Stage 1c: segment YAML loader
│ │ └── models.py # Provenance Pydantic models
│ ├── comparables/
│ │ └── comp_builder.py # Stage 3: dual-basket comp tables
│ ├── spread/
│ │ └── cross_basket.py # Stage 4: spread + reset sensitivity
│ ├── summary/
│ │ └── commentary.py # Stage 5: Anthropic-drafted commentary
│ ├── render/
│ │ └── bloomberg_view.py # Stage 6: 3-panel HTML
│ └── cli.py # Orchestrator
├── data/
│ ├── universes/
│ │ ├── china_td.yaml # Sieyuan + 5 peer basket
│ │ └── global_power.yaml # Siemens Energy + 5 peer basket
│ ├── raw/siemens_energy/ # Manual segment YAMLs (FY2024, FY2025-Q1/Q2)
│ └── policy/exposure_2026.yaml # CBAM / FSR / Section 232 / IRA / Stock Connect
├── outputs/
│ ├── comp_tables/ # Per-basket JSON (gitignored)
│ ├── spread_analysis/ # Cross-basket JSON
│ ├── valuation_drafts/ # Commentary drafts
│ ├── views/ # Rendered HTML
│ └── lineage/ # Audit-trail JSONs (planned)
├── scripts/
│ ├── smoke_test.py # Verify data sources
│ └── rerender.py # Re-render HTML from existing JSONs
├── tests/
└── CLAUDE.md # Project context + hard rules
git clone https://github.com/theodorklink/UBS_Challenge_2.git
cd UBS_Challenge_2
python3 -m venv .venv
source .venv/bin/activate # Windows: .venv\Scripts\activate
pip install -r requirements.txt
cp .env.example .env
# Edit .env: paste ANTHROPIC_API_KEY (for the Commentary drafter, Stage 5)
# FMP_API_KEY is optional (cross-validation, free tier)python -m src.cli runPipeline (5 stages, ~90s end-to-end):
- Build China T&D comp table — Sieyuan + 5 peers via yfinance
- Build Global Power comp table — Siemens Energy + 5 peers + Grid Tech segment row
- Cross-basket spread analysis — direct pair, basket-vs-basket, relative positioning, reset sensitivity, policy overlay
- Commentary draft — Anthropic Claude Sonnet 4.6, with strict safety perimeter
- Render Bloomberg HTML — single self-contained file
Output: outputs/views/sieyuan_vs_siemens_<timestamp>.html (open in any browser).
python -m src.cli run --no-commentarypython scripts/rerender.pycd outputs/views && python -m http.server 8001
# Open http://localhost:8001/The pipeline runs locally (yfinance + Anthropic + ~90s), and the rendered HTML is published as a static page on Vercel. The CLI automatically writes the latest run to public/index.html; pushing to GitHub triggers a Vercel auto-deploy.
- Sign up at https://vercel.com → "Continue with GitHub".
- "Add New" → "Project" → import
theodorklink/UBS_Challenge_2. - Framework Preset: leave on "Other" (Vercel auto-detects static via
vercel.json). - Root Directory:
./(default). - Build Settings: leave at defaults —
vercel.jsonoverrides them. - Environment Variables: NONE NEEDED. The deploy is pure static; no API keys at runtime.
- Deploy.
You'll get a URL like https://ubs-challenge-2.vercel.app.
# Generate fresh data + HTML locally
python -m src.cli run
# This writes BOTH:
# outputs/views/sieyuan_vs_siemens_<timestamp>.html (timestamped archive)
# public/index.html (overwritten with latest)
# Commit and push — Vercel auto-deploys in ~30s
git add public/index.html
git commit -m "Refresh Bloomberg view ($(date +%Y-%m-%d))"
git pushThat's the whole workflow. No serverless complexity, no API blocking, no 60s timeouts.
- yfinance is blocked from most cloud IPs (Yahoo blocks AWS/Vercel ranges)
- Pipeline takes ~90s — Vercel Hobby tier function timeout is 60s
- Anthropic call alone is ~45s — too tight for live serverless
- Snapshot is meant to be reproducible — frozen at run time is a feature, not a bug, for a competition submission
These are non-negotiable safety constraints, defined in CLAUDE.md:
| Rule | Where enforced |
|---|---|
| No DCF generated by the LLM | System prompt + postcheck |
| No fabricated tickers | yfinance verify_ticker() |
Every numeric output carries source + retrieved_at + source_url |
Pydantic Provenance mixin |
Commentary drafter cannot output long, short, buy, sell, overweight, underweight, outperform, underperform, target price, fair value, we recommend, trade idea — case-insensitive postcheck blocks save |
src/summary/commentary.py:_FORBIDDEN_RE |
Cross-market comparisons display the structured cross_market_caveats object |
bloomberg_view._render_cross_market_warning() |
| Siemens Energy comparisons to Sieyuan default to Grid Technologies segment | comp_builder._build_grid_tech_segment_row() |
| Missing data fails loudly — never imputed | renderer shows "—" / "data pending" |
The tool surfaces these as standard pair-trade analytics — never as opinions:
- Premium / discount to peer median as the pair-trade-relevant valuation view
- 3Y trajectory slope on margins (linear regression, annual obs; semi-annual for Sieyuan)
- Valuation-reset sensitivity (revert each target to its basket median forward P/E)
- Order quality composite (book-to-bill × backlog/revenue, where disclosed)
- Policy exposure overlay with citations (CBAM, FSR, Section 232, IRA, Stock Connect)
If the data points the other way on any single metric, the tool shows it. The output is honest.
| Source | Used for | Cost |
|---|---|---|
| yfinance | All 12 basket members (incl. A-shares via .SS / .SZ) |
Free |
| akshare | Optional A-share fallback (planned) | Free |
| Anthropic Claude Sonnet 4.6 | Commentary drafter | ~$0.20-0.30 per run |
| Manual YAML | Siemens Energy Grid Tech segment data with page citations from IR PDFs | Manual entry |
| Financial Modeling Prep (optional) | Cross-validation of yfinance values | Free tier 250/day |
- Sieyuan reports semi-annually (CSRC permits) — trajectory slopes computed on 6 semi-annual observations rather than 12 quarterly. Auto-flagged in
CompanyInfo.reporting_frequency. - A-share forward estimates are sparse on yfinance — Western analyst coverage is limited. Forward P/E shows "n/a — limited Western analyst coverage" for affected peers.
- EV multiples are not segment-allocable — the Grid Tech segment row shows operating metrics (margin, growth, book-to-bill) only; EV-based multiples are blank with the explicit caveat.
- Mitsubishi Heavy is a stretch peer — power business is one of three segments; flagged in basket validator output.
MIT — see LICENSE file (or use freely for academic / competition purposes).
Built for the 2026 UBS Finance Challenge submission. Sister project: ubs-ai-module (narrative research tool, Tavily + Claude, Vercel-deployed).