Currency attribution (Karnosky-Singer)#
A fund holding UK stocks in GBP but reporting in USD has two sources of return on top of local stock movement: the stock return itself and the GBP/USD exchange rate. Performance attribution for such a fund needs a fourth effect on top of the Brinson three — currency.
pybrinson ships the Karnosky-Singer (1994) additive
decomposition under
pybrinson.currency_attribution. Four effects per segment;
identity closes to \(10^{-9}\); same input style as BHB / BF with
two extra fields.
flowchart LR
Rp["Base-ccy return<br/>R_p,i"]
Rp --> Local["Local return<br/>R^L_p,i"]
Rp --> Ccy["Currency contrib.<br/>c_i"]
Local --> Sel["Selection S_i<br/>wb·(R^L_p - R^L_b)"]
Local --> Mkt["Market alloc. M_i<br/>(wp-wb)·(R^L_b - R^L_b,tot)"]
Ccy --> CAlloc["Currency alloc. C_i<br/>(wp-wb)·(c - c_b,tot)"]
Local --> Inter["Interaction I_i<br/>(wp-wb)·(R^L_p - R^L_b)"]
The four effects#
For each segment \(i\), with local portfolio return \(R^L_{p,i}\) and currency contribution \(c_i\) (shared by portfolio and benchmark — both hold segment \(i\) in the same local currency):
where benchmark local / currency totals are
Summed across segments the four terms reconstruct the base-currency arithmetic excess \(R_p - R_b\).
Convention pinned: additive (continuously compounded)#
Ankrim-Hensel (1994) and other sources use a multiplicative form
\((1 + R^L)(1 + c) - 1\) with an explicit forward-premium leg.
pybrinson pins additive and documents this inline in
src/pybrinson/single_period/currency.py.
- If your data is continuously compounded (log returns), additive is exact.
- If your data is arithmetic and the cross-term
\(R^L \cdot c\) is material, you can either (a) absorb it into
local_returnat the segment level before calling pybrinson, or (b) feed hedged base-currency returns withcurrency_returnset to the residual unhedged contribution.
pybrinson enforces the additive consistency at the segment level:
if \(|R_{p,i} - (R^L_{p,i} + c_i)| > 10^{-9}\) it raises
AttributionError so you catch the convention mismatch at the
input boundary.
API#
Segment gained two optional fields in v1.3:
Segment(
name="Japan Equity",
portfolio_weight=0.30,
benchmark_weight=0.20,
portfolio_return=-0.05,
benchmark_return=-0.04,
local_return=-0.02, # NEW: portfolio's local-currency segment return
currency_return=-0.03, # NEW: currency contribution (shared with benchmark)
)
The benchmark local return \(R^L_{b,i}\) is derived as \(R_{b,i} - c_i\) under the additive convention. No need to supply it explicitly.
from pybrinson import Segment, currency_attribution
segments = [
Segment("US Equity", 0.50, 0.40, 0.08, 0.06,
local_return=0.08, currency_return= 0.00),
Segment("UK Equity", 0.30, 0.35, 0.05, 0.04,
local_return=0.07, currency_return=-0.02),
Segment("Japan Equity", 0.20, 0.25, 0.02, 0.03,
local_return=0.05, currency_return=-0.03),
]
result = currency_attribution(segments, period="2024-Q1")
# result.market_allocation, result.security_selection,
# result.currency_allocation, result.interaction
# result.by_segment -> per-segment 4-effect rows
What pybrinson does not do (yet)#
- No forward-premium leg. The Karnosky-Singer framework's full
form separates currency return into spot and forward-premium
components; pybrinson v1.3 treats the currency return as a
single number. If you need the full hedged / forward-premium
model, feed hedged base-currency returns and set
currency_returnto the residual unhedged contribution. - No multiplicative (Ankrim-Hensel) variant. If the community asks, it can be added additively as a separate entry point; no standing plan.
Backward compatibility#
Segment.local_return and Segment.currency_return default to
None. BHB and Brinson-Fachler ignore them. Existing call sites
are byte-identical.
Worked example#
The karnosky_singer_3segment fixture exposes the same US / UK /
Japan numbers used in the code snippet above:
from pybrinson import currency_attribution
from pybrinson.fixtures import karnosky_singer_3segment
segments, expected = karnosky_singer_3segment()
result = currency_attribution(segments)
assert abs(result.excess_return - expected["excess_return"]) < 1e-12
The tutorial at docs/tutorials/currency.py walks through the
same example formula by formula.
Sources#
- Karnosky, D. S., & Singer, B. D. (1994). Global Asset Management and Performance Attribution. CFA Institute Research Foundation. Free PDF.
- Ankrim, E. M., & Hensel, C. R. (1994). "Multicurrency Performance Attribution." Financial Analysts Journal, 50(2). DOI 10.2469/faj.v50.n2.29.
- Bacon, C. R. (2008). Practical Portfolio Performance Measurement and Attribution, 2nd ed., chap. 7.