Skip to content

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):

\[ \begin{aligned} M_i &= (w_{p,i} - w_{b,i})(R^L_{b,i} - R^L_{b,\text{tot}}) & \text{market allocation (local BF form)} \\ S_i &= w_{b,i}(R^L_{p,i} - R^L_{b,i}) & \text{security selection} \\ C_i &= (w_{p,i} - w_{b,i})(c_i - c_{b,\text{tot}}) & \text{currency allocation (BF form on } c\text{)} \\ I_i &= (w_{p,i} - w_{b,i})(R^L_{p,i} - R^L_{b,i}) & \text{interaction} \end{aligned} \]

where benchmark local / currency totals are

\[R^L_{b,\text{tot}} = \sum_i w_{b,i} R^L_{b,i}, \qquad c_{b,\text{tot}} = \sum_i w_{b,i} c_i\]

Summed across segments the four terms reconstruct the base-currency arithmetic excess \(R_p - R_b\).

Convention pinned: additive (continuously compounded)#

\[R_{p,i} = R^L_{p,i} + c_i\]

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_return at the segment level before calling pybrinson, or (b) feed hedged base-currency returns with currency_return set 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_return to 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.