This repo contains two cross-sectional G10 FX studies that share one
from-scratch research stack (data → signal → dollar-neutral portfolio → variance
targeting → costs → date-clustered inference → diagnostics), built src/basis/:
- Cross-currency basis model — this README, below. The 3M OIS-based basis, a non-circular cross-sectional fair value, and the L/S residual strategy. See also investigations/beta_sign.md (why the cross-sectional slope is positive, opposite Du-Tepper-Verdelhan).
- FX factor model (carry / value / momentum) — reuses the basis package's
portfolio/risk/cost/inference apparatus unchanged on a new signal layer
(
src/basis/premium/). Writeup: reports/fx_factors.md.
Both are honest risk-premium-harvesting studies, not alpha/arbitrage. Reproduce
either via pip install -e . then python -m basis.pipeline /
python -m basis.premium.pipeline (needs LSEG Workspace for the first data pull;
see below). pytest runs 33 tests.
Constructs the 3M cross-currency basis for G10 currencies from first principles (covered interest parity, OIS legs), specifies a cross-sectional fair value, trades the residual as a dollar-neutral long/short portfolio, and reports defensible inference with an honest account of the strategy's negative skew.
Key caveat (read first). This harvests a known funding-liquidity risk premium (Du–Tepper–Verdelhan). It is not alpha discovery. The deliverable is correct construction, a non-circular fair value, defensible inference, and an honest account of the skew — not a high-Sharpe trading signal. After realistic costs the strategy is a modest-Sharpe (~1.2), heavily negatively-skewed, high-leverage carry harvest.
Clean-6: EUR, JPY, GBP, CHF, AUD, CAD vs USD, daily. Sample 2009-05-06 → present (OIS-limited; this covers the 2011-12 euro crisis and March-2020, but not the Sept-2008 Lehman peak — GBP/CHF/AUD/CAD/EUR OIS series start mid-2009).
All data is pulled from LSEG Refinitiv Workspace via its desktop API proxy.
The official lseg.data / eikon Python libraries hang on this machine, so we use
a thin direct-HTTP UDF client (src/basis/data/lseg_client.py).
Forward points and OIS are not available free with this history — they require the
terminal. RICs (field TR.MIDPRICE, daily):
| Series | RICs |
|---|---|
| Spot | EUR= JPY= GBP= CHF= AUD= CAD= |
| 3M FX forward points | EUR3M= JPY3M= GBP3M= CHF3M= AUD3M= CAD3M= |
| 3M OIS | USD3MOIS=, EUREON3M=, JPY3MOIS=, GBP3MOIS=, CHF3MOIS=, AUD3MOIS=, CAD3MOIS= |
| Exogenous vol state (Step 7) | .MOVE (TR.CLOSEPRICE) |
Everything in USD per 1 unit of foreign currency. The 3M basis x_i is the
wedge added to the foreign OIS leg that equates synthetic USD funding via the FX
swap with direct USD OIS (DTV convention):
(1 + r_US·τ) = (F/S)·(1 + (r_i + x_i)·τ)
x_i = (1/τ)·[ (S/F)·(1 + r_US·τ) − 1 ] − r_i
x_i < 0 ⇒ synthetic dollars are expensive (the dominant post-GFC G10 case).
The forward F is the market-observed outright (spot + market forward points);
it is never backed out of OIS — doing so would make the basis identically zero.
Required check (test): EUR & JPY bases are persistently negative and widen in
2011-12 and March-2020. ✔ (see tests/test_construction.py).
- Construction — CIP from first principles. EUR mean basis −30bp (98.6% of days negative), JPY −42bp; both spike in 2011-12 and to −150/−247bp in Mar-2020.
- Cross-sectional fair value — at each date
b_{i,t} = α_t + β·(r_i−r_US) + ε_{i,t}.α_t(date FE) absorbs the common dollar factor;εis the signal. This is not the circular own-rate time-series regression. β is POSITIVE (Fama-MacBeth +0.15, t=17.7; pooled date-FE +0.11, t=85.6): across the developed G6 the basis is a near-monotone increasing function of the rate differential (between-currency corr +0.99), high-yield AUD at a ~0 basis, low-yield JPY/CHF/EUR most negative. Low-yield currencies do carry the most negative bases (a level fact), but that is the ordering, not the cause of the slope — the slope survives dropping the safe havens and holds in every sub-period including DTV's own 2010-16 window (see investigations/beta_sign.md). This is the opposite sign to DTV's "higher-yield → more negative" phrasing, which corresponds to signing the differential(r_US−r_i); the residual signal is invariant to this choice. - Portfolio — weekly, dollar-neutral, weight ∝ −ε. P&L decomposed into carry (Sharpe ~10, smooth, ~74% of return) and convergence (Sharpe 0.62, bears the tail). Honest answer to "is it just carry?": largely yes.
- Variance targeting (Barroso–Santa-Clara, on the strategy's own trailing vol, no regime timing) — at equal vol, Sharpe 2.26 → 2.65, kurtosis 45 → 23. Hitting 10% vol needs ~310x average leverage (basis arb is a near-arbitrage).
- Costs — bid-ask on FX-swap + OIS legs, wider for AUD/CAD, widened in high-vol periods (named parameters in src/basis/costs.py). Costs eat ~45% of gross; Sharpe 2.29 → 1.26 net.
- Inference — net long-short premium Newey-West t = 6.7 (gross 11.6), SEs robust to the 3M overlap; ~6 currencies/cross-section, N_eff ≈ 815. Inference rests on limited cross-sectional dispersion and a carry-dominated premium — do not over-read the t-stat. Purged+embargoed CV splitter provided.
- Skew diagnostic (descriptive, not a timer) — net premium bucketed by lagged MOVE terciles: skew +0.5 (calm) → −3.4 (stress), mean return halving in stress. Worst drawdowns: Jan-2015 (CHF de-peg) and March-2020.
- EUR OIS =
EUREON3M=(EONIA basis; the only EUR OIS reaching 2009). It sits a constant ~8.5bp above €STR OIS — a constant level offset on the EUR basis, no dynamic distortion. - Day-count τ = 91/360 (EUR/CHF/USD) or 91/365 (JPY/GBP/AUD/CAD). Exact settlement-date accrual is a parameterized second-order refinement.
- Basis MTM duration for the convergence P&L ≈ tenor (0.25y).
- β positive in our
(r_i−r_US)convention — reported as-is, not forced negative.
python -m venv .venv && source .venv/bin/activate
pip install -e . # installs the `basis` package + deps
# Set the LSEG app key (Eikon Data API type, from Workspace App Key Generator):
# create lseg-data.config.json (see template) or export LSEG_APP_KEY=...
pytest # 27 tests incl. the basis sign sanity checks
python -m basis.pipeline # pulls+caches data, prints all numbers, writes figures/
jupyter notebook notebooks/results.ipynb # end-to-end notebookData is pulled once and cached under data/. Requires LSEG Workspace running and
logged in (desktop proxy on localhost:9000). All figures land in figures/.
src/basis/
data/lseg_client.py direct UDF client construction.py Step 1 (CIP, sign conv.)
data/loader.py RICs, fetch, cache regression.py Step 2 (FM + panel FE)
portfolio.py Step 3 (L/S, P&L decomp) risk.py Step 4 (variance targeting)
costs.py Step 5 (bid-ask) inference.py Step 6 (NW t, purged CV)
diagnostics.py Step 7 (skew) pipeline.py end-to-end orchestrator
tests/ 27 tests notebooks/results.ipynb