资讯动态

免费外汇API实战:Python获取实时与历史汇率全攻略

发布时间:2026/9/23 15:50:16 来源:尧图企业网站定制
做外汇数据开发这行当最烦的就是拿不到干净、稳定、还不要钱的数据源。我早期靠网上那些二手接口要么隔三差五挂掉要么返回的字段乱七八糟清洗数据比写代码还累。后来把市面上能薅羊毛的免费外汇API基本都试了一遍总算摸出一套能直接用的方案。这篇东西不跟你扯大道理全部是能落地的Python代码和实测经验照着抄就能跑。这篇文章适合谁想搞个人量化回测的、做跨境电商看汇率成本的、写爬虫做金融数据可视化的或者说单纯不想给付费数据商交智商税的都能在这里找到你要的东西。我会把实时查询、历史数据拉取、限流处理、数据清洗这些核心环节拆开讲清楚同时把我在使用中踩过的坑一并交代明白。1. 免费外汇API怎么选实测对比与选型思路1.1 市面主流免费外汇API横向对比先说结论免费的外汇数据接口远比你想的少。很多人一上来就搜外汇API结果找到的要么是试用两三天就收费的要么是文档写得天花乱坠但请求一次就报错的。我花了一个星期把主流的几个过了一遍给你们整理成一张对比表。API名称免费额度是否需要Key数据源历史数据更新频率Frankfurter无限制不需要欧洲央行(ECB)支持1999年至今工作日每日更新exchangerate-api500次/月需要各家银行汇总仅支持少量货币每日更新Open Exchange Rates1000次/月需要自家聚合数据仅付费版支持每日更新Twelve Data800次/天需要交易所及银行付费版才完整实时延迟较小Yahoo Finance非正式接口不需要多方来源支持分钟级很多人看到 Yahoo Finance 就觉得是神器毕竟它是真正意义上的免费高频。但我要泼一盆冷水Yahoo 的接口一直没有正式对外开放文档全是社区逆向出来的说不准哪天就改参数格式搞你一下。我自己用的时候经常遇到返回JSON结构突然变化导致整个程序直接崩掉。所以如果你要做的是长期稳定运行的服务我不太建议把它当主力数据源。1.2 为什么我把Frankfurter当主力在对比一圈之后我最终把主力数据源锁定在Frankfurter上。原因很简单不需要注册、不需要API Key、调用无限制、数据来自欧洲央行。对于个人项目和中小企业来说数据源的可信度足够高并且ECB的汇率本身就是全球外汇市场的重要参考基准很多银行和金融机构的结汇价格都是基于这个数据做的。它免费的力度有多大我实际测试过连续大批量拉取没有遇到任何限流的情况。文档里也写着 Free for everyone没有隐藏的收费陷阱。当然免费的东西一定有代价它的代价就是数据频率是日更级别不是秒级Tick数据。这就引出一个很多新手容易犯的认知错误外汇API的实时到底是什么意思我单独拿一节来讲。1.3 免费API的边界别被实时两个字忽悠了每一个刚接触外汇数据的人都会对实时查询这四个字产生误解。我最早做这个项目的时候以为实时就等于券商交易软件里那种跳动的价格直到数据拉回来发现一天只有一根价格才意识到问题的严重性。这里要厘清一个核心概念外汇市场是全球最大的金融市场但它没有一个统一的交易所价格由全球各地不同的做市商和交易平台各自报价。所以任何一家数据商给你的实时价格其实都是某几个数据源的抽样平均值或者延迟快照。ECB的数据更特殊它每天在特定时间点通常是欧洲中部时间下午4点左右采集一次全球主要货币的参考汇率然后对外发布。也就是说Frankfurter的最新数据实际上是最近一个工作日的收盘参考价。这对你的应用场景有直接影响如果你做的是日线级别的量化策略、跨境电商定价、财务报表折算这个频率完全够用。如果你做的是日内高频交易、剥头皮策略那免费API基本没戏你得直接对接流动性供应商或者付费用专业数据流。所以动手写代码之前先想清楚自己的业务需要什么频率的数据。别像我刚开始那样兴致勃勃写了个监控程序上线第一天发现价格一天都不带动的还以为是代码Bug。2. 环境准备与API调用基础2.1 Python环境与依赖库安装在开始写代码之前先把环境准备好。这篇文章的所有示例基于Python 3.9如果你用的3.6或者更早的版本建议先升级到3.9以上因为下面用到的类型注解和一些语法糖在旧版本上跑不起来。我习惯在虚拟环境里做开发避免不同项目之间的依赖打架。创建和激活虚拟环境的命令很简单python -m venv forex_env source forex_env/bin/activate # Windows下用 forex_env\Scripts\activate激活之后安装需要用到的库pip install requests pandas matplotlib这三个库的定位很明确requests用来发HTTP请求pandas做数据清洗和结构化matplotlib做可视化验证数据是否正常。别小看这些基础库后面所有的代码都是围绕它们展开的。2.2 REST API调用的核心认知再说一个基础概念懂的人可以直接跳过这节。外汇API本质上是一个RESTful接口所谓RESTful说白了就是通过HTTP协议的GET/POST方法去服务器要数据或者提交数据。你的代码发一个请求服务器返回一段JSON字符串你把这个字符串解析成Python字典就能拿到里面的汇率数字了。用生活化的方式理解你打开浏览器访问一个网址网址后面带了一串参数告诉服务器你想要什么数据服务器把结果以JSON格式返回到页面上。代码干的事情就是把手动访问网址这个过程自动化。拿Frankfurter的接口举例它的最基础调用长这样import requests url https://api.frankfurter.app/latest params { from: USD, to: CNY } resp requests.get(url, paramsparams, timeout10) data resp.json() print(data)跑完这段代码如果你收到的结果类似下面这种就说明数据链路已经通了{amount: 1.0, base: USD, date: 2025-01-15, rates: {CNY: 7.23}}注意看返回结果里有个date字段这个字段就是我说过的最后一个工作日的日期它不一定是今天。如果今天是周末它返回的就是周五的数据。很多人不知道这个特性拿着日报数据去跟实时行情对比怎么都对不上。3. 实时汇率查询实战从单币种到批量监控3.1 最小可用的实时查询代码既然数据链路通了接下来就是把这个最基础的调用包装成一个可以在真实项目里复用的函数。先说最简单的单币种查询版本只查一个货币对比如美元兑人民币。import requests def get_latest_rate(base: str USD, quote: str CNY) - dict: url https://api.frankfurter.app/latest params {from: base, to: quote} try: resp requests.get(url, paramsparams, timeout10) resp.raise_for_status() data resp.json() return { base: data[base], quote: quote, rate: data[rates][quote], date: data[date] } except requests.exceptions.RequestException as e: print(f请求失败: {e}) return {}这里有几个细节值得说明。timeout10必须加否则网络异常时请求可能会挂很久才超时拖慢你的程序。resp.raise_for_status()的作用是当HTTP状态码不是200时直接抛出异常这样你就能在调试时第一时间发现问题而不是拿到一个空JSON后再去排查。我自己在实际使用中还会在这个函数里加一个重试机制因为免费的接口虽然稳定但公网网络环境总会有波动。重试的逻辑很简单第一次失败就等两秒重试第二次失败等四秒最多重试三次。这比傻乎乎地失败一次就退出要靠谱得多。当然重试代码在下面的批量监控版本中会体现单次查询就没这个必要了。3.2 带异常处理与限流的健壮版本如果要把查询函数放到一个持续运行的程序里光有最基本的功能是不够的。你需要考虑网络抖动、服务器临时故障、参数错误等异常情况以及最重要的——请求频率控制。免费的API虽然不限制总调用次数但也不代表你可以用死循环疯狂请求。在Frankfurter的文档里虽然没有明确的QPS限制但出于礼貌和稳定性的考虑我建议单次请求间隔至少保持1秒以上。对于需要频繁查询的场景更合理的做法是在本地做缓存这个后面会讲到。下面是一个更健壮的批量监控版本封装了重试和限速逻辑import requests import time from typing import Optional class ForexClient: def __init__(self, base_url: str https://api.frankfurter.app): self.base_url base_url self.session requests.Session() self.last_request_time 0.0 def _request(self, path: str, params: dict, retries: int 3) - Optional[dict]: for attempt in range(retries): # 简单限速保证两次请求之间至少间隔1秒 sleep_time max(0, 1.0 - (time.time() - self.last_request_time)) if sleep_time 0: time.sleep(sleep_time) try: url f{self.base_url}{path} resp self.session.get(url, paramsparams, timeout10) resp.raise_for_status() self.last_request_time time.time() return resp.json() except requests.exceptions.RequestException as e: print(f请求失败第{attempt 1}次重试{e}) time.sleep(2 ** attempt) print(f请求最终失败{path} {params}) return None def get_latest(self, base: str EUR, symbols: str USD,CNY) - Optional[dict]: return self._request(/latest, {from: base, to: symbols}) def get_history(self, start: str, end: str, base: str EUR, symbols: str USD,CNY) - Optional[dict]: return self._request(f{start}..{end}, {from: base, to: symbols})这里我用了一个requests.Session()对象而不是每次调用requests.get()。Session的好处在于它会自动复用底层的TCP连接避免每次请求都重新握手的开销。在需要频繁请求的场景下这个优化能明显减少延迟。有人可能会问retries3够不够我的经验是对于公网API调用3次重试基本覆盖了绝大多数临时性故障。如果3次都失败说明要么是网络彻底断了要么是API服务真的挂了这时候再重试也没有意义正确做法是记日志告警让运维或者你自己去检查。3.3 批量监控多个货币对的实际操作单货币对查询只是开胃菜真正在业务中用得多的场景是批量监控多个货币对。比如我做跨境电商的朋友需要同时关注美元、欧元、英镑兑人民币的汇率以便决定什么时候结汇。批量监控的思路很简单循环调用查询函数每次查一个货币对控制好请求间隔把结果收集起来。但这里面有一个性能优化点Frankfurter支持一次请求返回多个目标货币的汇率相当于批量查询不用发多个请求。client ForexClient() result client.get_latest(baseUSD, symbolsCNY,EUR,GBP,JPY) if result: print(f基准货币: {result[base]}) print(f数据日期: {result[date]}) for currency, rate in result[rates].items(): print(f{result[base]}/{currency}: {rate})执行结果类似这样基准货币: USD 数据日期: 2025-01-15 USD/CNY: 7.2301 USD/EUR: 0.9217 USD/GBP: 0.7845 USD/JPY: 155.38这个优化非常关键。假设你要监控30个货币对如果每个货币对发一个请求总共要30次HTTP请求如果利用多目标查询一次请求就能搞定。请求次数减少的不仅仅是流量更重要的是降低了被限流的风险。回到批量监控的场景我建议把查询结果缓存到本地文件或者数据库中方便后面做趋势分析。最简单的做法是存成CSV一行一条记录包含日期、基准货币、目标货币、汇率。这样哪怕API服务有一天出了状况你手头还有历史数据可以用不至于抓瞎。4. 历史汇率数据获取与清洗导出4.1 按日期区间拉取历史数据历史数据是做量化回测、汇率趋势分析的基础。Frankfurter的接口设计得比较人性化直接通过URL路径指定日期范围就能返回区间内所有工作日的汇率数据。先看一个最简单的时间序列获取方式拉取2024年全年美元兑人民币的每日汇率import requests url https://api.frankfurter.app/2024-01-01..2024-12-31 params {from: USD, to: CNY} resp requests.get(url, paramsparams) data resp.json() print(f基准货币: {data[base]}) print(f时间跨度: {data[start_date]} 到 {data[end_date]}) print(f总记录数: {len(data[rates])}) # 打印前5条数据 for idx, (date, rate_dict) in enumerate(data[rates].items()): if idx 5: break print(f{date}: {rate_dict[CNY]})输出结果类似基准货币: USD 时间跨度: 2024-01-01 到 2024-12-31 总记录数: 261 2024-01-01: 7.1234 2024-01-02: 7.1205 2024-01-03: 7.1389 2024-01-04: 7.1512 2024-01-05: 7.1587注意到总记录数: 261这个数字了吗2024年是闰年有366天但返回的只有261条记录。这说明什么说明ECB全年只发布了261个工作日的汇率数据。周末和欧洲的节假日是没有汇率的。这是外汇历史数据最容易让新手踩坑的地方如果你拿这个数据去做时间序列分析一定要先做重采样把缺失的日期补上否则你的模型会因为时间轴不连续而出各种诡异的结果。4.2 用Pandas把JSON转为结构化数据接口返回的数据是嵌套的JSON结构直接看倒是挺清晰但要做分析就得转成结构化表格。这里就到了pandas的用武之地。转换逻辑很简单把rates这个字典转成DataFrame日期做索引货币列做字段。import pandas as pd rates data[rates] df pd.DataFrame(rates).T df.index pd.to_datetime(df.index) df df.sort_index() df.columns [f{data[base]}_{col} for col in df.columns] print(df.head()) print(df.tail())字段名我特意加上了基准货币的前缀比如USD_CNY。这样做的好处是当你同时拉取了多个基准货币的数据并要做合并的时候列名不会冲突。比如你拉过USD/CNY和EUR/CNY合并之后就自然分成两个独立的列。拿到干净的DataFrame之后就可以做各种分析了。最常见的是计算滚动均值来观察趋势df[ma20] df[USD_CNY].rolling(window20).mean() # 查看最近10条数据确认均线计算正常 print(df.tail(10))还有一个非常有用的操作是计算每日涨跌幅df[daily_pct_change] df[USD_CNY].pct_change() * 100 print(df[daily_pct_change].describe())通过这些统计量你能快速判断汇率在这个时间段内的波动情况。比如标准差大说明汇率波动剧烈做外贸的朋友就得注意结汇时机的把握。4.3 数据可视化验证与CSV导出数据拉下来、清洗好接下来就是要验证数据的合理性。别急着拿数据去跑策略先画个图看看曲线是否符合直觉。用matplotlib画折线图十几行代码的事import matplotlib.pyplot as plt fig, ax plt.subplots(figsize(12, 6)) ax.plot(df.index, df[USD_CNY], labelUSD/CNY, linewidth1.5) ax.plot(df.index, df[ma20], label20日均线, linewidth1.2, linestyle--) ax.set_title(USD/CNY Exchange Rate in 2024) ax.set_ylabel(Exchange Rate) ax.legend() ax.grid(True, alpha0.3) plt.tight_layout() plt.savefig(usd_cny_2024.png, dpi150)画图不是浪费时间这一步能帮你快速发现数据异常。比如你看到某天汇率突然跳了个大台阶但当天并没有重大经济新闻那很可能是数据质量问题需要进一步排查。数据验证没问题之后导出CSV存档df.to_csv(USD_CNY_history.csv, encodingutf-8-sig)这里有个小细节用utf-8-sig编码而不是默认的utf-8。因为CSV文件如果用Excel打开utf-8编码会导致中文列名乱码utf-8-sig带BOM头Excel能正确识别。CSV导出后你的历史数据就沉淀在本地了。下次再跑分析就不需要重新请求API直接从CSV读就行。这个习惯能帮你省下大量请求次数和时间。5. 高频踩坑状态码、限流与数据时区问题实录5.1 HTTP状态码速查表与对应处理调用任何API都会遇到各种状态码。网上很多教程只告诉你请求失败就重试但这其实是不负责任的。不同的状态码代表的问题完全不同处理方式也完全不一样。我把外汇API实际使用中最常见的状态码整理成了一张速查表。状态码含义常见原因处理方式200成功正常响应直接解析数据400请求参数错误货币代码拼写错误、日期格式不对检查请求参数404接口路径不存在URL拼写错误核对文档路径403禁止访问某些接口需要认证检查是否需要Header或Key429请求过于频繁触发限流等待后重试加退避策略500服务器内部错误API服务临时故障记录日志稍后重试503服务不可用服务器过载维护中暂停请求等待恢复我的经验是400错误最闹心因为它代表你的代码有问题不是重试能解决的。比如把货币代码CNY写成了CHN接口直接返回400这时候你应该去查货币代码表而不是一遍遍重试。Frankfurter支持的全部货币代码在官网文档里有一个ISO 4217标准的列表拿不准的时候去查一下。429错误是最常见的限流错误。遇到这种错误最重要的是绝对不要立刻重试那样只会让限流时间更长。正确的做法是退避重试第一次等1秒第二次等2秒第三次等4秒指数退避。如果连续多次都触发429说明你的请求频率确实太高了需要重新设计你的缓存策略。5.2 免费额度用尽怎么办缓存与降级策略虽然Frankfurter不限制调用次数但其它一些免费API比如exchangerate-api的500次/月额度很快就用完了。对此我总结出了一套完整的应对方案。缓存策略的核心思路是数据可以旧但不能没有。外汇汇率每天才更新一次你完全没必要每次都去请求API。正确的做法是首次请求成功后把数据存到本地后续请求先查本地缓存超过一定时间比如6小时再刷新。import json import os import time CACHE_FILE rate_cache.json CACHE_TTL 6 * 3600 # 缓存有效期6小时 def get_rate_with_cache(base: str, quote: str) - float: cache {} if os.path.exists(CACHE_FILE): with open(CACHE_FILE, r, encodingutf-8) as f: cache json.load(f) cache_key f{base}_{quote} cached cache.get(cache_key) if cached and time.time() - cached[timestamp] CACHE_TTL: return cached[rate] # 缓存过期或不存在请求API client ForexClient() data client.get_latest(basebase, symbolsquote) if data: cache[cache_key] { rate: data[rates][quote], timestamp: time.time() } with open(CACHE_FILE, w, encodingutf-8) as f: json.dump(cache, f, ensure_asciiFalse, indent2) return data[rates][quote] # 如果API请求失败但本地有旧缓存就直接用旧数据 if cached: print(f警告API请求失败返回{TTL}秒前的缓存数据) return cached[rate] raise Exception(API和缓存都不可用无法获取汇率)这套代码的精髓在最后那个if cached分支。很多设计的误区是缓存过期了就一定要请求新的但实际的容灾逻辑恰恰相反当上游请求失败时稍微旧一点的数据远比没有数据强。比如你的订单系统需要算实时结算汇率如果API挂了但缓存里有一个小时前甚至昨天的数据至少订单流程还能继续走只是汇率精度差一点。5.3 时区、非交易日和伪实时问题这是最后一个也是我最想强调的一个坑。外汇数据的时间处理比大多数人想象的要麻烦。我在项目上线后的第二周就栽在了这里当时发现程序在每天凌晨会拉取一次最新汇率结果连续三天返回的都是同一条数据我还以为API挂了。实际上问题出在数据发布节奏和时区上。ECB每天在欧洲中部时间下午4点左右发布当天参考汇率。换成北京时间大约是晚上10点到11点。也就是说北京时间早上8点请求最新数据拿到的是昨天发布的汇率。北京时间晚上11点后请求才能拿到今天的汇率。周六周日请求永远拿到的是周五的数据。遇到欧洲公共假日比如圣诞节、复活节ECB不发布数据拿到的是前一个工作日的。针对这个问题我的处理方案是在代码里明确区分数据日期和请求日期。不要简单地把date字段和系统当前日期做比较而是换成检查一个工作日周期内是否有新数据发布。最靠谱的方法是每次拉数据时记录date字段如果连续多个请求周期date都没变说明这段时间没有新数据可能是周末或假日这是正常的不需要告警。# 检测数据是否更新 previous_date 2025-01-14 current_date data[date] if current_date previous_date: print(数据未更新可能是周末或节假日) else: print(f数据已更新{current_date})时区处理上还有一个容易忽略的细节如果你把date字段解析成datetime对象一定要指定时区或者统一按照UTC处理否则在夏令时切换的时候会出现时间偏移问题。最简单的做法是把所有日期都按YYYY-MM-DD字符串处理不做时区转换因为汇率数据的粒度是天精确到小时反而容易出岔子。这就是我踩过的最贵的坑。项目里如果跑的是日线策略一定记住那条原则免费API给不了你分秒级的实时它给你的是一天中某几个时间点的准确快照。理解并接受这个限制你的程序才不会在生产环境里出幺蛾子。最后分享一个我一直在用的小技巧把拉回来的历史数据每周备份一次存到云盘或者另一个数据库里。免费的接口永远不应该成为你唯一的依赖本地数据的沉淀才是项目真正的资产。等到你积累了一两年的历史数据你会发现光靠这些数据本身就能做很多免费API根本做不了的分析。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价