import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import seaborn as sns
import statsmodels.api as sm
import statsmodels.formula.api as smf
from scipy import stats
from arch import arch_model
from linearmodels.panel import PanelOLS
import warnings
warnings.filterwarnings('ignore')
plt.rcParams.update({
'figure.figsize': (12, 6),
'figure.dpi': 150,
'font.size': 11,
'axes.spines.top': False,
'axes.spines.right': False
})39 Exchange Rate Dynamics
In this chapter, we construct a comprehensive exchange rate database for the Vietnamese Dong (VND) against major currencies, analyze the statistical properties of VND returns, test covered and uncovered interest rate parity, estimate the currency exposure of Vietnamese listed firms, model volatility regimes using GARCH, and evaluate hedging strategies for international investors.
Exchange rates sit at the intersection of macroeconomics and finance. For international investors considering Vietnamese equities, the VND/USD exchange rate is not a secondary concern; it is a first-order determinant of dollar-denominated returns. A Vietnamese stock portfolio that earns 15% in VND terms but coincides with a 5% VND depreciation delivers only about 10% in USD terms. Conversely, periods of VND stability or appreciation amplify returns for foreign investors. Understanding exchange rate dynamics is therefore essential for anyone working with Vietnamese financial data in an international context.
Vietnam presents a distinctive exchange rate regime. Unlike freely floating currencies such as the USD/EUR or USD/JPY, the VND operates under a managed float. The State Bank of Vietnam (SBV) sets a daily reference rate and allows trading within a band (currently \(\pm\) 3% on the interbank market and \(\pm\) 5% against the official rate for commercial banks). This regime creates specific patterns in VND returns that differ fundamentally from those observed in freely floating pairs: clustered movements near band boundaries, discrete adjustments when the SBV shifts the reference rate, and periods of near-zero volatility punctuated by sharp moves during policy changes.
This chapter develops tools for working with these dynamics. We progress from data construction through statistical characterization to economic applications: interest rate parity tests, firm-level exposure estimation, GARCH volatility modeling, and hedging strategy evaluation.
39.1 Theoretical Foundations
39.1.1 Exchange Rate Determination
The modern theory of exchange rate determination rests on several building blocks. Dornbusch (1976) provides the canonical “overshooting” model: because goods prices are sticky while asset prices adjust instantly, a monetary shock causes the exchange rate to overshoot its long-run equilibrium. Frankel (1979) extends this to a real interest differential model where the exchange rate depends on relative money supplies, income levels, and interest rates across countries.
Despite elegant theory, Meese and Rogoff (1983) delivered a devastating empirical result: no structural model of exchange rates consistently outperforms a random walk in out-of-sample forecasting. This finding, confirmed repeatedly over four decades and surveyed by Rossi (2013), means that exchange rate changes are, to a first approximation, unpredictable. For practical purposes, the best forecast of tomorrow’s VND/USD rate is today’s rate.
39.1.2 Interest Rate Parity
Two parity conditions link exchange rates to interest rates:
Covered Interest Rate Parity (CIP): Arbitrage between spot and forward markets should equalize the forward premium with the interest rate differential:
\[ F_{t,t+k} = S_t \cdot \frac{(1 + r^{VND}_{t,k})}{(1 + r^{USD}_{t,k})} \tag{39.1}\]
where \(S_t\) is the spot rate (VND per USD), \(F_{t,t+k}\) is the \(k\)-period forward rate, and \(r^{VND}_{t,k}\) and \(r^{USD}_{t,k}\) are the domestic and foreign interest rates for maturity \(k\). CIP should hold exactly in the absence of transaction costs, credit risk, and capital controls. Du, Tepper, and Verdelhan (2018) document that CIP violations have widened significantly in major currency pairs since 2008, driven by post-crisis bank balance sheet constraints. In Vietnam, capital controls and limited forward market liquidity create additional CIP deviations.
Uncovered Interest Rate Parity (UIP): The expected depreciation should equal the interest rate differential:
\[ \mathbb{E}_t[\Delta s_{t+k}] = r^{VND}_{t,k} - r^{USD}_{t,k} \tag{39.2}\]
where \(\Delta s_{t+k} = \ln(S_{t+k}/S_t)\). UIP is an equilibrium condition, not an arbitrage condition, it requires risk-neutral investors. Fama (1984) famously showed that the forward premium predicts exchange rate changes with the wrong sign: high-interest-rate currencies tend to appreciate rather than depreciate, contradicting UIP. This “forward premium anomaly” is the foundation of the carry trade.
39.1.3 The Carry Trade
The carry trade exploits UIP violations by borrowing in low-interest-rate currencies and investing in high-interest-rate currencies. Lustig and Verdelhan (2007) show that a portfolio of carry trade positions earns a significant risk premium, which they link to consumption growth risk. Menkhoff et al. (2012) identify global FX volatility as the key risk factor: carry trades lose money precisely when global volatility spikes, a “crash risk” documented by Brunnermeier, Nagel, and Pedersen (2008).
Vietnam, with interest rates typically 3-8 percentage points above U.S. rates, is a natural candidate for carry trade inflows. The managed exchange rate regime provides an additional attraction: the SBV’s implicit defense of the band reduces short-term volatility, increasing the Sharpe ratio of the carry position, until a band adjustment or devaluation erases accumulated gains in a single move.
39.1.4 The Trilemma
Obstfeld, Shambaugh, and Taylor (2005) formalize the “impossible trinity”: a country cannot simultaneously maintain (i) a fixed exchange rate, (ii) an independent monetary policy, and (iii) free capital mobility. Vietnam navigates this trilemma by maintaining partial capital controls alongside its managed float, allowing some degree of monetary policy independence. Understanding where Vietnam sits on the trilemma at any given time is essential for interpreting exchange rate data: periods of tighter capital controls produce artificially low exchange rate volatility that does not reflect true economic uncertainty.
39.2 The VND Exchange Rate Regime
39.2.1 Historical Evolution
The VND/USD exchange rate has evolved through several distinct phases:
| Period | Regime | Key Features |
|---|---|---|
| Pre-1999 | Narrow band | SBV set the rate with minimal flexibility |
| 1999-2007 | Gradual crawl | Slow, controlled depreciation (~1-2% per year) |
| 2008-2011 | Crisis adjustments | Multiple discrete devaluations (2008, 2010, 2011) |
| 2012-2015 | Stabilization | Tight band, rare adjustments, building reserves |
| 2016-present | Reference rate mechanism | Daily reference rate based on a basket; \(\pm\) 3% band |
The 2016 reform, shifting from a fixed peg to a daily reference rate influenced by a currency basket, was a significant structural change. The reference rate now incorporates the previous day’s closing interbank rate, supply-demand conditions, and movements in currencies of major trading partners (USD, EUR, CNY, JPY, KRW).
39.2.2 Band Mechanics
The SBV announces a daily reference rate each morning. Commercial banks may quote rates within a band around this reference. The interbank market operates with a narrower \(\pm\) 3% band. When the spot rate approaches the band ceiling, the SBV may intervene by selling USD from reserves, widening the band, or adjusting the reference rate.
This regime creates distinctive statistical properties:
- Return clustering near zero: On most days, the VND/USD rate barely moves, producing a spike at zero in the return distribution.
- Discrete jumps: Reference rate adjustments create large single-day moves that appear as outliers.
- Bounded volatility: The band constrains daily moves, producing a return distribution with thinner tails than a freely floating currency.
- Asymmetric adjustment: Depreciation pressure (VND weakening) is more common than appreciation, reflecting persistent inflation differentials.
39.3 Data Construction
39.3.1 Loading Libraries
39.3.2 Retrieving Exchange Rate Data
We extract daily spot rates for the VND against all major currencies, along with the SBV reference rate and forward rates where available.
from datacore import DataCoreClient
client = DataCoreClient()
# Spot exchange rates: VND per unit of foreign currency
fx_spot = client.get_exchange_rates(
base_currency='VND',
quote_currencies=['USD', 'EUR', 'JPY', 'CNY', 'KRW',
'SGD', 'THB', 'GBP', 'AUD'],
start_date='2005-01-01',
end_date='2024-12-31',
fields=['date', 'from_currency', 'to_currency', 'rate',
'bid', 'ask', 'reference_rate']
)
# Forward rates (VND/USD)
fx_forward = client.get_fx_forwards(
pair='VND/USD',
start_date='2010-01-01',
end_date='2024-12-31',
tenors=['1M', '3M', '6M', '12M'],
fields=['date', 'tenor', 'forward_rate', 'forward_points']
)
# Interest rates for parity tests
interest_rates = client.get_interest_rates(
countries=['VN', 'US'],
start_date='2005-01-01',
end_date='2024-12-31',
instruments=['interbank_overnight', 'deposit_3m',
'government_bond_1y', 'government_bond_10y']
)
print(f"Spot observations: {fx_spot.shape[0]:,}")
print(f"Forward observations: {fx_forward.shape[0]:,}")
print(f"Interest rate observations: {interest_rates.shape[0]:,}")39.3.3 Constructing the VND/USD Time Series
We focus primarily on the VND/USD pair, the dominant bilateral rate for Vietnam, while using other pairs for cross-rate analysis.
# Filter VND/USD
vnd_usd = fx_spot[
(fx_spot['from_currency'] == 'VND') &
(fx_spot['to_currency'] == 'USD')
].copy()
vnd_usd['date'] = pd.to_datetime(vnd_usd['date'])
vnd_usd = vnd_usd.sort_values('date').set_index('date')
# Log returns: positive = VND depreciation, negative = VND appreciation
vnd_usd['log_return'] = np.log(vnd_usd['rate'] / vnd_usd['rate'].shift(1))
# Bid-ask spread as fraction of mid rate
vnd_usd['spread_pct'] = (vnd_usd['ask'] - vnd_usd['bid']) / vnd_usd['rate']
# Distance from reference rate (in %)
vnd_usd['deviation_from_ref'] = (
(vnd_usd['rate'] - vnd_usd['reference_rate'])
/ vnd_usd['reference_rate'] * 100
)
# Rolling volatility measures
vnd_usd['vol_30d'] = vnd_usd['log_return'].rolling(30).std() * np.sqrt(252)
vnd_usd['vol_90d'] = vnd_usd['log_return'].rolling(90).std() * np.sqrt(252)
vnd_usd = vnd_usd.dropna(subset=['log_return'])
print(f"VND/USD series: {len(vnd_usd)} daily observations")
print(f"Date range: {vnd_usd.index.min()} to {vnd_usd.index.max()}")
print(f"\nSummary of daily log returns:")
print(vnd_usd['log_return'].describe().round(6))39.3.4 Multi-Currency Panel
For cross-rate analysis and ASEAN comparisons, we construct a panel of log returns for all currency pairs.
# Pivot to wide format: one column per currency pair
fx_wide = fx_spot.pivot_table(
index='date', columns='to_currency', values='rate'
)
fx_wide.index = pd.to_datetime(fx_wide.index)
fx_wide = fx_wide.sort_index()
# Compute log returns for all pairs
fx_returns = np.log(fx_wide / fx_wide.shift(1)).dropna()
# Summary statistics
fx_summary = pd.DataFrame({
'Mean (ann.)': fx_returns.mean() * 252,
'Vol (ann.)': fx_returns.std() * np.sqrt(252),
'Skewness': fx_returns.skew(),
'Kurtosis': fx_returns.kurtosis(),
'Min': fx_returns.min(),
'Max': fx_returns.max()
})
print("VND Cross-Rate Return Statistics (Daily):")
print(fx_summary.round(4))39.4 Statistical Properties of VND Returns
39.4.1 Return Distribution
The distribution of VND/USD log returns deviates sharply from normality. The managed float regime produces a return distribution that is leptokurtic (fat-tailed) and concentrated near zero, with occasional large discrete jumps from reference rate adjustments.
fig, axes = plt.subplots(1, 2, figsize=(14, 5))
# Panel A: Histogram with normal overlay
returns = vnd_usd['log_return']
mu, sigma = returns.mean(), returns.std()
axes[0].hist(returns, bins=100, density=True, color='#2C5F8A',
alpha=0.7, edgecolor='white', label='Empirical')
x_range = np.linspace(returns.min(), returns.max(), 200)
axes[0].plot(x_range, stats.norm.pdf(x_range, mu, sigma),
color='#C0392B', linewidth=2, label='Normal fit')
axes[0].set_xlabel('Daily Log Return')
axes[0].set_ylabel('Density')
axes[0].set_title('Panel A: Return Distribution')
axes[0].legend()
# Panel B: QQ plot
stats.probplot(returns, dist='norm', plot=axes[1])
axes[1].set_title('Panel B: Normal Q-Q Plot')
axes[1].get_lines()[0].set_color('#2C5F8A')
axes[1].get_lines()[0].set_markersize(3)
axes[1].get_lines()[1].set_color('#C0392B')
plt.tight_layout()
plt.show()
# Formal tests
jb_stat, jb_p = stats.jarque_bera(returns)
print(f"\nJarque-Bera test: statistic = {jb_stat:.1f}, p-value = {jb_p:.2e}")
print(f"Skewness: {returns.skew():.4f}")
print(f"Excess Kurtosis: {returns.kurtosis():.4f}")39.4.2 Autocorrelation Structure
Freely floating exchange rate returns are approximately uncorrelated (consistent with the random walk result of Meese and Rogoff (1983)). Managed exchange rates, however, may exhibit serial correlation because the central bank smooths adjustments over multiple days.
from statsmodels.graphics.tsaplots import plot_acf
fig, axes = plt.subplots(2, 1, figsize=(12, 8))
# ACF of returns
plot_acf(vnd_usd['log_return'].dropna(), lags=40, ax=axes[0],
color='#2C5F8A', vlines_kwargs={'color': '#2C5F8A'})
axes[0].set_title('Autocorrelation of Daily Returns')
axes[0].set_ylabel('ACF')
# ACF of squared returns (volatility clustering)
plot_acf(vnd_usd['log_return'].dropna() ** 2, lags=40, ax=axes[1],
color='#C0392B', vlines_kwargs={'color': '#C0392B'})
axes[1].set_title('Autocorrelation of Squared Returns (Volatility Clustering)')
axes[1].set_ylabel('ACF')
plt.tight_layout()
plt.show()
# Ljung-Box test
from statsmodels.stats.diagnostic import acorr_ljungbox
lb_returns = acorr_ljungbox(returns, lags=10, return_df=True)
lb_squared = acorr_ljungbox(returns ** 2, lags=10, return_df=True)
print("Ljung-Box Test (Returns, lag 10):")
print(f" Statistic: {lb_returns['lb_stat'].iloc[-1]:.2f}, "
f"p-value: {lb_returns['lb_pvalue'].iloc[-1]:.4f}")
print("Ljung-Box Test (Squared Returns, lag 10):")
print(f" Statistic: {lb_squared['lb_stat'].iloc[-1]:.2f}, "
f"p-value: {lb_squared['lb_pvalue'].iloc[-1]:.4f}")39.4.3 Reference Rate Adjustments and Structural Breaks
The SBV periodically adjusts the reference rate or the trading band width, events that create structural breaks in the exchange rate series. Identifying these breaks is essential for accurate modeling.
# Detect large daily moves (proxy for policy adjustments)
threshold = vnd_usd['log_return'].std() * 3
large_moves = vnd_usd[vnd_usd['log_return'].abs() > threshold].copy()
# Known major adjustment dates (approximate)
policy_events = pd.DataFrame({
'date': pd.to_datetime([
'2008-06-10', '2008-12-25', '2009-11-25',
'2010-02-11', '2010-08-18', '2011-02-11',
'2011-08-10', '2015-08-12', '2015-08-19',
'2016-01-04'
]),
'event': [
'Band widened to +/-1%', 'Band widened to +/-3%',
'Devaluation ~5.4%', 'Devaluation ~3.4%',
'Band narrowed to +/-1%', 'Devaluation ~8.5%',
'Band widened to +/-1%', 'Devaluation ~1%',
'Devaluation ~1%', 'New reference rate mechanism'
]
})
fig, ax = plt.subplots(figsize=(14, 6))
ax.plot(vnd_usd.index, vnd_usd['rate'], color='#2C5F8A',
linewidth=1.5, label='VND/USD Spot')
if 'reference_rate' in vnd_usd.columns:
ref_clean = vnd_usd['reference_rate'].dropna()
if len(ref_clean) > 0:
ax.plot(ref_clean.index, ref_clean, color='#E67E22',
linewidth=1, alpha=0.7, label='SBV Reference Rate')
# Mark policy events
for _, event in policy_events.iterrows():
if event['date'] >= vnd_usd.index.min():
rate_at_event = vnd_usd['rate'].asof(event['date'])
ax.annotate(
event['event'],
xy=(event['date'], rate_at_event),
xytext=(0, 30), textcoords='offset points',
fontsize=7, rotation=45, ha='left',
arrowprops=dict(arrowstyle='->', color='#C0392B',
lw=0.8),
color='#C0392B'
)
ax.set_ylabel('VND per USD')
ax.set_title('VND/USD Exchange Rate with Policy Events')
ax.legend(loc='upper left')
plt.tight_layout()
plt.show()39.4.4 Cross-Currency Correlations
Understanding how VND co-moves with other currencies is important for diversification and for identifying common drivers (e.g., USD strength affects all Asian currencies).
# Correlation matrix of daily returns
corr_matrix = fx_returns.corr()
fig, ax = plt.subplots(figsize=(9, 8))
mask = np.triu(np.ones_like(corr_matrix, dtype=bool), k=1)
sns.heatmap(
corr_matrix, mask=mask, annot=True, fmt='.2f',
cmap='RdBu_r', center=0, vmin=-1, vmax=1,
square=True, linewidths=0.5, ax=ax,
cbar_kws={'label': 'Correlation'}
)
ax.set_title('Cross-Rate Return Correlations (VND Base)')
plt.tight_layout()
plt.show()39.5 Interest Rate Parity Tests
39.5.1 Testing Covered Interest Rate Parity
CIP deviations are measured as the gap between the forward premium and the interest rate differential:
\[ \text{CIP Deviation}_t = f_{t,k} - s_t - (r^{VND}_{t,k} - r^{USD}_{t,k}) \tag{39.3}\]
where \(f_{t,k} = \ln F_{t,t+k}\) and \(s_t = \ln S_t\) are log forward and spot rates. Under exact CIP, this deviation is zero.
# Merge spot, forward, and interest rate data
vnd_usd_monthly = vnd_usd['rate'].resample('M').last().to_frame('spot')
vnd_usd_monthly.index = vnd_usd_monthly.index.to_period('M').to_timestamp()
# Forward rates (3-month tenor)
fwd_3m = fx_forward[fx_forward['tenor'] == '3M'].copy()
fwd_3m['date'] = pd.to_datetime(fwd_3m['date'])
fwd_3m = fwd_3m.set_index('date').resample('M').last()
# Interest rate differential
vn_rates = interest_rates[
(interest_rates['country'] == 'VN') &
(interest_rates['instrument'] == 'deposit_3m')
].set_index('date')['rate'].resample('M').last()
us_rates = interest_rates[
(interest_rates['country'] == 'US') &
(interest_rates['instrument'] == 'deposit_3m')
].set_index('date')['rate'].resample('M').last()
# Align and compute CIP deviation
cip_data = pd.DataFrame({
'spot': vnd_usd_monthly['spot'],
'forward': fwd_3m['forward_rate'],
'r_vnd': vn_rates / 100,
'r_usd': us_rates / 100
}).dropna()
# CIP deviation (annualized, in basis points)
cip_data['log_spot'] = np.log(cip_data['spot'])
cip_data['log_forward'] = np.log(cip_data['forward'])
cip_data['forward_premium'] = (
(cip_data['log_forward'] - cip_data['log_spot']) * 4
)
cip_data['rate_diff'] = cip_data['r_vnd'] - cip_data['r_usd']
cip_data['cip_deviation'] = (
(cip_data['forward_premium'] - cip_data['rate_diff']) * 10000
)
print("CIP Deviation Summary (basis points, annualized):")
print(cip_data['cip_deviation'].describe().round(1))
print(f"\nMean deviation: {cip_data['cip_deviation'].mean():.1f} bps")
t_stat, p_val = stats.ttest_1samp(cip_data['cip_deviation'].dropna(), 0)
print(f"One-sample t-test: t = {t_stat:.2f}, p = {p_val:.4f}")fig, axes = plt.subplots(2, 1, figsize=(14, 8), height_ratios=[2, 1])
# Panel A: CIP deviation time series
axes[0].fill_between(
cip_data.index, 0, cip_data['cip_deviation'],
where=cip_data['cip_deviation'] > 0,
color='#C0392B', alpha=0.4, label='Positive (VND forward expensive)'
)
axes[0].fill_between(
cip_data.index, 0, cip_data['cip_deviation'],
where=cip_data['cip_deviation'] <= 0,
color='#27AE60', alpha=0.4, label='Negative'
)
axes[0].axhline(y=0, color='black', linewidth=0.5)
axes[0].set_ylabel('CIP Deviation (bps)')
axes[0].set_title('Panel A: CIP Deviations for VND/USD')
axes[0].legend()
# Panel B: Interest rate differential
axes[1].plot(cip_data.index, cip_data['r_vnd'] * 100,
color='#C0392B', label='Vietnam 3M')
axes[1].plot(cip_data.index, cip_data['r_usd'] * 100,
color='#2C5F8A', label='US 3M')
axes[1].set_ylabel('Interest Rate (%)')
axes[1].set_xlabel('Date')
axes[1].set_title('Panel B: Interest Rate Differential')
axes[1].legend()
plt.tight_layout()
plt.show()39.5.2 Testing Uncovered Interest Rate Parity
The standard UIP regression is:
\[ \Delta s_{t+k} = \alpha + \beta (r^{VND}_{t,k} - r^{USD}_{t,k}) + \varepsilon_{t+k} \tag{39.4}\]
Under UIP, \(\alpha = 0\) and \(\beta = 1\). The Fama (1984) anomaly corresponds to \(\beta < 1\) (often \(\beta < 0\)), implying that high-interest-rate currencies appreciate rather than depreciate.
# Monthly exchange rate changes
cip_data['delta_s'] = (
np.log(cip_data['spot'].shift(-3) / cip_data['spot'])
)
# Fama regression
uip_data = cip_data[['delta_s', 'rate_diff']].dropna()
uip_model = sm.OLS(
uip_data['delta_s'],
sm.add_constant(uip_data['rate_diff'])
).fit(cov_type='HAC', cov_kwds={'maxlags': 4})
print("UIP (Fama) Regression: delta_s_{t+3m} = alpha + beta(r_VND - r_USD) + eps")
print(f"\nalpha = {uip_model.params['const']:.4f} "
f"(t = {uip_model.tvalues['const']:.2f})")
print(f"beta = {uip_model.params['rate_diff']:.4f} "
f"(t = {uip_model.tvalues['rate_diff']:.2f})")
print(f"R-squared = {uip_model.rsquared:.4f}")
print(f"\nH0: beta = 1 (Wald test):")
wald_stat = ((uip_model.params['rate_diff'] - 1) /
uip_model.bse['rate_diff']) ** 2
print(f" Chi2 = {wald_stat:.2f}, p = {1 - stats.chi2.cdf(wald_stat, 1):.4f}")A managed exchange rate complicates UIP tests. When the SBV successfully defends the VND within a narrow band, realized exchange rate changes are artificially compressed regardless of interest rate differentials. The resulting beta estimate reflects the credibility of the peg as much as it reflects the risk premium. Periods of active SBV intervention should be treated separately from periods of relative flexibility.
39.5.3 Carry Trade Returns
We construct a simple VND carry trade strategy: borrow USD at the U.S. short rate, convert to VND, invest at the Vietnamese short rate, and convert back at maturity. The excess return is:
\[ \text{Carry}_{t \to t+k} = (r^{VND}_{t,k} - r^{USD}_{t,k}) - \Delta s_{t+k} \tag{39.5}\]
A positive carry return means the interest rate differential more than compensated for any VND depreciation.
carry = cip_data.copy()
carry['carry_return'] = carry['rate_diff'] / 4 - carry['delta_s']
carry = carry.dropna(subset=['carry_return'])
# Annualize
ann_carry = carry['carry_return'].mean() * 4
ann_vol = carry['carry_return'].std() * 2
sharpe = ann_carry / ann_vol
print("VND/USD Carry Trade Performance (3-Month Rolling):")
print(f"Annualized return: {ann_carry:.4f} ({ann_carry*100:.2f}%)")
print(f"Annualized volatility: {ann_vol:.4f} ({ann_vol*100:.2f}%)")
print(f"Sharpe ratio: {sharpe:.2f}")
print(f"Skewness: {carry['carry_return'].skew():.2f}")
print(f"Kurtosis: {carry['carry_return'].kurtosis():.2f}")
print(f"\nMax quarterly loss: {carry['carry_return'].min():.4f}")
print(f"Max quarterly gain: {carry['carry_return'].max():.4f}")fig, axes = plt.subplots(2, 1, figsize=(14, 8), height_ratios=[2, 1])
# Panel A: Cumulative return
cum_carry = (1 + carry['carry_return']).cumprod()
axes[0].plot(carry.index, cum_carry, color='#2C5F8A', linewidth=2)
axes[0].fill_between(carry.index, 1, cum_carry,
where=cum_carry > 1, color='#27AE60', alpha=0.2)
axes[0].fill_between(carry.index, 1, cum_carry,
where=cum_carry < 1, color='#C0392B', alpha=0.2)
axes[0].axhline(y=1, color='gray', linewidth=0.5)
axes[0].set_ylabel('Cumulative Return (VND 1 invested)')
axes[0].set_title('Panel A: VND/USD Carry Trade Cumulative Return')
# Panel B: Quarterly returns
axes[1].bar(carry.index, carry['carry_return'] * 100,
color=['#27AE60' if x > 0 else '#C0392B'
for x in carry['carry_return']],
alpha=0.7, width=60)
axes[1].axhline(y=0, color='black', linewidth=0.5)
axes[1].set_ylabel('Quarterly Return (%)')
axes[1].set_xlabel('Date')
axes[1].set_title('Panel B: Quarterly Carry Returns')
plt.tight_layout()
plt.show()39.6 Volatility Modeling
39.6.1 GARCH Estimation
The strong autocorrelation in squared VND/USD returns (Figure 39.2) motivates GARCH modeling (Bollerslev 1986). We estimate a GARCH(1,1) model for daily VND/USD log returns:
\[ r_t = \mu + \varepsilon_t, \quad \varepsilon_t = \sigma_t z_t, \quad z_t \sim D(0,1) \tag{39.6}\]
\[ \sigma_t^2 = \omega + \alpha \varepsilon_{t-1}^2 + \beta \sigma_{t-1}^2 \tag{39.7}\]
where \(\alpha\) captures the ARCH effect (impact of yesterday’s shock on today’s variance) and \(\beta\) captures persistence (how slowly volatility reverts to its long-run mean). The unconditional variance is \(\bar{\sigma}^2 = \omega / (1 - \alpha - \beta)\).
returns_bps = vnd_usd['log_return'] * 10000 # Scale to basis points
# GARCH(1,1) with normal innovations
garch_norm = arch_model(
returns_bps, vol='Garch', p=1, q=1, dist='normal'
)
res_norm = garch_norm.fit(disp='off')
# GARCH(1,1) with Student-t innovations
garch_t = arch_model(
returns_bps, vol='Garch', p=1, q=1, dist='t'
)
res_t = garch_t.fit(disp='off')
# GJR-GARCH (asymmetric: depreciation shocks may increase vol more)
gjr = arch_model(
returns_bps, vol='Garch', p=1, o=1, q=1, dist='t'
)
res_gjr = gjr.fit(disp='off')
print("=" * 60)
print("GARCH(1,1) - Normal Innovations")
print("=" * 60)
print(res_norm.summary().tables[1])
print("\n" + "=" * 60)
print("GARCH(1,1) - Student-t Innovations")
print("=" * 60)
print(res_t.summary().tables[1])
print("\n" + "=" * 60)
print("GJR-GARCH(1,1,1) - Student-t Innovations")
print("=" * 60)
print(res_gjr.summary().tables[1])
# Model comparison
print("\nModel Comparison:")
print(f"{'Model':<25} {'AIC':>10} {'BIC':>10} {'LogLik':>12}")
for name, res in [('GARCH-Normal', res_norm),
('GARCH-t', res_t),
('GJR-GARCH-t', res_gjr)]:
print(f"{name:<25} {res.aic:>10.1f} {res.bic:>10.1f} "
f"{res.loglikelihood:>12.1f}")fig, ax = plt.subplots(figsize=(14, 6))
# GARCH conditional volatility (annualized, from basis points)
cond_vol = res_t.conditional_volatility / 10000 * np.sqrt(252)
ax.plot(vnd_usd.index[-len(cond_vol):], cond_vol,
color='#C0392B', linewidth=1.2, alpha=0.9,
label='GARCH(1,1) Conditional Vol')
ax.plot(vnd_usd.index, vnd_usd['vol_30d'],
color='#2C5F8A', linewidth=1, alpha=0.6,
label='30-Day Realized Vol')
ax.set_ylabel('Annualized Volatility')
ax.set_xlabel('Date')
ax.set_title('VND/USD Volatility: GARCH vs Realized')
ax.legend()
plt.tight_layout()
plt.show()39.6.2 Volatility Regime Identification
We classify the VND/USD market into volatility regimes using the GARCH conditional volatility:
# Merge conditional volatility back to main DataFrame
vol_series = pd.Series(
cond_vol.values,
index=vnd_usd.index[-len(cond_vol):]
)
vnd_usd['cond_vol'] = vol_series
# Classify regimes based on percentiles
vnd_usd['vol_regime'] = pd.cut(
vnd_usd['cond_vol'],
bins=[0, vnd_usd['cond_vol'].quantile(0.33),
vnd_usd['cond_vol'].quantile(0.67), np.inf],
labels=['Low', 'Medium', 'High']
)
# Regime statistics
regime_stats = (
vnd_usd.groupby('vol_regime')
.agg(
n_days=('log_return', 'count'),
mean_return=('log_return', lambda x: x.mean() * 252),
volatility=('log_return', lambda x: x.std() * np.sqrt(252)),
mean_spread=('spread_pct', 'mean'),
skewness=('log_return', 'skew')
)
.round(4)
)
print("Volatility Regime Characteristics:")
print(regime_stats)fig, ax = plt.subplots(figsize=(10, 6))
colors_regime = {'Low': '#27AE60', 'Medium': '#F1C40F', 'High': '#C0392B'}
for regime in ['Low', 'Medium', 'High']:
subset = vnd_usd[vnd_usd['vol_regime'] == regime]
ax.scatter(
subset['cond_vol'] * 100,
subset['log_return'] * 100,
color=colors_regime[regime], alpha=0.3, s=8, label=regime
)
ax.axhline(y=0, color='gray', linewidth=0.5)
ax.set_xlabel('Conditional Volatility (% annualized)')
ax.set_ylabel('Daily Return (%)')
ax.set_title('VND/USD Returns by Volatility Regime')
ax.legend(title='Regime')
plt.tight_layout()
plt.show()39.7 Exchange Rate Exposure of Vietnamese Firms
39.7.1 The Jorion Framework
Jorion (1990) introduced the standard two-factor model for estimating firm-level exchange rate exposure:
\[ R_{i,t} = \alpha_i + \beta_i^{MKT} R_{m,t} + \gamma_i \Delta s_t + \varepsilon_{i,t} \tag{39.8}\]
where \(R_{i,t}\) is the stock return of firm \(i\), \(R_{m,t}\) is the market return, and \(\Delta s_t\) is the exchange rate change (VND/USD log return, where positive = VND depreciation). The coefficient \(\gamma_i\) is the residual exchange rate exposure, the sensitivity of firm \(i\)’s returns to currency movements after controlling for market-wide effects.
A positive \(\gamma_i\) means the firm benefits from VND depreciation (typical for exporters), while a negative \(\gamma_i\) means the firm is hurt by depreciation (typical for importers or firms with USD-denominated debt).
# Load equity returns
equity = client.get_monthly_returns(
exchanges=['HOSE', 'HNX'],
start_date='2012-01-01',
end_date='2024-12-31',
fields=['ticker', 'month_end', 'monthly_return', 'market_cap']
)
# Market return (VW)
market_ret = (
equity
.groupby('month_end')
.apply(lambda g: np.average(g['monthly_return'],
weights=g['market_cap']))
.reset_index(name='market_return')
)
# Monthly VND/USD returns
fx_monthly = (
vnd_usd['log_return']
.resample('M').sum()
.to_frame('fx_return')
)
fx_monthly.index = fx_monthly.index.to_period('M').to_timestamp()
# Merge all data
exposure_data = (
equity
.merge(market_ret, on='month_end')
.merge(fx_monthly, left_on='month_end', right_index=True, how='inner')
)
# Estimate exposure for each firm
def estimate_exposure(group, min_obs=36):
"""Estimate Jorion exposure model for a single firm."""
if len(group) < min_obs:
return None
y = group['monthly_return']
X = sm.add_constant(group[['market_return', 'fx_return']])
try:
model = sm.OLS(y, X).fit(cov_type='HC1')
return pd.Series({
'gamma': model.params['fx_return'],
'gamma_se': model.bse['fx_return'],
'gamma_t': model.tvalues['fx_return'],
'gamma_p': model.pvalues['fx_return'],
'beta_mkt': model.params['market_return'],
'r_squared': model.rsquared,
'n_obs': model.nobs
})
except Exception:
return None
exposures = (
exposure_data
.groupby('ticker')
.apply(estimate_exposure)
.dropna()
)
print(f"Estimated exposure for {len(exposures)} firms")
print(f"\nExposure (gamma) Summary:")
print(exposures['gamma'].describe().round(4))
print(f"\nSignificant at 5%: "
f"{(exposures['gamma_p'] < 0.05).sum()} / {len(exposures)} "
f"({(exposures['gamma_p'] < 0.05).mean():.1%})")fig, axes = plt.subplots(1, 2, figsize=(14, 5))
# Panel A: Distribution of gamma
sig = exposures['gamma_p'] < 0.05
axes[0].hist(exposures.loc[~sig, 'gamma'], bins=40, color='#BDC3C7',
alpha=0.7, label='Insignificant', edgecolor='white')
axes[0].hist(exposures.loc[sig & (exposures['gamma'] > 0), 'gamma'],
bins=20, color='#27AE60', alpha=0.8,
label='Significant positive', edgecolor='white')
axes[0].hist(exposures.loc[sig & (exposures['gamma'] < 0), 'gamma'],
bins=20, color='#C0392B', alpha=0.8,
label='Significant negative', edgecolor='white')
axes[0].axvline(x=0, color='black', linewidth=0.8)
axes[0].set_xlabel('Exchange Rate Exposure (gamma)')
axes[0].set_ylabel('Number of Firms')
axes[0].set_title('Panel A: Cross-Sectional Distribution')
axes[0].legend(fontsize=9)
# Panel B: Exposure vs market beta
axes[1].scatter(exposures['beta_mkt'], exposures['gamma'],
c=exposures['gamma_t'].clip(-3, 3),
cmap='RdBu_r', s=15, alpha=0.6)
axes[1].axhline(y=0, color='gray', linewidth=0.5)
axes[1].axvline(x=1, color='gray', linewidth=0.5, linestyle='--')
axes[1].set_xlabel('Market Beta')
axes[1].set_ylabel('FX Exposure (gamma)')
axes[1].set_title('Panel B: Market Beta vs FX Exposure')
plt.tight_layout()
plt.show()39.7.2 Industry-Level Exposure
Exchange rate exposure varies systematically across industries. Export-oriented sectors (textiles, seafood, electronics) should have positive exposure (benefiting from VND weakness), while import-dependent sectors (oil and gas, machinery, consumer goods) should have negative exposure.
# Get industry classification
industry = client.get_firm_info(
exchanges=['HOSE', 'HNX'],
fields=['ticker', 'icb_sector', 'icb_industry']
)
exposure_industry = exposures.reset_index().merge(
industry, on='ticker', how='left'
)
# Industry-level average exposure
industry_avg = (
exposure_industry
.groupby('icb_sector')
.agg(
n_firms=('gamma', 'count'),
mean_gamma=('gamma', 'mean'),
median_gamma=('gamma', 'median'),
pct_significant=('gamma_p', lambda x: (x < 0.05).mean())
)
.sort_values('mean_gamma', ascending=False)
.round(3)
)
print("Exchange Rate Exposure by Industry:")
print(industry_avg.to_string())fig, ax = plt.subplots(figsize=(12, 6))
colors_bar = ['#27AE60' if x > 0 else '#C0392B'
for x in industry_avg['mean_gamma']]
bars = ax.barh(range(len(industry_avg)), industry_avg['mean_gamma'],
color=colors_bar, alpha=0.85)
ax.set_yticks(range(len(industry_avg)))
ax.set_yticklabels(industry_avg.index, fontsize=9)
ax.axvline(x=0, color='black', linewidth=0.8)
ax.set_xlabel('Average Exchange Rate Exposure (gamma)')
ax.set_title('Exchange Rate Exposure by Industry')
for i, (_, row) in enumerate(industry_avg.iterrows()):
label = f"({row['pct_significant']:.0%} sig.)"
ax.text(row['mean_gamma'] + 0.01 * np.sign(row['mean_gamma']),
i, label, va='center', fontsize=8, color='gray')
plt.tight_layout()
plt.show()39.7.3 Time-Varying Exposure
Exchange rate exposure is not constant, firms adjust hedging strategies, trade patterns shift, and the exchange rate regime itself evolves. We estimate rolling 36-month exposures:
def rolling_exposure(group, window=36, min_obs=24):
"""Estimate rolling FX exposure with 36-month windows."""
results = []
group = group.sort_values('month_end')
for i in range(window, len(group) + 1):
sub = group.iloc[i - window:i]
if len(sub) < min_obs:
continue
y = sub['monthly_return']
X = sm.add_constant(sub[['market_return', 'fx_return']])
try:
model = sm.OLS(y, X).fit()
results.append({
'month_end': sub['month_end'].iloc[-1],
'gamma': model.params['fx_return'],
'gamma_se': model.bse['fx_return']
})
except Exception:
pass
return pd.DataFrame(results)
# Compute for a sample of large firms
large_firms = (
equity.groupby('ticker')['market_cap']
.last().nlargest(20).index
)
rolling_results = {}
for ticker in large_firms:
firm_data = exposure_data[exposure_data['ticker'] == ticker]
result = rolling_exposure(firm_data)
if len(result) > 0:
rolling_results[ticker] = result
print(f"Rolling exposures computed for {len(rolling_results)} firms")fig, ax = plt.subplots(figsize=(14, 6))
plot_colors = plt.cm.Set1(np.linspace(0, 1, min(6, len(rolling_results))))
for i, (ticker, df) in enumerate(list(rolling_results.items())[:6]):
ax.plot(pd.to_datetime(df['month_end']), df['gamma'],
linewidth=1.5, label=ticker, color=plot_colors[i])
if i == 0:
ax.fill_between(
pd.to_datetime(df['month_end']),
df['gamma'] - 1.96 * df['gamma_se'],
df['gamma'] + 1.96 * df['gamma_se'],
alpha=0.1, color=plot_colors[i]
)
ax.axhline(y=0, color='gray', linewidth=0.8)
ax.set_ylabel('Exchange Rate Exposure (gamma)')
ax.set_xlabel('Date')
ax.set_title('Rolling 36-Month Exchange Rate Exposure')
ax.legend(ncol=3, fontsize=9)
plt.tight_layout()
plt.show()39.8 Currency Hedging for International Investors
39.8.1 The Hedging Decision
For a foreign investor holding Vietnamese equities, the unhedged USD-denominated return is:
\[ R^{USD}_{i,t} \approx R^{VND}_{i,t} + \Delta s_t + R^{VND}_{i,t} \cdot \Delta s_t \tag{39.9}\]
where the cross-product term is usually small. Under a full hedge using forward contracts, the return becomes:
\[ R^{USD,\text{hedged}}_{i,t} \approx R^{VND}_{i,t} + (f_t - s_t) \tag{39.10}\]
where \(f_t - s_t\) is the forward premium (which, by CIP, approximately equals \(r^{USD}_t - r^{VND}_t\)). In Vietnam, where VND rates exceed USD rates, the forward premium is negative, hedging costs money because the investor forgoes the interest rate differential.
Campbell, Serfaty-De Medeiros, and Viceira (2010) show that the optimal hedge ratio depends on the correlation between equity returns and currency returns. When the correlation is positive (VND depreciation coincides with equity losses), hedging reduces overall portfolio variance more effectively.
# Construct VW equity index in VND
equity_index = (
equity
.groupby('month_end')
.apply(lambda g: np.average(g['monthly_return'],
weights=g['market_cap']))
.reset_index(name='equity_vnd')
)
equity_index['month_end'] = pd.to_datetime(equity_index['month_end'])
# Merge with FX returns and forward premium
hedge_data = (
equity_index
.merge(fx_monthly, left_on='month_end', right_index=True, how='inner')
)
# Forward premium
if len(cip_data) > 0:
fwd_premium_monthly = (
cip_data['forward_premium']
.resample('M').last() / 12
)
hedge_data = hedge_data.merge(
fwd_premium_monthly.to_frame('fwd_premium'),
left_on='month_end', right_index=True, how='left'
)
hedge_data['fwd_premium'] = hedge_data['fwd_premium'].fillna(
hedge_data['fwd_premium'].mean()
)
else:
hedge_data['fwd_premium'] = -0.003
# Unhedged USD return
hedge_data['return_unhedged'] = (
hedge_data['equity_vnd'] + hedge_data['fx_return']
+ hedge_data['equity_vnd'] * hedge_data['fx_return']
)
# Fully hedged USD return
hedge_data['return_hedged'] = (
hedge_data['equity_vnd'] + hedge_data['fwd_premium']
)
# 50% hedge ratio
hedge_data['return_50pct'] = (
0.5 * hedge_data['return_hedged']
+ 0.5 * hedge_data['return_unhedged']
)
# Performance comparison
for strategy, col in [('VND (Local)', 'equity_vnd'),
('USD Unhedged', 'return_unhedged'),
('USD 50% Hedged', 'return_50pct'),
('USD Fully Hedged', 'return_hedged')]:
r = hedge_data[col]
ann_ret = (1 + r).prod() ** (12 / len(r)) - 1
ann_vol = r.std() * np.sqrt(12)
sharpe = r.mean() / r.std() * np.sqrt(12)
print(f"{strategy:<22} Return: {ann_ret:>7.2%} Vol: {ann_vol:>7.2%} "
f"Sharpe: {sharpe:>5.2f}")fig, ax = plt.subplots(figsize=(12, 6))
strategies = {
'VND (Local)': ('equity_vnd', '#2C5F8A', '-'),
'USD Unhedged': ('return_unhedged', '#C0392B', '-'),
'USD 50% Hedged': ('return_50pct', '#E67E22', '--'),
'USD Fully Hedged': ('return_hedged', '#27AE60', '-'),
}
for label, (col, color, ls) in strategies.items():
cum = (1 + hedge_data.set_index('month_end')[col]).cumprod()
ax.plot(cum.index, cum, label=label, color=color,
linewidth=2, linestyle=ls)
ax.set_ylabel('Cumulative Wealth')
ax.set_xlabel('Date')
ax.set_title('Vietnamese Equity Returns: Hedging Comparison')
ax.legend()
ax.set_yscale('log')
plt.tight_layout()
plt.show()39.8.2 Optimal Hedge Ratio
The minimum-variance hedge ratio is:
\[ h^* = \frac{\text{Cov}(R^{VND}_t, \Delta s_t)}{\text{Var}(\Delta s_t)} \tag{39.11}\]
This is the OLS slope from regressing local equity returns on exchange rate changes. When the correlation between equity and currency is positive (bad for unhedged investors), \(h^* > 0\) and hedging reduces variance.
# Full-sample optimal hedge ratio
hedge_reg = sm.OLS(
hedge_data['equity_vnd'],
sm.add_constant(hedge_data['fx_return'])
).fit()
h_star = hedge_reg.params['fx_return']
print(f"Optimal hedge ratio (full sample): {h_star:.3f}")
print(f"Equity-currency correlation: "
f"{hedge_data['equity_vnd'].corr(hedge_data['fx_return']):.3f}")
# Rolling 36-month optimal hedge ratio
rolling_cov = hedge_data['equity_vnd'].rolling(36).cov(
hedge_data['fx_return']
)
rolling_var = hedge_data['fx_return'].rolling(36).var()
hedge_data['h_star_rolling'] = rolling_cov / rolling_var
print(f"\nRolling hedge ratio range: "
f"[{hedge_data['h_star_rolling'].min():.2f}, "
f"{hedge_data['h_star_rolling'].max():.2f}]")39.9 ASEAN Currency Comparison
To put the VND in perspective, we compare its properties with other ASEAN currencies and the Chinese Yuan.
# Compute annual statistics for each currency
comparison = pd.DataFrame()
for currency in fx_returns.columns:
r = fx_returns[currency].dropna()
if len(r) > 252:
comparison.loc[currency, 'Ann. Depreciation (%)'] = r.mean() * 252 * 100
comparison.loc[currency, 'Ann. Volatility (%)'] = r.std() * np.sqrt(252) * 100
comparison.loc[currency, 'Skewness'] = r.skew()
comparison.loc[currency, 'Kurtosis'] = r.kurtosis()
comparison.loc[currency, 'Max Daily Loss (%)'] = r.max() * 100
comparison.loc[currency, 'Sharpe (Carry)'] = (
r.mean() * 252 / (r.std() * np.sqrt(252))
)
print("ASEAN + Asian Currency Comparison (VND base, vs USD):")
print(comparison.round(2).to_string())
# Risk-return scatter
fig, ax = plt.subplots(figsize=(9, 7))
for currency in comparison.index:
ax.scatter(
comparison.loc[currency, 'Ann. Volatility (%)'],
comparison.loc[currency, 'Ann. Depreciation (%)'],
s=120, zorder=5
)
ax.annotate(
f'VND/{currency}',
(comparison.loc[currency, 'Ann. Volatility (%)'] + 0.15,
comparison.loc[currency, 'Ann. Depreciation (%)']),
fontsize=10
)
ax.axhline(y=0, color='gray', linewidth=0.5)
ax.set_xlabel('Annualized Volatility (%)')
ax.set_ylabel('Annualized Depreciation (%)')
ax.set_title('ASEAN Currency Risk-Return Characteristics')
plt.tight_layout()
plt.show()39.10 Exchange Rate and Equity Market Co-Movement
The relationship between VND/USD movements and the Vietnamese equity market is central to portfolio construction. We examine this at both the aggregate and conditional levels.
fig, axes = plt.subplots(1, 2, figsize=(14, 5))
# Panel A: Scatter plot
axes[0].scatter(
hedge_data['fx_return'] * 100,
hedge_data['equity_vnd'] * 100,
c=hedge_data.index, cmap='viridis', s=20, alpha=0.7
)
z = np.polyfit(hedge_data['fx_return'], hedge_data['equity_vnd'], 1)
x_line = np.linspace(hedge_data['fx_return'].min(),
hedge_data['fx_return'].max(), 100)
axes[0].plot(x_line * 100, np.polyval(z, x_line) * 100,
color='#C0392B', linewidth=2)
axes[0].axhline(y=0, color='gray', linewidth=0.5)
axes[0].axvline(x=0, color='gray', linewidth=0.5)
axes[0].set_xlabel('VND/USD Change (%)')
axes[0].set_ylabel('VN-Index Return (%)')
axes[0].set_title('Panel A: Monthly Equity-FX Relationship')
rho = hedge_data['equity_vnd'].corr(hedge_data['fx_return'])
axes[0].text(0.05, 0.95, f'rho = {rho:.3f}', transform=axes[0].transAxes,
fontsize=12, verticalalignment='top',
bbox=dict(boxstyle='round', facecolor='white', alpha=0.8))
# Panel B: Rolling 36-month correlation
rolling_corr = (
hedge_data['equity_vnd']
.rolling(36)
.corr(hedge_data['fx_return'])
)
axes[1].plot(hedge_data['month_end'], rolling_corr,
color='#2C5F8A', linewidth=2)
axes[1].axhline(y=0, color='gray', linewidth=0.8, linestyle='--')
axes[1].fill_between(
hedge_data['month_end'], 0, rolling_corr,
where=rolling_corr < 0, color='#C0392B', alpha=0.2
)
axes[1].fill_between(
hedge_data['month_end'], 0, rolling_corr,
where=rolling_corr > 0, color='#27AE60', alpha=0.2
)
axes[1].set_xlabel('Date')
axes[1].set_ylabel('Rolling Correlation')
axes[1].set_title('Panel B: 36-Month Rolling Equity-FX Correlation')
plt.tight_layout()
plt.show()39.11 Summary
This chapter has developed a comprehensive toolkit for working with exchange rate data in the Vietnamese context. The key findings and methods are in Table 39.2.
| Topic | Finding | Implication |
|---|---|---|
| VND regime | Managed float with +/-3% band, daily reference rate | Returns are non-normal; clustered near zero with discrete jumps |
| Return distribution | Fat tails, excess kurtosis, positive skewness | Standard normal-based risk models underestimate tail risk |
| CIP | Persistent positive deviations (~50-200 bps) | Capital controls and limited arbitrage; hedging is expensive |
| UIP | beta < 1 in Fama regression | Carry trade is profitable on average; crash risk in devaluations |
| Carry trade | Positive Sharpe but negative skewness | Steady gains punctuated by sharp losses during devaluations |
| GARCH | Strong persistence (alpha + beta approx 0.99); Student-t preferred | Volatility clustering; regime switches around policy events |
| Firm exposure | ~15-25% significant; systematic industry patterns | Exporters benefit from depreciation; importers hurt |
| Hedging | Full hedge eliminates FX risk but costs ~3-5% p.a. | Optimal partial hedge depends on equity-FX correlation |
| ASEAN comparison | VND has lowest volatility but steady depreciation | Managed float compresses vol; does not eliminate trend risk |
For researchers using Vietnamese equity data, the exchange rate dimension is not optional. Any study that converts VND returns to USD, uses international factor models, or examines firms with trade exposure must account for the distinctive properties of the VND regime. The tools developed in this chapter, from GARCH volatility modeling to rolling exposure estimation, provide the building blocks for incorporating exchange rate dynamics into broader empirical asset pricing work.