Forex Analysis (wraquant.forex)

Foreign exchange analysis tools: currency pair analytics, trading session detection, carry trade analysis, and FX-specific risk measures.

Quick Example

from wraquant.forex import pairs, session, carry

# Identify the active trading session
current_session = session.current_session()
print(f"Active session: {current_session}")

# Carry trade analysis: interest rate differential
carry_result = carry.carry_trade_return(
    spot_rate=1.10,
    domestic_rate=0.05,
    foreign_rate=0.03,
    holding_period=90,
)
print(f"Carry return: {carry_result['return']:.4f}")

# Currency pair correlation analysis
corr = pairs.cross_correlation(fx_returns_df)
print(corr)

See also

API Reference

Forex-specific analysis and tools.

Provides a complete toolkit for foreign exchange analysis, covering currency pair abstractions, pip and lot-size calculations, trading session analytics, carry trade modeling, and forex-specific risk management. Designed for both discretionary FX traders and systematic currency strategies.

Key sub-modules:

  • Pairs (pairs) – CurrencyPair dataclass for structured pair handling, cross_rate computation, major_pairs convenience list, correlation_matrix across pairs, currency_strength scoring, and volatility_by_session for session-level vol profiles.

  • Analysis (analysis) – Core FX calculations: pips (price to pip conversion), pip_value, pip_distance, lot_size for position sizing, spread_cost, position_value, risk_reward_ratio, and margin_call_price.

  • Sessions (session) – ForexSession enum (Tokyo, London, New York), current_session detection, and session_overlaps for identifying high-liquidity windows.

  • Carry (carry) – Carry trade analytics: carry_return, carry_attractiveness scoring, carry_portfolio construction, interest_rate_differential, forward_premium, and uncovered_interest_parity testing.

  • Risk (risk) – fx_portfolio_risk for multi-currency portfolio risk aggregation.

Example

>>> from wraquant.forex import CurrencyPair, pip_value, carry_return
>>> pair = CurrencyPair("EUR", "USD")
>>> pv = pip_value("EURUSD", lot_size=100_000)
>>> cr = carry_return(spot=1.10, forward=1.0985, days=90)

Use wraquant.forex for FX-specific analytics. For general portfolio risk that includes currency exposure, combine with wraquant.risk. For macroeconomic data (interest rates, GDP) that feeds carry models, use wraquant.data.fetch_macro.

class CurrencyPair[source]

Bases: object

A forex currency pair.

Parameters:
  • base (Currency) – Base currency (e.g., EUR in EURUSD).

  • quote (Currency) – Quote currency (e.g., USD in EURUSD).

Example

>>> pair = CurrencyPair(Currency.EUR, Currency.USD)
>>> pair.symbol
'EURUSD'
base: Currency
quote: Currency
property symbol: str

Standard pair symbol (e.g., ‘EURUSD’).

property yahoo_symbol: str

Yahoo Finance ticker format.

property is_jpy_pair: bool

Whether this pair involves JPY (different pip size).

property pip_size: float

Size of one pip for this pair.

inverse()[source]

Return the inverse pair (e.g., EURUSD -> USDEUR).

Return type:

CurrencyPair

classmethod from_string(s)[source]

Parse a pair from string like ‘EURUSD’ or ‘EUR/USD’.

Parameters:

s (str) – Pair string (6 chars or with separator).

Return type:

CurrencyPair

Returns:

CurrencyPair instance.

__init__(base, quote)
Parameters:
Return type:

None

cross_rate(pair1_rate, pair2_rate, method='divide')[source]

Calculate a cross rate from two pairs sharing a common currency.

Use cross rates to derive the exchange rate for a currency pair that is not directly quoted. For example, EUR/JPY can be derived from EUR/USD and USD/JPY.

The method depends on how the pairs share a common currency:

  • 'multiply': when pair1 = A/B and pair2 = B/C, result = A/C.

  • 'divide': when pair1 = A/B and pair2 = C/B, result = A/C.

Parameters:
  • pair1_rate (float) – Rate for first pair.

  • pair2_rate (float) – Rate for second pair.

  • method (str, default: 'divide') – 'divide' (pair1/pair2) or 'multiply' (pair1 * pair2).

Return type:

float

Returns:

Cross rate.

Example

>>> cross_rate(1.1000, 110.00, method="multiply")  # EURJPY from EURUSD * USDJPY
121.0
>>> cross_rate(1.1000, 1.3000, method="divide")  # EURGBP from EURUSD / GBPUSD
0.8461538461538461

See also

CurrencyPair: Currency pair representation.

major_pairs()[source]

Return the 7 major forex pairs.

Return type:

list[CurrencyPair]

Returns:

List of major currency pairs.

correlation_matrix(pairs_df, window=60)[source]

Rolling correlation matrix between currency pairs.

Use this to identify which currency pairs move together and which diverge. High positive correlation means two pairs track each other closely (little diversification benefit); negative correlation offers hedging opportunities.

Computes pairwise Pearson correlations of returns over a rolling window. Returns the most recent window’s correlation matrix.

Parameters:
  • pairs_df (DataFrame) – DataFrame where each column is the price series of a currency pair (e.g., columns ['EURUSD', 'GBPUSD', 'USDJPY']). Index should be datetime.

  • window (int, default: 60) – Rolling window size in periods (default 60, roughly 3 months of daily data). Shorter windows capture recent regime shifts; longer windows are more stable.

Return type:

DataFrame

Returns:

Correlation matrix as a DataFrame (pairs x pairs). Values range from -1.0 (perfect negative correlation) to +1.0 (perfect positive correlation).

Example

>>> import pandas as pd
>>> import numpy as np
>>> rng = np.random.default_rng(42)
>>> prices = pd.DataFrame({
...     'EURUSD': np.cumsum(rng.normal(0, 0.001, 100)) + 1.10,
...     'GBPUSD': np.cumsum(rng.normal(0, 0.001, 100)) + 1.30,
... })
>>> corr = correlation_matrix(prices, window=30)
>>> corr.shape
(2, 2)

See also

currency_strength: Relative strength of individual currencies.

currency_strength(pairs_df, window=None)[source]

Compute relative strength of each currency from cross rates.

Use this to identify which currencies are strengthening and which are weakening across the board. A currency that is appreciating against most counterparts will have a high strength score.

The algorithm extracts individual currency codes from pair column names (e.g., 'EURUSD' yields EUR and USD), computes returns, and averages each currency’s performance across all pairs it appears in (positive for appreciation, negative for depreciation).

Parameters:
  • pairs_df (DataFrame) – DataFrame where each column is named as a 6-character pair (e.g., 'EURUSD', 'USDJPY'). Values are prices.

  • window (int | None, default: None) – Number of recent periods to use for strength calculation. If None, uses the full history.

Return type:

Series

Returns:

Series indexed by currency code with mean return as the strength score. Positive values indicate the currency is strengthening on average; negative values indicate weakening.

Example

>>> import pandas as pd
>>> import numpy as np
>>> rng = np.random.default_rng(42)
>>> prices = pd.DataFrame({
...     'EURUSD': np.cumsum(rng.normal(0.0001, 0.001, 100)) + 1.10,
...     'USDJPY': np.cumsum(rng.normal(0.0001, 0.001, 100)) + 110.0,
... })
>>> strength = currency_strength(prices)
>>> 'EUR' in strength.index
True

See also

correlation_matrix: Pairwise correlation between pairs.

volatility_by_session(prices, sessions=None)[source]

Compute price volatility during each forex trading session.

Use this to identify which session carries the most volatility for a given currency pair. Typically London and the London/New York overlap have the highest volatility for major pairs.

The function groups intraday returns by session (based on UTC hour) and computes annualised volatility for each.

Parameters:
  • prices (DataFrame | Series) – Intraday price series or DataFrame with a DatetimeIndex. For a DataFrame, uses the first column. Must have sub-daily frequency (e.g., 1H, 15min).

  • sessions (dict[str, tuple[int, int]] | None, default: None) – Dictionary mapping session name to (start_hour, end_hour) in UTC. Hours are inclusive of start, exclusive of end. Defaults to the four major sessions: Sydney (21-6), Tokyo (0-9), London (7-16), New York (12-21).

Return type:

dict[str, float]

Returns:

Dictionary mapping session name to annualised volatility (assuming 252 trading days). Higher values indicate more volatile sessions.

Example

>>> import pandas as pd
>>> import numpy as np
>>> idx = pd.date_range('2024-01-01', periods=240, freq='1h')
>>> prices = pd.Series(np.cumsum(np.random.default_rng(42).normal(0, 0.001, 240)) + 1.10, index=idx)
>>> vol = volatility_by_session(prices)
>>> 'London' in vol
True

Notes

For pairs involving Asian currencies, Tokyo session volatility is often the highest. For EUR and GBP pairs, London dominates.

See also

wraquant.forex.session.ForexSession: Session definitions. wraquant.forex.session.current_session: Active session detection.

pips(price_change, pair=None, is_jpy=False)[source]

Convert price change to pips.

Use this to express price movements in the standard forex unit (pips) for consistent comparison across pairs. One pip is 0.0001 for most pairs and 0.01 for JPY pairs.

Parameters:
  • price_change (float | Series) – Price difference (e.g., 1.1050 - 1.1000 = 0.0050).

  • pair (CurrencyPair | None, default: None) – CurrencyPair (auto-detects JPY pairs).

  • is_jpy (bool, default: False) – Whether pair involves JPY (if pair not provided).

Return type:

float | Series

Returns:

Number of pips (can be negative for downward moves).

Example

>>> pips(0.0050)  # 50 pips for non-JPY pair
50.0
>>> pips(0.50, is_jpy=True)  # 50 pips for JPY pair
50.0

See also

pip_value: Dollar value of one pip.

pip_value(pair=None, lot_size_units=100000, is_jpy=False, exchange_rate=1.0)[source]

Calculate the value of one pip in account currency.

Use pip value to determine the dollar (or account currency) impact of a one-pip move for a given position size. This is fundamental to position sizing and risk management in forex.

Formula: pip_value = (pip_size * lot_size_units) / exchange_rate

Parameters:
  • pair (CurrencyPair | None, default: None) – CurrencyPair (auto-detects JPY pip size).

  • lot_size_units (float, default: 100000) – Position size in units (standard lot = 100,000, mini = 10,000, micro = 1,000).

  • is_jpy (bool, default: False) – Whether pair involves JPY (if pair not provided).

  • exchange_rate (float, default: 1.0) – Rate to convert to account currency. Set to 1.0 if account currency matches the quote currency.

Return type:

float

Returns:

Value of one pip in account currency.

Example

>>> pip_value(lot_size_units=100_000)  # Standard lot, non-JPY
10.0
>>> pip_value(lot_size_units=10_000)  # Mini lot
1.0
>>> pip_value(lot_size_units=100_000, is_jpy=True)  # JPY pair
1000.0

See also

lot_size: Calculate position size from risk parameters. pips: Convert price change to pip count.

pip_distance(entry, exit, pair=None, is_jpy=False)[source]

Calculate the pip distance between two prices.

Use this to measure the signed distance in pips between an entry and exit price. Automatically detects JPY pairs (2-decimal pip size) versus standard pairs (4-decimal pip size).

Formula: pip_distance = (exit - entry) / pip_size

Parameters:
  • entry (float) – Entry (open) price.

  • exit (float) – Exit (close) price.

  • pair (CurrencyPair | str | None, default: None) – CurrencyPair instance or string like 'USDJPY' for automatic JPY detection. If None, uses is_jpy flag.

  • is_jpy (bool, default: False) – Whether the pair involves JPY (only used when pair is not provided).

Return type:

float

Returns:

Signed pip distance. Positive means the price moved up from entry to exit (profit for a long position).

Example

>>> pip_distance(1.1000, 1.1050)  # 50 pips up on EUR/USD
50.0
>>> pip_distance(110.00, 110.50, is_jpy=True)  # 50 pips on USD/JPY
50.0
>>> pip_distance(1.1050, 1.1000)  # 50 pips down
-50.0

See also

pips: Convert a raw price change to pip count. risk_reward_ratio: Use pip distances for R:R analysis.

lot_size(account_balance, risk_percent, stop_loss_pips, pair=None, is_jpy=False, exchange_rate=1.0)[source]

Calculate position size in lots based on risk management.

Use lot size calculation to determine how large a position to take given your account size, risk tolerance, and stop-loss distance. This ensures that if the stop loss is hit, the loss is exactly the specified percentage of your account.

Formula: lots = (account * risk%) / (stop_pips * pip_value_per_lot)

Parameters:
  • account_balance (float) – Account balance in account currency.

  • risk_percent (float) – Risk per trade as percentage (e.g., 1.0 = 1%). Professional traders typically risk 0.5-2% per trade.

  • stop_loss_pips (float) – Stop loss distance in pips. Wider stops require smaller positions to maintain the same risk.

  • pair (CurrencyPair | None, default: None) – CurrencyPair.

  • is_jpy (bool, default: False) – Whether pair involves JPY.

  • exchange_rate (float, default: 1.0) – Rate to convert to account currency.

Return type:

float

Returns:

Position size in standard lots (1 lot = 100,000 units).

Example

>>> lot_size(10_000, risk_percent=1.0, stop_loss_pips=50)
0.2
>>> lot_size(50_000, risk_percent=2.0, stop_loss_pips=100)
1.0

See also

pip_value: Value of one pip for a given position size. pips: Convert price change to pip count.

spread_cost(spread_pips, lot_size_units=100000, pair=None, is_jpy=False)[source]

Calculate the cost of the spread for a position.

The spread cost is an implicit transaction cost paid every time you enter or exit a position. Use this to assess whether the spread makes a strategy unviable at a given position size.

Parameters:
  • spread_pips (float) – Bid-ask spread in pips (e.g., 1.5 pips for EUR/USD).

  • lot_size_units (float, default: 100000) – Position size in units (default 100,000 = 1 lot).

  • pair (CurrencyPair | None, default: None) – CurrencyPair.

  • is_jpy (bool, default: False) – Whether pair involves JPY.

Return type:

float

Returns:

Spread cost in quote currency.

Example

>>> spread_cost(1.5, lot_size_units=100_000)  # 1.5 pip spread, 1 lot
15.0

See also

pip_value: Value of one pip.

position_value(lots, pip_val, pips_moved)[source]

Calculate position P&L in account currency.

Use this to compute the profit or loss of a forex position given the number of lots, the pip value per lot, and the number of pips the price has moved.

Formula: P&L = lots * pip_value * pips

Parameters:
  • lots (float) – Number of standard lots (1 lot = 100,000 units). Fractional lots are supported (e.g., 0.1 for a mini lot).

  • pip_val (float) – Value of one pip per lot in account currency. Use pip_value() to compute this.

  • pips_moved (float) – Number of pips the position has moved. Positive for favourable moves (long profits / short losses), negative for adverse moves.

Return type:

float

Returns:

Profit or loss in account currency. Positive means profit.

Example

>>> position_value(lots=1.0, pip_val=10.0, pips_moved=50)
500.0
>>> position_value(lots=0.5, pip_val=10.0, pips_moved=-30)
-150.0

See also

pip_value: Calculate pip value per lot. lot_size: Calculate position size from risk parameters.

risk_reward_ratio(entry, stop, target, pair=None, is_jpy=False)[source]

Calculate the risk-reward ratio for a trade.

Use this before entering a trade to evaluate whether the potential reward justifies the risk. A ratio above 2.0 is generally considered favourable; below 1.0 means you risk more than you stand to gain.

The function works for both long and short trades by comparing absolute distances from entry to stop and entry to target.

Parameters:
  • entry (float) – Entry price.

  • stop (float) – Stop-loss price.

  • target (float) – Take-profit price.

  • pair (CurrencyPair | str | None, default: None) – CurrencyPair or string (e.g., 'USDJPY') for automatic JPY detection.

  • is_jpy (bool, default: False) – Whether the pair involves JPY.

Returns:

  • ratio (float) – Reward-to-risk ratio (target distance / stop distance). Values above 2.0 are generally favourable.

  • risk_pips (float) – Distance from entry to stop in pips (always positive).

  • reward_pips (float) – Distance from entry to target in pips (always positive).

Return type:

dict[str, float]

Example

>>> result = risk_reward_ratio(1.1000, 1.0950, 1.1100)
>>> result['ratio']
2.0
>>> result['risk_pips']
50.0
>>> result['reward_pips']
100.0

See also

pip_distance: Raw pip distance between two prices. lot_size: Size position based on risk.

margin_call_price(entry, balance, margin_used, leverage, side='long')[source]

Calculate the price at which a margin call occurs.

Use this to determine the maximum adverse move before a margin call is triggered. A margin call occurs when the account equity falls to (or below) the margin required to maintain the position.

For a long position, the margin call price is below entry; for a short position, it is above entry.

Formula (long):

margin_call_price = entry - (balance - margin_used) / (leverage * margin_used / entry)

Simplified:

margin_call_price = entry * (1 - (balance - margin_used) / (leverage * margin_used))

Parameters:
  • entry (float) – Entry price of the position.

  • balance (float) – Account balance in account currency.

  • margin_used (float) – Margin (collateral) used for this position in account currency.

  • leverage (float) – Leverage ratio (e.g., 50.0 for 50:1 leverage).

  • side (str, default: 'long') – 'long' or 'short'.

Return type:

float

Returns:

Price at which a margin call is triggered. For longs this is below entry; for shorts it is above entry. If the margin call price is negative (for longs), returns 0.0 since prices cannot go negative.

Example

>>> # $10,000 balance, $2,000 margin, 50:1 leverage, long at 1.1000
>>> mc = margin_call_price(1.1000, 10_000, 2_000, 50.0)
>>> mc < 1.1000  # margin call below entry for long
True

Notes

This is a simplified model. Real margin calls depend on the broker’s margin call level (e.g., 100% or 50% of margin), floating P&L on other positions, and swap costs.

See also

lot_size: Size positions to control risk.

class ForexSession[source]

Bases: StrEnum

Major forex trading sessions.

SYDNEY = 'sydney'
TOKYO = 'tokyo'
LONDON = 'london'
NEW_YORK = 'new_york'
__new__(value)
current_session(dt=None)[source]

Determine which forex sessions are active.

Parameters:

dt (datetime | None, default: None) – Datetime in UTC. Defaults to now.

Return type:

list[ForexSession]

Returns:

List of active ForexSession values.

Example

>>> current_session(datetime(2024, 1, 15, 14, 0, tzinfo=timezone.utc))
[<ForexSession.LONDON: 'london'>, <ForexSession.NEW_YORK: 'new_york'>]
session_overlaps()[source]

Return the major session overlap periods.

Returns:

(session1, session2, start_utc, end_utc).

Return type:

list[tuple[ForexSession, ForexSession, time, time]]

carry_return(spot_change, base_rate, quote_rate, periods_per_year=252)[source]

Calculate total carry trade return (spot + carry).

Use this to evaluate the full P&L of a carry trade, which earns the interest rate differential daily but is exposed to spot rate movements. A positive carry (base rate > quote rate) means you earn interest by holding the position, but adverse spot moves can overwhelm the carry.

Total return = spot return + (base_rate - quote_rate) / periods_per_year

Parameters:
  • spot_change (Series) – Daily spot rate returns (log or simple).

  • base_rate (float) – Annual interest rate of base currency (e.g., 0.04 = 4%).

  • quote_rate (float) – Annual interest rate of quote currency.

  • periods_per_year (int, default: 252) – Periods per year (default 252 for daily data).

Return type:

Series

Returns:

Total return series (spot return + daily carry). Positive values indicate profit for a long carry position.

Example

>>> import pandas as pd
>>> spot_returns = pd.Series([0.001, -0.002, 0.0005, 0.001])
>>> total = carry_return(spot_returns, base_rate=0.05, quote_rate=0.01)
>>> total.iloc[0] > spot_returns.iloc[0]  # carry adds return
True

See also

interest_rate_differential: Raw rate differential. carry_attractiveness: Rank pairs by carry.

carry_attractiveness(rates, pairs=None)[source]

Rank currency pairs by carry attractiveness.

Use this to screen the forex universe for the best carry trade opportunities. Pairs with the highest interest rate differential offer the most carry income, but also tend to have higher crash risk (carry trade unwinds).

Parameters:
  • rates (dict[str, float]) – Dict mapping currency code to annual interest rate (e.g., {'USD': 0.05, 'JPY': 0.001, 'AUD': 0.04}).

  • pairs (list[tuple[str, str]] | None, default: None) – Optional list of (base, quote) pairs to evaluate. If None, evaluates all combinations.

Return type:

DataFrame

Returns:

DataFrame with columns pair, base_rate, quote_rate, differential, sorted by differential descending. Top rows are the most attractive carry trades.

Example

>>> rates = {'USD': 0.05, 'JPY': 0.001, 'EUR': 0.04}
>>> df = carry_attractiveness(rates)
>>> df.iloc[0]['pair']  # highest carry
'USDJPY'

See also

carry_return: Full carry trade P&L including spot moves. forward_premium: Covered interest rate parity forward rate.

carry_portfolio(rates_dict, weights=None, n_long=3, n_short=3)[source]

Construct a carry trade portfolio: long high-yield, short low-yield.

Use this to build a systematic carry strategy. The portfolio goes long the n_long highest-yielding currencies and short the n_short lowest-yielding currencies. If custom weights are provided, they override the automatic equal-weight allocation.

This is the standard G10 carry trade approach used by institutional investors. It earns the interest rate differential but is exposed to crash risk (carry unwinds).

Parameters:
  • rates_dict (dict[str, float]) – Dictionary mapping currency codes to their annual interest rates (e.g., {'USD': 0.05, 'JPY': 0.001, 'AUD': 0.04, 'EUR': 0.03, 'CHF': 0.015, 'NZD': 0.045}).

  • weights (dict[str, float] | None, default: None) – Optional custom weight for each currency. If None, equal-weights the long and short legs separately.

  • n_long (int, default: 3) – Number of currencies in the long leg (default 3).

  • n_short (int, default: 3) – Number of currencies in the short leg (default 3).

Returns:

  • weights (dict) – Portfolio weights per currency. Positive for long positions, negative for short positions. Weights sum to approximately zero (dollar-neutral).

  • expected_carry (float) – Expected annualised carry return (weighted sum of rates for longs minus shorts).

  • long_currencies (list) – Currencies in the long leg.

  • short_currencies (list) – Currencies in the short leg.

Return type:

dict[str, object]

Example

>>> rates = {'USD': 0.05, 'JPY': 0.001, 'AUD': 0.04,
...          'EUR': 0.03, 'CHF': 0.015, 'NZD': 0.045}
>>> result = carry_portfolio(rates, n_long=2, n_short=2)
>>> result['expected_carry'] > 0
True
>>> len(result['long_currencies'])
2

See also

carry_attractiveness: Rank all pairs by carry differential. carry_return: Full P&L including spot moves.

interest_rate_differential(base_rate, quote_rate)[source]

Calculate interest rate differential between two currencies.

The interest rate differential is the foundation of carry trades. A positive differential means the base currency has a higher yield, so a long position earns positive carry.

Parameters:
  • base_rate (float) – Annual interest rate of base currency (e.g., 0.05 = 5%).

  • quote_rate (float) – Annual interest rate of quote currency.

Return type:

float

Returns:

Interest rate differential (base - quote). Positive means long carry, negative means short carry.

Example

>>> interest_rate_differential(0.05, 0.01)  # AUD vs JPY
0.04
>>> interest_rate_differential(0.01, 0.05)  # JPY vs AUD (negative carry)
-0.04

See also

carry_return: Full carry trade return including spot moves. carry_attractiveness: Rank pairs by carry.

forward_premium(spot, base_rate, quote_rate, days=365)[source]

Calculate the forward premium/discount.

Use this to compute the theoretical forward exchange rate based on covered interest rate parity. If the forward rate exceeds the spot rate, the base currency trades at a forward discount (its interest rate is higher than the quote currency’s).

Formula: F = S * (1 + r_quote * days/365) / (1 + r_base * days/365)

Parameters:
  • spot (float) – Current spot rate.

  • base_rate (float) – Base currency annual rate.

  • quote_rate (float) – Quote currency annual rate.

  • days (int, default: 365) – Forward period in days (default 365 for 1-year forward).

Return type:

float

Returns:

Forward rate. Compare to spot to determine premium (F > S) or discount (F < S).

Example

>>> forward_premium(1.1000, base_rate=0.04, quote_rate=0.02, days=365)
1.0788461538461539

See also

carry_return: Full carry trade P&L.

uncovered_interest_parity(domestic_rate, foreign_rate, spot, maturity=1.0)[source]

Uncovered Interest Rate Parity (UIP) expected future spot rate.

Use this to compute the expected future exchange rate implied by the interest rate differential under UIP. UIP states that the expected depreciation of a currency equals the interest rate differential.

While Covered Interest Parity (CIP) holds by arbitrage, UIP is an equilibrium condition that often fails empirically (the “forward premium puzzle”), which is why carry trades can be profitable.

Formula:

E[S_T] = S * (1 + r_domestic * T) / (1 + r_foreign * T)

This is the same formula as Covered Interest Parity but interpreted as the expected future spot rate rather than the no-arbitrage forward rate.

Parameters:
  • domestic_rate (float) – Annual interest rate of the domestic (base) currency.

  • foreign_rate (float) – Annual interest rate of the foreign (quote) currency.

  • spot (float) – Current spot exchange rate (domestic/foreign).

  • maturity (float, default: 1.0) – Horizon in years (default 1.0).

Returns:

  • forward_rate (float) – UIP-implied expected future spot rate. If domestic rate > foreign rate, the domestic currency is expected to depreciate (forward_rate > spot).

  • forward_premium (float) – Forward premium as a percentage ((forward - spot) / spot). Positive means the domestic currency trades at a forward premium (expected to depreciate).

Return type:

dict[str, float]

Example

>>> result = uncovered_interest_parity(0.05, 0.01, 1.1000, maturity=1.0)
>>> result['forward_rate'] > 1.1000  # domestic rate higher -> depreciation expected
True
>>> abs(result['forward_premium'] - 0.0396) < 0.01
True

Notes

Reference: Fama (1984). “Forward and Spot Exchange Rates.” Journal of Monetary Economics, 14, 319-338.

See also

forward_premium: CIP-based forward rate calculation. carry_return: Carry trade P&L (profits when UIP fails).

fx_portfolio_risk(positions, exchange_rates, base_currency='USD', returns=None, fx_returns=None)[source]

Compute FX-adjusted portfolio risk.

Accounts for currency exposure in portfolio risk calculation. Without FX adjustment, a portfolio denominated in multiple currencies has hidden risk from exchange rate movements. This function bridges forex and risk by computing:

  1. Base-currency positions: converts all positions to the base currency using current exchange rates.

  2. Currency exposure: the net exposure to each currency as a fraction of total portfolio value.

  3. FX-adjusted volatility: if asset and FX return data are provided, computes the portfolio volatility including currency risk.

When to use:

Use this for any multi-currency portfolio to understand how much of your total risk comes from FX movements vs asset returns. Essential for international equity, fixed income, and carry trade portfolios.

Parameters:
  • positions (dict[str, float]) – Dictionary mapping asset names to position values in their local currency (e.g., {'AAPL': 100_000, 'Toyota': 5_000_000}).

  • exchange_rates (dict[str, float]) – Dictionary mapping currency codes to their value in base currency (e.g., {'USD': 1.0, 'JPY': 0.0067, 'EUR': 1.10}). Each asset’s currency should be present. If an asset name contains a known currency code, it is auto-detected.

  • base_currency (str, default: 'USD') – The base (reporting) currency (default 'USD').

  • returns (DataFrame | None, default: None) – Optional DataFrame of asset returns (columns = asset names). If provided along with fx_returns, enables full FX-adjusted volatility calculation.

  • fx_returns (DataFrame | None, default: None) – Optional DataFrame of FX returns (columns = currency codes). Required for volatility decomposition.

Returns:

  • 'total_value_base' (float) – Total portfolio value in base currency.

  • 'positions_base' (dict) – Each position converted to base currency.

  • 'currency_exposure' (dict) – Net exposure to each currency as a fraction of total value.

  • 'fx_adjusted_vol' (float or None) – Annualised portfolio volatility including FX risk (only if returns and fx_returns are provided).

  • 'asset_vol' (float or None) – Annualised portfolio volatility from asset returns only.

  • 'fx_vol_contribution' (float or None) – Additional volatility from FX movements.

Return type:

dict[str, float | dict]

Example

>>> positions = {'US_stock': 100_000, 'EU_stock': 80_000}
>>> rates = {'USD': 1.0, 'EUR': 1.10}
>>> # Map assets to currencies
>>> result = fx_portfolio_risk(
...     positions, rates, base_currency='USD',
... )
>>> result['total_value_base'] > 0
True

See also

wraquant.risk.portfolio.portfolio_volatility: Asset-only vol. wraquant.forex.carry.carry_return: Carry trade returns.

Pairs

Currency pair definitions and cross rate calculations.

class CurrencyPair[source]

Bases: object

A forex currency pair.

Parameters:
  • base (Currency) – Base currency (e.g., EUR in EURUSD).

  • quote (Currency) – Quote currency (e.g., USD in EURUSD).

Example

>>> pair = CurrencyPair(Currency.EUR, Currency.USD)
>>> pair.symbol
'EURUSD'
base: Currency
quote: Currency
property symbol: str

Standard pair symbol (e.g., ‘EURUSD’).

property yahoo_symbol: str

Yahoo Finance ticker format.

property is_jpy_pair: bool

Whether this pair involves JPY (different pip size).

property pip_size: float

Size of one pip for this pair.

inverse()[source]

Return the inverse pair (e.g., EURUSD -> USDEUR).

Return type:

CurrencyPair

classmethod from_string(s)[source]

Parse a pair from string like ‘EURUSD’ or ‘EUR/USD’.

Parameters:

s (str) – Pair string (6 chars or with separator).

Return type:

CurrencyPair

Returns:

CurrencyPair instance.

__init__(base, quote)
Parameters:
Return type:

None

major_pairs()[source]

Return the 7 major forex pairs.

Return type:

list[CurrencyPair]

Returns:

List of major currency pairs.

cross_rate(pair1_rate, pair2_rate, method='divide')[source]

Calculate a cross rate from two pairs sharing a common currency.

Use cross rates to derive the exchange rate for a currency pair that is not directly quoted. For example, EUR/JPY can be derived from EUR/USD and USD/JPY.

The method depends on how the pairs share a common currency:

  • 'multiply': when pair1 = A/B and pair2 = B/C, result = A/C.

  • 'divide': when pair1 = A/B and pair2 = C/B, result = A/C.

Parameters:
  • pair1_rate (float) – Rate for first pair.

  • pair2_rate (float) – Rate for second pair.

  • method (str, default: 'divide') – 'divide' (pair1/pair2) or 'multiply' (pair1 * pair2).

Return type:

float

Returns:

Cross rate.

Example

>>> cross_rate(1.1000, 110.00, method="multiply")  # EURJPY from EURUSD * USDJPY
121.0
>>> cross_rate(1.1000, 1.3000, method="divide")  # EURGBP from EURUSD / GBPUSD
0.8461538461538461

See also

CurrencyPair: Currency pair representation.

correlation_matrix(pairs_df, window=60)[source]

Rolling correlation matrix between currency pairs.

Use this to identify which currency pairs move together and which diverge. High positive correlation means two pairs track each other closely (little diversification benefit); negative correlation offers hedging opportunities.

Computes pairwise Pearson correlations of returns over a rolling window. Returns the most recent window’s correlation matrix.

Parameters:
  • pairs_df (DataFrame) – DataFrame where each column is the price series of a currency pair (e.g., columns ['EURUSD', 'GBPUSD', 'USDJPY']). Index should be datetime.

  • window (int, default: 60) – Rolling window size in periods (default 60, roughly 3 months of daily data). Shorter windows capture recent regime shifts; longer windows are more stable.

Return type:

DataFrame

Returns:

Correlation matrix as a DataFrame (pairs x pairs). Values range from -1.0 (perfect negative correlation) to +1.0 (perfect positive correlation).

Example

>>> import pandas as pd
>>> import numpy as np
>>> rng = np.random.default_rng(42)
>>> prices = pd.DataFrame({
...     'EURUSD': np.cumsum(rng.normal(0, 0.001, 100)) + 1.10,
...     'GBPUSD': np.cumsum(rng.normal(0, 0.001, 100)) + 1.30,
... })
>>> corr = correlation_matrix(prices, window=30)
>>> corr.shape
(2, 2)

See also

currency_strength: Relative strength of individual currencies.

currency_strength(pairs_df, window=None)[source]

Compute relative strength of each currency from cross rates.

Use this to identify which currencies are strengthening and which are weakening across the board. A currency that is appreciating against most counterparts will have a high strength score.

The algorithm extracts individual currency codes from pair column names (e.g., 'EURUSD' yields EUR and USD), computes returns, and averages each currency’s performance across all pairs it appears in (positive for appreciation, negative for depreciation).

Parameters:
  • pairs_df (DataFrame) – DataFrame where each column is named as a 6-character pair (e.g., 'EURUSD', 'USDJPY'). Values are prices.

  • window (int | None, default: None) – Number of recent periods to use for strength calculation. If None, uses the full history.

Return type:

Series

Returns:

Series indexed by currency code with mean return as the strength score. Positive values indicate the currency is strengthening on average; negative values indicate weakening.

Example

>>> import pandas as pd
>>> import numpy as np
>>> rng = np.random.default_rng(42)
>>> prices = pd.DataFrame({
...     'EURUSD': np.cumsum(rng.normal(0.0001, 0.001, 100)) + 1.10,
...     'USDJPY': np.cumsum(rng.normal(0.0001, 0.001, 100)) + 110.0,
... })
>>> strength = currency_strength(prices)
>>> 'EUR' in strength.index
True

See also

correlation_matrix: Pairwise correlation between pairs.

volatility_by_session(prices, sessions=None)[source]

Compute price volatility during each forex trading session.

Use this to identify which session carries the most volatility for a given currency pair. Typically London and the London/New York overlap have the highest volatility for major pairs.

The function groups intraday returns by session (based on UTC hour) and computes annualised volatility for each.

Parameters:
  • prices (DataFrame | Series) – Intraday price series or DataFrame with a DatetimeIndex. For a DataFrame, uses the first column. Must have sub-daily frequency (e.g., 1H, 15min).

  • sessions (dict[str, tuple[int, int]] | None, default: None) – Dictionary mapping session name to (start_hour, end_hour) in UTC. Hours are inclusive of start, exclusive of end. Defaults to the four major sessions: Sydney (21-6), Tokyo (0-9), London (7-16), New York (12-21).

Return type:

dict[str, float]

Returns:

Dictionary mapping session name to annualised volatility (assuming 252 trading days). Higher values indicate more volatile sessions.

Example

>>> import pandas as pd
>>> import numpy as np
>>> idx = pd.date_range('2024-01-01', periods=240, freq='1h')
>>> prices = pd.Series(np.cumsum(np.random.default_rng(42).normal(0, 0.001, 240)) + 1.10, index=idx)
>>> vol = volatility_by_session(prices)
>>> 'London' in vol
True

Notes

For pairs involving Asian currencies, Tokyo session volatility is often the highest. For EUR and GBP pairs, London dominates.

See also

wraquant.forex.session.ForexSession: Session definitions. wraquant.forex.session.current_session: Active session detection.

Sessions

Forex trading session utilities.

Defines the four major forex sessions and their overlaps.

class ForexSession[source]

Bases: StrEnum

Major forex trading sessions.

SYDNEY = 'sydney'
TOKYO = 'tokyo'
LONDON = 'london'
NEW_YORK = 'new_york'
__new__(value)
current_session(dt=None)[source]

Determine which forex sessions are active.

Parameters:

dt (datetime | None, default: None) – Datetime in UTC. Defaults to now.

Return type:

list[ForexSession]

Returns:

List of active ForexSession values.

Example

>>> current_session(datetime(2024, 1, 15, 14, 0, tzinfo=timezone.utc))
[<ForexSession.LONDON: 'london'>, <ForexSession.NEW_YORK: 'new_york'>]
session_overlaps()[source]

Return the major session overlap periods.

Returns:

(session1, session2, start_utc, end_utc).

Return type:

list[tuple[ForexSession, ForexSession, time, time]]

session_hours(session)[source]

Get the UTC start and end times for a session.

Parameters:

session (ForexSession) – Forex trading session.

Return type:

tuple[time, time]

Returns:

Tuple of (start_time, end_time) in UTC.

Analysis

Forex-specific calculations: pips, lot sizing, position sizing.

pips(price_change, pair=None, is_jpy=False)[source]

Convert price change to pips.

Use this to express price movements in the standard forex unit (pips) for consistent comparison across pairs. One pip is 0.0001 for most pairs and 0.01 for JPY pairs.

Parameters:
  • price_change (float | Series) – Price difference (e.g., 1.1050 - 1.1000 = 0.0050).

  • pair (CurrencyPair | None, default: None) – CurrencyPair (auto-detects JPY pairs).

  • is_jpy (bool, default: False) – Whether pair involves JPY (if pair not provided).

Return type:

float | Series

Returns:

Number of pips (can be negative for downward moves).

Example

>>> pips(0.0050)  # 50 pips for non-JPY pair
50.0
>>> pips(0.50, is_jpy=True)  # 50 pips for JPY pair
50.0

See also

pip_value: Dollar value of one pip.

pip_value(pair=None, lot_size_units=100000, is_jpy=False, exchange_rate=1.0)[source]

Calculate the value of one pip in account currency.

Use pip value to determine the dollar (or account currency) impact of a one-pip move for a given position size. This is fundamental to position sizing and risk management in forex.

Formula: pip_value = (pip_size * lot_size_units) / exchange_rate

Parameters:
  • pair (CurrencyPair | None, default: None) – CurrencyPair (auto-detects JPY pip size).

  • lot_size_units (float, default: 100000) – Position size in units (standard lot = 100,000, mini = 10,000, micro = 1,000).

  • is_jpy (bool, default: False) – Whether pair involves JPY (if pair not provided).

  • exchange_rate (float, default: 1.0) – Rate to convert to account currency. Set to 1.0 if account currency matches the quote currency.

Return type:

float

Returns:

Value of one pip in account currency.

Example

>>> pip_value(lot_size_units=100_000)  # Standard lot, non-JPY
10.0
>>> pip_value(lot_size_units=10_000)  # Mini lot
1.0
>>> pip_value(lot_size_units=100_000, is_jpy=True)  # JPY pair
1000.0

See also

lot_size: Calculate position size from risk parameters. pips: Convert price change to pip count.

lot_size(account_balance, risk_percent, stop_loss_pips, pair=None, is_jpy=False, exchange_rate=1.0)[source]

Calculate position size in lots based on risk management.

Use lot size calculation to determine how large a position to take given your account size, risk tolerance, and stop-loss distance. This ensures that if the stop loss is hit, the loss is exactly the specified percentage of your account.

Formula: lots = (account * risk%) / (stop_pips * pip_value_per_lot)

Parameters:
  • account_balance (float) – Account balance in account currency.

  • risk_percent (float) – Risk per trade as percentage (e.g., 1.0 = 1%). Professional traders typically risk 0.5-2% per trade.

  • stop_loss_pips (float) – Stop loss distance in pips. Wider stops require smaller positions to maintain the same risk.

  • pair (CurrencyPair | None, default: None) – CurrencyPair.

  • is_jpy (bool, default: False) – Whether pair involves JPY.

  • exchange_rate (float, default: 1.0) – Rate to convert to account currency.

Return type:

float

Returns:

Position size in standard lots (1 lot = 100,000 units).

Example

>>> lot_size(10_000, risk_percent=1.0, stop_loss_pips=50)
0.2
>>> lot_size(50_000, risk_percent=2.0, stop_loss_pips=100)
1.0

See also

pip_value: Value of one pip for a given position size. pips: Convert price change to pip count.

spread_cost(spread_pips, lot_size_units=100000, pair=None, is_jpy=False)[source]

Calculate the cost of the spread for a position.

The spread cost is an implicit transaction cost paid every time you enter or exit a position. Use this to assess whether the spread makes a strategy unviable at a given position size.

Parameters:
  • spread_pips (float) – Bid-ask spread in pips (e.g., 1.5 pips for EUR/USD).

  • lot_size_units (float, default: 100000) – Position size in units (default 100,000 = 1 lot).

  • pair (CurrencyPair | None, default: None) – CurrencyPair.

  • is_jpy (bool, default: False) – Whether pair involves JPY.

Return type:

float

Returns:

Spread cost in quote currency.

Example

>>> spread_cost(1.5, lot_size_units=100_000)  # 1.5 pip spread, 1 lot
15.0

See also

pip_value: Value of one pip.

pip_distance(entry, exit, pair=None, is_jpy=False)[source]

Calculate the pip distance between two prices.

Use this to measure the signed distance in pips between an entry and exit price. Automatically detects JPY pairs (2-decimal pip size) versus standard pairs (4-decimal pip size).

Formula: pip_distance = (exit - entry) / pip_size

Parameters:
  • entry (float) – Entry (open) price.

  • exit (float) – Exit (close) price.

  • pair (CurrencyPair | str | None, default: None) – CurrencyPair instance or string like 'USDJPY' for automatic JPY detection. If None, uses is_jpy flag.

  • is_jpy (bool, default: False) – Whether the pair involves JPY (only used when pair is not provided).

Return type:

float

Returns:

Signed pip distance. Positive means the price moved up from entry to exit (profit for a long position).

Example

>>> pip_distance(1.1000, 1.1050)  # 50 pips up on EUR/USD
50.0
>>> pip_distance(110.00, 110.50, is_jpy=True)  # 50 pips on USD/JPY
50.0
>>> pip_distance(1.1050, 1.1000)  # 50 pips down
-50.0

See also

pips: Convert a raw price change to pip count. risk_reward_ratio: Use pip distances for R:R analysis.

position_value(lots, pip_val, pips_moved)[source]

Calculate position P&L in account currency.

Use this to compute the profit or loss of a forex position given the number of lots, the pip value per lot, and the number of pips the price has moved.

Formula: P&L = lots * pip_value * pips

Parameters:
  • lots (float) – Number of standard lots (1 lot = 100,000 units). Fractional lots are supported (e.g., 0.1 for a mini lot).

  • pip_val (float) – Value of one pip per lot in account currency. Use pip_value() to compute this.

  • pips_moved (float) – Number of pips the position has moved. Positive for favourable moves (long profits / short losses), negative for adverse moves.

Return type:

float

Returns:

Profit or loss in account currency. Positive means profit.

Example

>>> position_value(lots=1.0, pip_val=10.0, pips_moved=50)
500.0
>>> position_value(lots=0.5, pip_val=10.0, pips_moved=-30)
-150.0

See also

pip_value: Calculate pip value per lot. lot_size: Calculate position size from risk parameters.

risk_reward_ratio(entry, stop, target, pair=None, is_jpy=False)[source]

Calculate the risk-reward ratio for a trade.

Use this before entering a trade to evaluate whether the potential reward justifies the risk. A ratio above 2.0 is generally considered favourable; below 1.0 means you risk more than you stand to gain.

The function works for both long and short trades by comparing absolute distances from entry to stop and entry to target.

Parameters:
  • entry (float) – Entry price.

  • stop (float) – Stop-loss price.

  • target (float) – Take-profit price.

  • pair (CurrencyPair | str | None, default: None) – CurrencyPair or string (e.g., 'USDJPY') for automatic JPY detection.

  • is_jpy (bool, default: False) – Whether the pair involves JPY.

Returns:

  • ratio (float) – Reward-to-risk ratio (target distance / stop distance). Values above 2.0 are generally favourable.

  • risk_pips (float) – Distance from entry to stop in pips (always positive).

  • reward_pips (float) – Distance from entry to target in pips (always positive).

Return type:

dict[str, float]

Example

>>> result = risk_reward_ratio(1.1000, 1.0950, 1.1100)
>>> result['ratio']
2.0
>>> result['risk_pips']
50.0
>>> result['reward_pips']
100.0

See also

pip_distance: Raw pip distance between two prices. lot_size: Size position based on risk.

margin_call_price(entry, balance, margin_used, leverage, side='long')[source]

Calculate the price at which a margin call occurs.

Use this to determine the maximum adverse move before a margin call is triggered. A margin call occurs when the account equity falls to (or below) the margin required to maintain the position.

For a long position, the margin call price is below entry; for a short position, it is above entry.

Formula (long):

margin_call_price = entry - (balance - margin_used) / (leverage * margin_used / entry)

Simplified:

margin_call_price = entry * (1 - (balance - margin_used) / (leverage * margin_used))

Parameters:
  • entry (float) – Entry price of the position.

  • balance (float) – Account balance in account currency.

  • margin_used (float) – Margin (collateral) used for this position in account currency.

  • leverage (float) – Leverage ratio (e.g., 50.0 for 50:1 leverage).

  • side (str, default: 'long') – 'long' or 'short'.

Return type:

float

Returns:

Price at which a margin call is triggered. For longs this is below entry; for shorts it is above entry. If the margin call price is negative (for longs), returns 0.0 since prices cannot go negative.

Example

>>> # $10,000 balance, $2,000 margin, 50:1 leverage, long at 1.1000
>>> mc = margin_call_price(1.1000, 10_000, 2_000, 50.0)
>>> mc < 1.1000  # margin call below entry for long
True

Notes

This is a simplified model. Real margin calls depend on the broker’s margin call level (e.g., 100% or 50% of margin), floating P&L on other positions, and swap costs.

See also

lot_size: Size positions to control risk.

Carry Trade

Carry trade analysis and interest rate differential calculations.

interest_rate_differential(base_rate, quote_rate)[source]

Calculate interest rate differential between two currencies.

The interest rate differential is the foundation of carry trades. A positive differential means the base currency has a higher yield, so a long position earns positive carry.

Parameters:
  • base_rate (float) – Annual interest rate of base currency (e.g., 0.05 = 5%).

  • quote_rate (float) – Annual interest rate of quote currency.

Return type:

float

Returns:

Interest rate differential (base - quote). Positive means long carry, negative means short carry.

Example

>>> interest_rate_differential(0.05, 0.01)  # AUD vs JPY
0.04
>>> interest_rate_differential(0.01, 0.05)  # JPY vs AUD (negative carry)
-0.04

See also

carry_return: Full carry trade return including spot moves. carry_attractiveness: Rank pairs by carry.

carry_return(spot_change, base_rate, quote_rate, periods_per_year=252)[source]

Calculate total carry trade return (spot + carry).

Use this to evaluate the full P&L of a carry trade, which earns the interest rate differential daily but is exposed to spot rate movements. A positive carry (base rate > quote rate) means you earn interest by holding the position, but adverse spot moves can overwhelm the carry.

Total return = spot return + (base_rate - quote_rate) / periods_per_year

Parameters:
  • spot_change (Series) – Daily spot rate returns (log or simple).

  • base_rate (float) – Annual interest rate of base currency (e.g., 0.04 = 4%).

  • quote_rate (float) – Annual interest rate of quote currency.

  • periods_per_year (int, default: 252) – Periods per year (default 252 for daily data).

Return type:

Series

Returns:

Total return series (spot return + daily carry). Positive values indicate profit for a long carry position.

Example

>>> import pandas as pd
>>> spot_returns = pd.Series([0.001, -0.002, 0.0005, 0.001])
>>> total = carry_return(spot_returns, base_rate=0.05, quote_rate=0.01)
>>> total.iloc[0] > spot_returns.iloc[0]  # carry adds return
True

See also

interest_rate_differential: Raw rate differential. carry_attractiveness: Rank pairs by carry.

forward_premium(spot, base_rate, quote_rate, days=365)[source]

Calculate the forward premium/discount.

Use this to compute the theoretical forward exchange rate based on covered interest rate parity. If the forward rate exceeds the spot rate, the base currency trades at a forward discount (its interest rate is higher than the quote currency’s).

Formula: F = S * (1 + r_quote * days/365) / (1 + r_base * days/365)

Parameters:
  • spot (float) – Current spot rate.

  • base_rate (float) – Base currency annual rate.

  • quote_rate (float) – Quote currency annual rate.

  • days (int, default: 365) – Forward period in days (default 365 for 1-year forward).

Return type:

float

Returns:

Forward rate. Compare to spot to determine premium (F > S) or discount (F < S).

Example

>>> forward_premium(1.1000, base_rate=0.04, quote_rate=0.02, days=365)
1.0788461538461539

See also

carry_return: Full carry trade P&L.

carry_attractiveness(rates, pairs=None)[source]

Rank currency pairs by carry attractiveness.

Use this to screen the forex universe for the best carry trade opportunities. Pairs with the highest interest rate differential offer the most carry income, but also tend to have higher crash risk (carry trade unwinds).

Parameters:
  • rates (dict[str, float]) – Dict mapping currency code to annual interest rate (e.g., {'USD': 0.05, 'JPY': 0.001, 'AUD': 0.04}).

  • pairs (list[tuple[str, str]] | None, default: None) – Optional list of (base, quote) pairs to evaluate. If None, evaluates all combinations.

Return type:

DataFrame

Returns:

DataFrame with columns pair, base_rate, quote_rate, differential, sorted by differential descending. Top rows are the most attractive carry trades.

Example

>>> rates = {'USD': 0.05, 'JPY': 0.001, 'EUR': 0.04}
>>> df = carry_attractiveness(rates)
>>> df.iloc[0]['pair']  # highest carry
'USDJPY'

See also

carry_return: Full carry trade P&L including spot moves. forward_premium: Covered interest rate parity forward rate.

carry_portfolio(rates_dict, weights=None, n_long=3, n_short=3)[source]

Construct a carry trade portfolio: long high-yield, short low-yield.

Use this to build a systematic carry strategy. The portfolio goes long the n_long highest-yielding currencies and short the n_short lowest-yielding currencies. If custom weights are provided, they override the automatic equal-weight allocation.

This is the standard G10 carry trade approach used by institutional investors. It earns the interest rate differential but is exposed to crash risk (carry unwinds).

Parameters:
  • rates_dict (dict[str, float]) – Dictionary mapping currency codes to their annual interest rates (e.g., {'USD': 0.05, 'JPY': 0.001, 'AUD': 0.04, 'EUR': 0.03, 'CHF': 0.015, 'NZD': 0.045}).

  • weights (dict[str, float] | None, default: None) – Optional custom weight for each currency. If None, equal-weights the long and short legs separately.

  • n_long (int, default: 3) – Number of currencies in the long leg (default 3).

  • n_short (int, default: 3) – Number of currencies in the short leg (default 3).

Returns:

  • weights (dict) – Portfolio weights per currency. Positive for long positions, negative for short positions. Weights sum to approximately zero (dollar-neutral).

  • expected_carry (float) – Expected annualised carry return (weighted sum of rates for longs minus shorts).

  • long_currencies (list) – Currencies in the long leg.

  • short_currencies (list) – Currencies in the short leg.

Return type:

dict[str, object]

Example

>>> rates = {'USD': 0.05, 'JPY': 0.001, 'AUD': 0.04,
...          'EUR': 0.03, 'CHF': 0.015, 'NZD': 0.045}
>>> result = carry_portfolio(rates, n_long=2, n_short=2)
>>> result['expected_carry'] > 0
True
>>> len(result['long_currencies'])
2

See also

carry_attractiveness: Rank all pairs by carry differential. carry_return: Full P&L including spot moves.

uncovered_interest_parity(domestic_rate, foreign_rate, spot, maturity=1.0)[source]

Uncovered Interest Rate Parity (UIP) expected future spot rate.

Use this to compute the expected future exchange rate implied by the interest rate differential under UIP. UIP states that the expected depreciation of a currency equals the interest rate differential.

While Covered Interest Parity (CIP) holds by arbitrage, UIP is an equilibrium condition that often fails empirically (the “forward premium puzzle”), which is why carry trades can be profitable.

Formula:

E[S_T] = S * (1 + r_domestic * T) / (1 + r_foreign * T)

This is the same formula as Covered Interest Parity but interpreted as the expected future spot rate rather than the no-arbitrage forward rate.

Parameters:
  • domestic_rate (float) – Annual interest rate of the domestic (base) currency.

  • foreign_rate (float) – Annual interest rate of the foreign (quote) currency.

  • spot (float) – Current spot exchange rate (domestic/foreign).

  • maturity (float, default: 1.0) – Horizon in years (default 1.0).

Returns:

  • forward_rate (float) – UIP-implied expected future spot rate. If domestic rate > foreign rate, the domestic currency is expected to depreciate (forward_rate > spot).

  • forward_premium (float) – Forward premium as a percentage ((forward - spot) / spot). Positive means the domestic currency trades at a forward premium (expected to depreciate).

Return type:

dict[str, float]

Example

>>> result = uncovered_interest_parity(0.05, 0.01, 1.1000, maturity=1.0)
>>> result['forward_rate'] > 1.1000  # domestic rate higher -> depreciation expected
True
>>> abs(result['forward_premium'] - 0.0396) < 0.01
True

Notes

Reference: Fama (1984). “Forward and Spot Exchange Rates.” Journal of Monetary Economics, 14, 319-338.

See also

forward_premium: CIP-based forward rate calculation. carry_return: Carry trade P&L (profits when UIP fails).