返回博客

如何计算货币波动率:开发者指南

V
Vlado Grigirov
September 07, 2026
Currency API Exchange Rates Volatility Historical Data Python Risk Management

掌握如何计算货币波动率,决定了你的多币种应用是能悄无声息地消化隔夜 2% 的汇率变动,还是会在凌晨三点把你叫醒。波动率是唯一一个能告诉你汇率可能走多远的数字 —— 而在一个对外汇敏感的系统里,几乎每一项实际决策都依赖于它:一个报价能保持有效多久、价格上要预留多少缓冲、什么时候对冲、什么时候告警。

麻烦的是,大多数讲波动率计算的教程都是为股票写的。它们假设有每日收盘价、一年 252 个交易日,以及一个明确无歧义的报价。外汇一个都不占。它每周五天、每天 24 小时交易,运行在一个没有官方收盘的去中心化场外市场上,而且 EUR/USD 的"价格"取决于你从哪个方向报价。

本文将带你走一遍真正的数学推导、外汇特有的陷阱,以及可直接运行的 Python 和 JavaScript 代码 —— 用历史汇率数据计算已实现波动率。

为什么现在要关注货币波动率

2026 年 9 月是思考波动率的绝佳时机,因为今年市场大部分时间都处在一种安静得可疑的状态 —— 然后又短暂地不安静了一下。

整个夏天,新兴市场货币的波动率比 G7 货币还要,这一状态持续了将近 200 个连续交易日;彭博社指出,若能延续下去,这将是 2000 年以来最长的一次。以美元融资的套息交易创下了 2008 年以来最长的连胜纪录;而一个流行的、以欧元融资并由巴西雷亚尔、哥伦比亚比索和土耳其里拉组成的组合,据报道年初至今上涨约 19% —— 为 2005 年以来最强的年内表现。套息策略只有在平静的市场中才奏效,因此它们的表现本身就是一个波动率信号。

随后在 9 月初,由于交易员押注日本财务省会进一步干预,日元在单个交易日内对美元跳涨超过 2%,触及 155.28,为 8 月初以来的最强水平。在此之前,美日曾于 7 月 31 日联合干预;而在 USD/JPY 突破 160 之后,财务省又在 4 月和 5 月合计投入了创纪录的 11.73 万亿日元(约合 730 亿美元)进行干预。

这种反差就是全部要点。平均波动率很低。而某一天的已实现波动率却极其巨大。 一个按过去一年平均值来设定外汇缓冲的系统,恰恰会在最关键的那一天错得离谱。正确且持续地测量波动率,才能避免这种情况。

货币波动率究竟衡量什么

波动率是收益率的标准差,以年化形式表示。它不是对方向的预测,也不是变动的幅度。它衡量的是离散程度:一个货币对的每日收益率围绕其均值分散得有多开。

有两种类型值得关注:

  • 已实现(历史)波动率 —— 根据实际观测到的汇率反推计算。这是你可以自己从时间序列里算出来的,也是本文要讲的内容。
  • 隐含波动率 —— 从外汇期权的价格中提取。它是市场对未来的预期估计。你无法从即期汇率推导出它,需要期权数据源。

对大多数应用开发者来说 —— 定价、计费、资金管理仪表盘、风险告警 —— 已实现波动率既够用,而且只要你手上已经有历史汇率,计算它就是免费的。

对数量级的一个粗略直觉:像 EUR/USD 这样的主要货币对,在平静时期年化波动率通常落在 5–10%,承压时期则是 10–15%。新兴市场货币对通常要高出两到三倍。这些区间会随时间移动,而这恰恰就是你应该去测量、而不是想当然的原因。

公式:对数收益率、标准差、年化

计算分三步。

第 1 步 —— 把汇率转换为对数收益率。

r_t = ln(P_t / P_t-1)

第 2 步 —— 在你选定的窗口上,对这些收益率取标准差(使用样本标准差,即除以 n − 1)。

第 3 步 —— 年化,方法是乘以每年观测次数的平方根。

annualised_volatility = stdev(r) * sqrt(N)

合起来:

σ_annual = stdev( ln(P_t / P_t-1) ) × √N

为什么用对数收益率而不是百分比变化

简单的百分比变化是不对称的:从 1.10 涨到 1.20 是 +9.09%,但从 1.20 回到 1.10 却是 −8.33%。这两者本应大小相等、方向相反。对数收益率就做到了这一点 —— ln(1.20/1.10)ln(1.10/1.20) 只差一个符号。

这一点在外汇里比在股票里重要得多,原因是报价方向反转。EUR/USD 和 USD/EUR 描述的是同一个市场。如果你用百分比变化计算波动率,就会因为报价方向不同而得到两个略有差异的答案,这显然是错的。对数收益率对反转是对称的:ln(1/x) = −ln(x),而标准差会忽略符号。同一个货币对,无论哪个方向,波动率相同。

对数收益率在时间上还是可加的,这让多日聚合变得非常简单。

为什么是 √N —— 以及外汇里的 N 该取多少

波动率随时间的平方根缩放,因为方差随时间线性增长(在标准随机游走假设下)。要把日波动率换算成年化,就乘以 √(每年天数)

股票教程用的是 252,即美国交易所交易日的大致数量。外汇不一样。市场从周日晚上到周五晚上连续运行 —— 每年大约 260 个工作日交易时段,再减去少数几个成交清淡的假日。大多数外汇数据提供商在每个工作日都会发布汇率,包括某个国家自己的市场休市的日子。

实用规则:N 必须匹配你数据的采样频率,而不是从其他资产类别借来的惯例。

数据频率N(年化因子)
每日(工作日,外汇惯例)260
每日(252 天股票惯例)252
每周52
每月12
每小时(24×5)约 6,240
252 和 260 之间的差距会让结果相差约 1.6%(相对值)—— 对仪表盘来说无关紧要,但如果你要拿自己的数字和供应商的对比,那就有影响了。无论选哪个,都要写进文档并保持不变。

第 1 步:获取干净的历史汇率数据

波动率的质量取决于它底层的那条序列。用一次对 /v1/timeseries 端点的调用就能拉取每日序列:

curl -G "https://api.finexly.com/v1/timeseries" \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -d base=EUR \
  -d symbols=USD,JPY,GBP \
  -d start_date=2026-06-08 \
  -d end_date=2026-09-05

响应是一个以日期为键的映射:

{
  "success": true,
  "base": "EUR",
  "start_date": "2026-06-08",
  "end_date": "2026-09-05",
  "rates": {
    "2026-06-08": { "USD": 1.1421, "JPY": 178.62, "GBP": 0.8447 },
    "2026-06-09": { "USD": 1.1408, "JPY": 178.94, "GBP": 0.8451 }
  }
}

一次请求,一条序列,不需要分页循环。完整的参数说明见 Finexly API 文档,处理日期范围时更通用的模式则可以参考我们的历史汇率 API 指南

外汇特有的四个数据问题

在你信任输出结果之前,先把这些问题处理掉。它们在股票波动率教程里一个都不会出现,而这四个都会扭曲你的数字。

1. 没有官方收盘价。 外汇是去中心化的,没有人敲收盘钟。不同的提供商在不同时刻取快照 —— UTC 22:00、UTC 午夜、欧洲央行 CET 14:15 的参考定盘价,或者伦敦下午 4 点的 WM/Reuters 定盘价。在同一条序列里混用不同的快照时间,会引入人为的方差。选定一种惯例,绝不混用。如果你不确定自己的提供商是怎么做的,我们那篇关于汇率 API 的数据从哪里来的说明是个不错的起点。

2. 周末与数据缺口。 市场周五晚间收市,周日晚间重新开市。周五→周一的收益率覆盖了大约 65 小时的日历时间,但在朴素的计算里却被当成一个"交易日"。大多数从业者接受这一点,并按工作日数量做年化 —— 这是标准做法 —— 但你必须保持一致。不要把周末的汇率向前填充到序列里;一连串相同的数值会人为地把标准差拉低。

3. 基准货币的不对称性。 如上所述,对数收益率在数学上解决了反转问题。但你仍然需要决定,自己是在用哪种基准货币衡量风险。以欧元计量的美元敞口波动率,和以美元计量的同一货币对,是两个不同的业务问题,尽管算出来的数字完全一样。要明确你的计价货币。

4. 三角套算的交叉汇率。 如果你的提供商是用 SEK/USD 除以 NOK/USD 来推导 SEK/NOK,那么结果会同时继承两条腿的噪声,以及两者之间任何时间上的错配。合成交叉汇率的波动率,可能明显高于直接报价的市场。我们的交叉汇率指南解释了三角套算什么时候安全、什么时候不安全。

另外要注意精度。在计算收益率之前把 JPY 货币对截断到两位小数,会把小幅变动量化成零,从而使波动率被低估 —— 一般规则参见货币舍入与小数位数。存储时保留完整精度,只在展示时才四舍五入。

第 2 步:用 Python 计算已实现波动率

下面是一个完整、依赖极少的实现。它只用到 requests 和标准库 —— 不需要 pandas。

import math
import statistics
import requests

FX_DAYS_PER_YEAR = 260  # weekday sessions; use 252 for the equity convention


def fetch_series(base, symbol, start, end, api_key):
    url = "https://api.finexly.com/v1/timeseries"
    params = {
        "base": base,
        "symbols": symbol,
        "start_date": start,
        "end_date": end,
    }
    headers = {"Authorization": "Bearer " + api_key}
    response = requests.get(url, params=params, headers=headers, timeout=10)
    response.raise_for_status()
    return response.json()["rates"]


def log_returns(rates, symbol):
    """Ordered log returns from a date-keyed rates map."""
    dates = sorted(rates.keys())
    series = [rates[d][symbol] for d in dates]
    return [
        math.log(series[i] / series[i - 1])
        for i in range(1, len(series))
        if series[i] > 0 and series[i - 1] > 0
    ]


def annualised_volatility(returns, periods_per_year=FX_DAYS_PER_YEAR):
    if len(returns) < 2:
        raise ValueError("need at least two returns")
    return statistics.stdev(returns) * math.sqrt(periods_per_year)


rates = fetch_series("EUR", "USD", "2026-06-08", "2026-09-05", "YOUR_API_KEY")
returns = log_returns(rates, "USD")

vol = annualised_volatility(returns)
print("Observations:      {}".format(len(returns)))
print("Daily volatility:  {:.4%}".format(vol / math.sqrt(FX_DAYS_PER_YEAR)))
print("Annual volatility: {:.2%}".format(vol))

statistics.stdev样本标准差(分母为 n − 1),这正是你想要的。如果你改用 NumPy,记住 numpy.std 默认是总体版本 —— 传入 ddof=1 才能对上。

生产代码里有两道防线值得保留:取对数之前先跳过非正的汇率;以及当窗口内观测值太少时,拒绝返回结果。用九个数据点算出来的 30 日波动率,不是 30 日波动率。

第 3 步:同样的计算,用 JavaScript 实现

这里同样不需要任何数值计算库。

const FX_DAYS_PER_YEAR = 260;

async function fetchSeries(base, symbol, start, end, apiKey) {
  const url = new URL('https://api.finexly.com/v1/timeseries');
  url.searchParams.set('base', base);
  url.searchParams.set('symbols', symbol);
  url.searchParams.set('start_date', start);
  url.searchParams.set('end_date', end);

  const res = await fetch(url, {
    headers: { Authorization: `Bearer ${apiKey}` },
  });
  if (!res.ok) throw new Error(`Finexly returned ${res.status}`);
  const body = await res.json();
  return body.rates;
}

function logReturns(rates, symbol) {
  const dates = Object.keys(rates).sort();
  const series = dates.map((d) => rates[d][symbol]);
  const out = [];
  for (let i = 1; i < series.length; i += 1) {
    if (series[i] > 0 && series[i - 1] > 0) {
      out.push(Math.log(series[i] / series[i - 1]));
    }
  }
  return out;
}

function annualisedVolatility(returns, periodsPerYear = FX_DAYS_PER_YEAR) {
  if (returns.length < 2) throw new Error('need at least two returns');
  const mean = returns.reduce((a, b) => a + b, 0) / returns.length;
  const variance =
    returns.reduce((acc, r) => acc + (r - mean) ** 2, 0) / (returns.length - 1);
  return Math.sqrt(variance) * Math.sqrt(periodsPerYear);
}

const rates = await fetchSeries('EUR', 'USD', '2026-06-08', '2026-09-05', 'YOUR_API_KEY');
const vol = annualisedVolatility(logReturns(rates, 'USD'));
console.log(`Annualised volatility: ${(vol * 100).toFixed(2)}%`);

注意 (returns.length - 1) 这个分母 —— 和 Python 版本一样的样本方差修正。

滚动波动率与识别市场状态切换

固定窗口上的单个数字,几乎无法告诉你市场是什么时候开始动荡的。滚动波动率可以,而且它才是你真正想放到仪表盘上的版本。

def rolling_volatility(rates, symbol, window=30,
                       periods_per_year=FX_DAYS_PER_YEAR):
    dates = sorted(rates.keys())
    series = [rates[d][symbol] for d in dates]
    returns = [math.log(series[i] / series[i - 1]) for i in range(1, len(series))]
    return_dates = dates[1:]

    out = []
    for i in range(window - 1, len(returns)):
        chunk = returns[i - window + 1:i + 1]
        out.append((
            return_dates[i],
            statistics.stdev(chunk) * math.sqrt(periods_per_year),
        ))
    return out


for date, vol in rolling_volatility(rates, "USD", window=30)[-5:]:
    print("{}  {:.2%}".format(date, vol))

窗口的选择是一种权衡,而不是既定事实:

  • 10 天 —— 非常灵敏,也非常嘈杂。适合告警,不适合定价。
  • 30 天 —— 常见的默认值。几个交易日内就会作出反应,但不会来回乱跳。
  • 90 天 —— 平滑而稳定。适合设定对冲比例和定价缓冲。

一个实用的状态切换信号是波动率比值:用 10 日波动率除以 90 日波动率。持续高于大约 1.5,就意味着短期离散程度已经明显跑赢了较长期的基准。把它应用到前文提到的日元干预事件前后,这个比值会急剧飙升,而 90 日的数字几乎纹丝不动 —— 这恰恰说明了问题。长窗口会掩盖事件,短窗口才能发现事件。

如果你打算在许多货币对上持续运行这套计算,请先读一读我们关于货币 API 的缓存与错误处理的笔记。历史每日汇率一经发布就不会再变,因此可以无限期缓存 —— 只需重新计算最新的那个窗口,而不必在每次页面加载时重新拉取一整年的历史数据。定价方案页面展示了各档套餐的请求量。

把波动率数字转化为决策

计算波动率很容易。把它用好才是价值所在。四个具体的应用:

  1. 报价有效期窗口。 你展示给客户的价格,本质上是你写给他们的一份短期期权。在 t 天的持有期内,预期漂移大约是 σ_annual × √(t / 260)。在 8% 的年化波动率下,24 小时的报价大约承担 0.5% 的预期漂移;在 20% 时则接近 1.2%。当波动率上升时应缩短 TTL,而不是永远固定用 15 分钟。
  1. 定价缓冲。 与其把外汇加价硬编码成 2%,不如让它按比例缩放:buffer = k × σ_annual × √(settlement_days / 260)。缓冲会在承压环境下自动放宽,在市场平静时自动收紧,这既能让你保持竞争力,又不会承担隐性风险。
  1. 对冲触发条件。 光有敞口不等于风险;敞口乘以波动率才是。对一个波动率 4% 的货币对持有 200 万美元敞口,比在一个波动率 30% 的货币对上持有 40 万美元问题更小。对冲决策应该按 exposure × σ 排序,而不是按名义金额。我们的货币对冲指南介绍了各种工具;而波动率数字会告诉你哪些敞口值得对冲。
  1. 异常检测。 把每一次新的日度变动,换算成相对于过去 30 天标准差的 z 分数,超过 ±3 就告警。这既能捕捉真实的市场事件,也能捕捉坏数据 —— 一个过期的数据源,或者一个三角套算出错的交叉汇率,通常会先以一个不合理的离群值暴露出来,然后才会有人在报表里发现它。

如果你想用面向交易员的单位而不是百分比来表达这些变动,可以看看什么是点(pip)

常见错误

  • 用百分比变化代替对数收益率。 会引入不对称性,并让你的答案取决于报价方向。
  • 使用总体标准差。 不加 ddof=1numpy.std() 会低估波动率,在短窗口上尤其严重。
  • 对周末和假日做向前填充。 重复的数值就是零收益日,会机械地把波动率拉低。
  • 用错误的 N 做年化。 周度数据如果用 260 而不是 52 来年化,得到的数字大约会大 2.2 倍。
  • 比较来自不同快照时间的波动率。 两个提供商、两个定盘时间,就是两个不可比的数字。
  • 只报一个年度数字,就把它当作风险。 波动率会聚集。平静的月份和剧烈的周次被平均成一个不上不下的数字,两者都描述不了。务必在这个头条数值旁边同时发布一条滚动序列。
  • 以为低波动率就意味着低风险。 2026 年那些适合套息的平静时期,一直平静到干预的新闻标题让日元在一个交易日内变动 2% 为止。低已实现波动率描述的是刚刚过去的历史,而不是对下周的承诺。

常见问题

一个货币对的正常波动率水平是多少?

像 EUR/USD 和 GBP/USD 这样的主要货币对,历史上在平静环境下年化波动率大约为 5–10%,承压时为 10–15%。新兴市场货币对通常要高出两到三倍。与其依赖经验法则,不如为你关心的货币对计算 90 日滚动波动率,用你自己的基准 —— 这些水平会随市场状态变化。

年化外汇波动率时,该用 252 还是 260 个交易日?

260 更合适,因为外汇市场在每个工作日都交易,而不是遵循某一家交易所的日历。252 是股票惯例,不过也被广泛使用。两者相差约 1.6%(相对值),所以关键在于选定一个、写进文档,并在所有需要对比数字的地方一致地使用它。

我能用汇率 API 计算隐含波动率吗?

不能。隐含波动率是从外汇期权价格反推出来的,需要期权数据源。你能从即期或历史汇率序列中算出来的是已实现波动率。对于应用代码里的定价、对冲阈值和告警来说,已实现波动率通常就是合适的度量。

我需要多少历史数据?

最少要比窗口长度多一个观测值 —— 但那样得到的估计非常不稳定。作为实用下限,30 日窗口就用 30 个观测值,并且至少拉取一年的历史数据,这样你才能把当前读数和更长期的基准作比较。/v1/timeseries 端点可以在一次请求中返回任意日期范围。

为什么我算出的波动率和数据提供商的不一样?

几乎总是以下四个原因之一:年化因子不同(252 对 260)、用了总体标准差而不是样本标准差、每日汇率的快照时间不同,或者用了简单收益率而不是对数收益率。在断定数据有问题之前,先检查这四点。

波动率能预测货币会往哪个方向走吗?

不能。波动率衡量的是离散程度,不是方向。一个年化波动率 20% 的货币对,预计会波动很大;但这个计算完全没有说它是涨还是跌。用它来度量风险的大小,而不是用来判断方向。


准备好在实时数据上计算波动率了吗?免费获取 Finexly API 密钥 —— 无需信用卡。你将获得 170 多种货币的实时和历史汇率、每月 1,000 次免费请求,以及一个能在一次调用中返回完整日期范围的 /v1/timeseries 端点。可以先从免费货币汇率 API 开始,再随着数据需求的增长升级。

Vlado Grigirov

Senior Currency Markets Analyst & Financial Strategist

Vlado Grigirov is a senior currency markets analyst and financial strategist with over 14 years of experience in foreign exchange markets, cross-border finance, and currency risk management. He has wo...

View full profile →