返回博客

税务申报中的汇率:面向开发者的 IRS、HMRC 与欧盟增值税换算指南

V
Vlado Grigirov
August 20, 2026
Currency API Exchange Rates Historical Rates Tax Reporting VAT Accounting Finexly

每一套多币种系统最终都会遇到会计。他们会提一个听起来微不足道、实则棘手的问题:"这张发票你用的是哪个汇率?" 如果诚实的回答是"那天下午 API 返回什么就是什么,而且我们没留存",那么你面对的问题,再干净的代码到了申报季也救不回来。

税务申报中的汇率做对,重点并不在于挑出那个正确的汇率——多数税务机关在这一点上出人意料地宽松——而在于多年之后你仍能证明自己用了哪个汇率、它来自哪里,以及同一期间内的每一笔交易你都套用了同一条规则。这是一个数据建模问题,也恰恰是没人写的那部分。

本指南梳理 IRS、HMRC 和欧盟增值税指令的真实要求、会演变成财报重述的五个换算 bug,以及一套本周就能落地的汇率快照表结构。

没人对"那个"汇率有共识——而这正是关键

这整个领域里最有用的一句话来自 IRS 自己:

"美国国税局没有官方汇率。一般而言,只要某个公布的汇率被一贯地使用,国税局都予以接受。"

请读两遍,因为几乎每个法域都是同样的套路。义务很少是必须用这个特定数字,而是使用一个站得住脚的来源,并且一贯地使用它。一贯性是你系统的属性,不是汇率供应商的属性。如果你的代码在周末悄悄回退到另一个数据源,那么即便你从未取到过错误的数字,你也已经违反了这项要求。

美国:默认即期汇率,年度平均汇率作为让步

IRS 的基准是即期汇率:"一般而言,应使用你收取、支付或权责发生该项目时的现行汇率(即即期汇率)。" 对于持续稳定产生的收入——工资、租金、经常性经营收入——IRS 发布年度平均货币汇率表,并指示申报人"将外币金额除以适用的年度平均汇率。" 截至该页面最后一次更新的 2026 年 2 月 24 日,该表覆盖 2021 至 2025 纳税年度。

请注意这一运算的方向。IRS 的表格以每 1 美元折合多少外币单位报价,所以你要做除法。一旦搞反,你不是稍微算错——你错的是汇率的平方。对日元金额而言,这大约相差四个数量级。关于汇率方向的更多内容见下文,因为它是本领域最常见的单个集成 bug。

美国联邦机构报送还有第二套官方序列:美国财政部报送汇率(Treasury Reporting Rates of Exchange)每季度FiscalData.Treasury.gov 以 CSV、JSON 和 XML 格式发布。财政部将其描述为反映"美国政府为官方支出取得外币时可适用的汇率,由各驻外机构的出纳官按发布报告日期前一个月最后一个营业日报送。" 若实时汇率偏离已发布汇率达 10% 或以上,财政部会在季度中途发布修订。Fiscal Data API 是开放的,无需账户或令牌——如果你需要一套政府来源的参考序列来做对账,这一点值得记住。

英国:VAT Notice 700 §7.6 具有法律效力

英国的规定更具强制性,相关文本依据 1994 年增值税法附表 6 第 11 段具有法定效力。VAT Notice 700 为企业提供了三条将外币供应折算为英镑的路径:

  1. 供应发生时的英国市场卖出汇率。 这是默认选项。该公告指出"全国性报纸上公布的汇率,可以作为相关时点汇率的可接受证据。"
  2. HMRC 期间汇率,为海关目的而发布。你可以"对你的全部供应,或对某一特定类别或描述的全部供应"采用该汇率。无需事先通知——但"一经作出此项选择,未先以书面形式取得 VAT Written Enquiries Team 的同意,便不得再行变更。"
  3. 自行确定的商业汇率或方法,这需要提交书面申请。HMRC 会衡量该汇率是否"参照英国货币市场确定"、是否"可客观验证",以及更新频率如何。关键的一点是,"远期汇率或由远期汇率导出的方法不予接受"——这是一条硬性边界,值得结合即期汇率与远期汇率的区别一并理解。

还有一句直接管住你缓存层的话:"无论你采用何种汇率或方法,任何一项供应的适用汇率均为该供应发生时的现行汇率。" 是供应时点,不是开票时点,不是付款时点,当然更不是你夜间批处理任务的时点。

如果你走路径 2,其机制对机器相当友好。HMRC 在每月倒数第二个星期四发布月度汇率;这些汇率适用于下一个日历月,代表发布前一天中午的汇率。文件位于可预测的 URL——注意月份没有补零:

https://www.trade-tariff.service.gov.uk/exchange_rates/view/files/monthly_csv_2026-9.csv
https://www.trade-tariff.service.gov.uk/exchange_rates/view/files/monthly_xml_2026-9.xml

每月抓取一次、缓存一个月,该期间内的每一笔英国增值税换算都可以从一个你能直接交给检查人员的文件中复现。

欧盟:增值税指令第 91 条

对于欧盟内部供应,理事会指令 2006/112/EC 第 91(2) 条将规则定为"在增值税应纳税时,相关成员国最具代表性的一个或多个外汇市场上所记录的最新卖出汇率,或参照该市场或该等市场确定的汇率。"

要在 27 个成员国之间实现这一点会很困难,因此该指令增加了一个务实的出口:成员国"应转而接受在税款应纳时由欧洲中央银行发布的最新汇率。" 两种非欧元货币之间的换算"应使用各自货币的欧元汇率"进行——换言之,通过 EUR 交叉换算,而不是直接对该货币对报价。成员国可以要求你就行使该选项进行通知。

对于进口,第 91(1) 条转而援引关于计算海关完税价格的海关规则——在同一本账里,这是一个确实不同的汇率、一个确实不同的日期。如果你的系统把"欧盟增值税"当作一条换算规则,那它已经错了。

这一切之下:IAS 21

税务规则叠加在你的会计政策之上,而对 IFRS 报送方而言,该政策就是 IAS 21。四项规定承担了大部分工作:

  • 外币交易在初始确认时按交易日的即期汇率计量(IAS 21.21)。
  • 允许使用平均汇率作为简化处理,但仅限于"汇率未发生显著波动"的情况(IAS 21.22)。这是一个条件,不是默认选项——也正是在波动剧烈的季度里悄然失效的那一条。
  • 货币性项目在报告日按期末汇率重新折算(IAS 21.23)。
  • 以历史成本计量的非货币性项目保持交易日汇率,重新折算。

美国公认会计原则在 ASC 830 下得出的结论大体相似。对开发者而言,实际后果是:一笔交易在其生命周期内完全可能合理地需要两三个不同的汇率——确认时一个、期末结账时一个、结算时一个——而你的表结构必须为它们全部留出空间。我们的企业货币风险管理指南讲解了由此产生的损益在商业上意味着什么。

会演变成财报重述的五个换算 Bug

1. 汇率方向

base=USD&symbols=EUR 返回的是每美元折合多少欧元。base=EUR&symbols=USD 返回的是每欧元折合多少美元。IRS 的年度表是"每美元折合多少外币",所以你要做除法;而 base=EUR 的 Finexly 响应是"每欧元折合多少美元",所以你要做乘法。两者都正确;把它们混起来就不对了。

修复办法既平淡又有效:永远不要把某一列命名为 rate。把它命名为 quote_per_base,让方向在表结构里而不是在注释里变得毫不含糊。

2. 重新查询而非重放

2029 年的一次审计问到 2026 年的一笔交易。如果你的报表代码在生成报表时调用实时 endpoint,那么同一份报表跑两次会得出两个不同的数字。适用于某笔交易的汇率是关于该交易的一个事实,不是一次查表——在换算发生的那一刻就把它持久化。历史 endpoint 的存在是为了回填对账,而不是替代存储;回填模式请参见我们的历史汇率 API 指南

3. 该用即期时用了平均

月度平均汇率很方便,也常常被允许,但 IAS 21.22 附带了一个条件,而在 IRS 的指引下一次性交易通常要求使用交易日汇率。把方法与汇率一并存储,这样你回答"为什么是这个数字?"时就不必做考古。

4. 缺失的日期

周末、法定节假日和非 TARGET 日没有公布汇率。每个系统都需要一条明确规则——通常是"该日期当天或之前最后公布的汇率"——而且它需要记录哪一条规则被触发了。半年之后,一次静默的回退和一个 bug 是无法区分的。这与缓存与错误处理实践直接重叠。

5. 精度与舍入

存储的小数位要比展示的多,并且只在最终展示或过账环节舍入一次。在几千张发票上对每个中间步骤都做舍入,会制造出一个既难以解释又无法回溯的对账缺口。我们在货币舍入与小数位中详细讲过其中的算术。

设计汇率快照,而不是汇率查询

如果你不再把换算看作一次函数调用,而开始把它看作一条不可变记录,整个问题就会土崩瓦解。下面是一套能满足上文所有要求的最小表结构:

CREATE TABLE fx_rate_snapshot (
    id                BIGSERIAL PRIMARY KEY,
    transaction_id    BIGINT      NOT NULL,
    -- direction is in the name, not in a comment
    base_currency     CHAR(3)     NOT NULL,   -- e.g. 'EUR'
    quote_currency    CHAR(3)     NOT NULL,   -- e.g. 'USD'
    quote_per_base    NUMERIC(20,10) NOT NULL,

    rate_date         DATE        NOT NULL,   -- the date the rate APPLIES to
    method            TEXT        NOT NULL,   -- 'spot' | 'monthly_period' | 'yearly_average'
    fallback_applied  TEXT,                   -- 'previous_business_day' | NULL

    source            TEXT        NOT NULL,   -- 'finexly' | 'hmrc_monthly' | 'irs_yearly'
    source_reference  TEXT,                   -- file name, request id, or table year
    retrieved_at      TIMESTAMPTZ NOT NULL,   -- when WE obtained it

    amount_base       NUMERIC(20,4) NOT NULL,
    amount_quote      NUMERIC(20,4) NOT NULL,

    created_at        TIMESTAMPTZ NOT NULL DEFAULT now()
);

-- one authoritative conversion per transaction per purpose
CREATE UNIQUE INDEX ux_fx_snapshot_txn_method
    ON fx_rate_snapshot (transaction_id, method, rate_date);

有三列承担了大部分审计分量。rate_date 是该汇率所适用的日期,与你取得它的时点 retrieved_at 刻意分开——在 9 月回填时抓取的 3 月 13 日汇率完全合规,而这一对 timestamp 正是诚实地说明了这一点。fallback_applied 把你的周末规则从不可见的行为变成了留痕的证据。

请注意,行永远不做更新。如果某个汇率被更正,插入一条新行并让它取代旧行。一份你可以编辑的审计轨迹,不叫审计轨迹。

获取一个站得住脚的历史汇率

要取得交易日汇率,请请求该特定日期,而不是当前日期:

curl "https://api.finexly.com/v1/historical?date=2026-03-13&base=EUR&symbols=USD" \
  -H "Authorization: Bearer YOUR_API_KEY"
{
  "base": "EUR",
  "date": "2026-03-13",
  "rates": {
    "USD": 1.0842
  }
}

base=EUR 的情况下,该数值是每欧元折合多少美元——因此一张 €10,000 的发票通过乘法换算为 $10,842.00。下面是 Python 中的写入即快照模式,其中包含缺失日回退以及上文表结构所需的字段:

import requests
from datetime import date, timedelta
from decimal import Decimal, ROUND_HALF_UP

API = "https://api.finexly.com/v1/historical"
HEADERS = {"Authorization": "Bearer YOUR_API_KEY"}


def fetch_rate(base: str, quote: str, on: date, max_lookback: int = 5):
    """Return (quote_per_base, rate_date, fallback) for a given date.

    Walks back to the most recent published rate if `on` is a weekend
    or a market holiday, and reports which day it actually landed on.
    """
    for offset in range(max_lookback + 1):
        d = on - timedelta(days=offset)
        r = requests.get(
            API,
            params={"date": d.isoformat(), "base": base, "symbols": quote},
            headers=HEADERS,
            timeout=10,
        )
        r.raise_for_status()
        rates = r.json().get("rates", {})
        if quote in rates:
            fallback = "previous_business_day" if offset else None
            return Decimal(str(rates[quote])), d, fallback

    raise LookupError(f"No {base}/{quote} rate within {max_lookback} days of {on}")


def convert_for_filing(amount_base, base, quote, transaction_date):
    """Convert once, and return everything an auditor will ask for."""
    rate, rate_date, fallback = fetch_rate(base, quote, transaction_date)
    amount = Decimal(str(amount_base))
    converted = (amount * rate).quantize(Decimal("0.01"), rounding=ROUND_HALF_UP)

    return {
        "base_currency": base,
        "quote_currency": quote,
        "quote_per_base": rate,
        "rate_date": rate_date.isoformat(),
        "method": "spot",
        "fallback_applied": fallback,
        "source": "finexly",
        "amount_base": amount,
        "amount_quote": converted,
    }


snapshot = convert_for_filing(10000, "EUR", "USD", date(2026, 3, 13))
print(snapshot["amount_quote"], snapshot["rate_date"], snapshot["fallback_applied"])
# -> 10842.00 2026-03-13 None

这个函数在换算的同时就完成了留痕。无论你的持久化层是什么,都要在下游任何环节看到这个数字之前,把这个字典写进去。同样的纪律在多币种开票SaaS 计费跨境薪酬发放中都会带来回报——而它们最终都会汇入同一份纳税申报表。

与官方公布汇率进行对账

对于走路径 2 的英国增值税,每月拉取一次 HMRC 的月度文件,并将其存储为该期间的事实来源:

import csv, io, requests
from datetime import date

def hmrc_monthly_rates(year: int, month: int) -> dict:
    """HMRC monthly rates for VAT/customs. Note: month is NOT zero-padded."""
    url = (
        "https://www.trade-tariff.service.gov.uk/exchange_rates/view/files/"
        f"monthly_csv_{year}-{month}.csv"
    )
    resp = requests.get(url, timeout=15)
    resp.raise_for_status()

    reader = csv.DictReader(io.StringIO(resp.text))
    # Don't hard-code the header text: find the ISO-4217 code column by shape.
    code_col = next(
        c for c in reader.fieldnames if "code" in c.strip().lower()
    )
    return {
        row[code_col].strip(): row
        for row in reader
        if row.get(code_col) and len(row[code_col].strip()) == 3
    }

然后运行一个定期的偏移检查:将每条已存储的快照与该期间的官方汇率比对,并对超出你有意设定的容差范围的任何数据打标。市场汇率与公布的行政汇率之间存在小幅差异是预期之中的,通常也可以接受——但你希望在检查人员替你算出来之前,先知道这个差距有多大。如果你要把它接入总账,我们关于会计软件集成以及汇率 API 的数据从哪里来的笔记,涵盖了接下来会出现的数据溯源问题。

多币种数据的申报前检查清单

  1. 每一笔换算后的金额都有已存储的汇率。 没有任何报表在运行时重新计算历史换算。
  2. 汇率方向毫不含糊——体现在列名里,而不是在文档里。
  3. rate_dateretrieved_at 是彼此独立的字段,并且两者都有值。
  4. 缺失日规则是明确且留痕的,而不是一次隐式重试。
  5. 每一类交易只用一种方法,并在整个期间内一贯适用——这正是美国和英国的实际法律要求。
  6. 舍入只发生一次,在最后一步,并采用有记录的模式。
  7. 快照行只追加不修改。 更正是取代,绝不覆盖。

把这七条走一遍,会计的那个问题就不再可怕了。答案变成了一条查询。

常见问题

IRS 在税务申报中要求使用哪个汇率? 没有特定要求。IRS 明确表示自己"没有官方汇率",并且"一般接受任何被一贯使用的已公布汇率。" 默认是你收取、支付或权责发生该项目时的现行即期汇率;对于稳定产生的收入,IRS 发布年度平均汇率表,并要求申报人用外币金额除以表中所列汇率。

增值税方面,我可以用货币 API 代替 HMRC 公布的汇率吗? 可以,但有限度。VAT Notice 700 §7.6 将供应发生时的英国市场卖出汇率定为默认选项,因此只要你一贯适用,基于市场的 API 汇率符合这条路径。HMRC 自身的期间汇率是一个无需事先通知即可采用的明确替代方案——但一经采用,未取得书面同意就不能改回来。落在这两条路径之外的汇率或方法需要提交书面申请,而远期汇率不被接受。

我需要存储汇率,还是以后再查一次就行? 存下来。已存储的汇率让换算可复现;重新查询则变成一次全新计算,可能与你已经提交的申报表对不上。存储汇率、它的日期、它的来源以及你取得它的时点,正是把一个数字变成证据的过程。

周末或节假日的交易日期该用什么汇率? 非交易日没有公布汇率,所以你需要一条明示规则——最常见的是交易日当天或之前最后公布的汇率。比你选哪条规则更重要的是:它有文档记录、被统一适用,并且在每一条受影响的记录上留痕。

出于税务目的应该存储多少位小数? 存储你的供应商返回的完整精度——6 到 10 位小数是一个合理的列宽——并且只在过账或展示时舍入。过早且反复舍入,是那些无人能追溯的对账缺口的常见成因。

欧洲央行的汇率能满足欧盟增值税的要求吗? 增值税指令第 91(2) 条要求成员国接受在税款应纳时公布的最新欧洲央行汇率,跨币种换算则通过各币种的欧元汇率进行。部分成员国要求你就使用该选项进行通知,而进口则改为适用海关估价规则。

让你的汇率经得起审计

准备好把实时汇率与历史汇率集成进你的项目了吗?免费获取 Finexly API 密钥——无需信用卡。从每月 1,000 次免费请求开始,随业务增长再升级。完整的历史 endpoint 参考请查阅 API 文档,一次性查询可试用货币换算器,若你正在评估供应商可比较各家货币 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 →