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
Statistics (wraquant.stats) – Cointegration for FX pairs trading
Risk Management (wraquant.risk) – VaR for FX portfolios
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) –CurrencyPairdataclass for structured pair handling,cross_ratecomputation,major_pairsconvenience list,correlation_matrixacross pairs,currency_strengthscoring, andvolatility_by_sessionfor session-level vol profiles.Analysis (
analysis) – Core FX calculations:pips(price to pip conversion),pip_value,pip_distance,lot_sizefor position sizing,spread_cost,position_value,risk_reward_ratio, andmargin_call_price.Sessions (
session) –ForexSessionenum (Tokyo, London, New York),current_sessiondetection, andsession_overlapsfor identifying high-liquidity windows.Carry (
carry) – Carry trade analytics:carry_return,carry_attractivenessscoring,carry_portfolioconstruction,interest_rate_differential,forward_premium, anduncovered_interest_paritytesting.Risk (
risk) –fx_portfolio_riskfor 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:
objectA forex currency pair.
- Parameters:
Example
>>> pair = CurrencyPair(Currency.EUR, Currency.USD) >>> pair.symbol 'EURUSD'
- 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:
- Return type:
- 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:
- 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:
- 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'yieldsEURandUSD), computes returns, and averages each currency’s performance across all pairs it appears in (positive for appreciation, negative for depreciation).- Parameters:
- Return type:
- 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:
- 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:
- 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:
- 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:
- 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:
- 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:
- 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. Usepip_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:
- 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:
- 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:
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:
- 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:
StrEnumMajor 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:
- 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:
- 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:
- Return type:
- 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:
- Return type:
- 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:
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:
- Return type:
- 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.
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:
- Return type:
- 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:
- 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:
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
forexandriskby computing:Base-currency positions: converts all positions to the base currency using current exchange rates.
Currency exposure: the net exposure to each currency as a fraction of total portfolio value.
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:
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:
objectA forex currency pair.
- Parameters:
Example
>>> pair = CurrencyPair(Currency.EUR, Currency.USD) >>> pair.symbol 'EURUSD'
- major_pairs()[source]¶
Return the 7 major forex pairs.
- Return type:
- 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:
- Return type:
- 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:
- 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'yieldsEURandUSD), computes returns, and averages each currency’s performance across all pairs it appears in (positive for appreciation, negative for depreciation).- Parameters:
- Return type:
- 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:
- 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:
StrEnumMajor 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:
- 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'>]
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:
- 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:
- 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:
- 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:
- 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:
- 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. Usepip_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:
- 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:
- 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:
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:
- 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:
- Return type:
- 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:
- Return type:
- 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.
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:
- Return type:
- 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:
- Return type:
- 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:
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:
- 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:
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).