ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

CoinGlass资金费率数据清洗实战:从原始API到量化可用表

CoinGlass资金费率数据清洗实战:从原始API到量化可用表 先声明两句。这是一个纯技术复盘的记录数据仅用于个人量化研究不构成任何投资建议。这个模块的定位很明确抓取 CoinGlass 上 BTC 永续合约的资金费率、持仓量这类原始资金流数据然后通过 pandas 做标准化清洗最终得到可以直接入模型、直接做回测的干净表。整个项目从需求到落地花了不到一周但真正让我花掉大半时间的不是抓取接口而是数据清洗。原始数据就像从菜市场拎回来的带泥萝卜不洗就下锅后面整个策略分析都会带着一股土腥味。项目完成后我复盘了一遍整个过程觉得有几段经验特别值得分享包括接口请求怎么设计、清洗流程怎么分层、时间戳和百分比字符串这些经典坑怎么排以及为什么最后我把模块拆成了三个独立层。如果你也在做加密货币数据分析、量化研究或者只是好奇这类公开行情数据为什么要做这么重的清洗这篇文章应该能给你一份可以直接参考的实操手册。1. 项目整体设计与需求拆解1.1 这次要处理的数据到底是什么比特币合约市场里有一个核心指标叫资金费率英文是 Funding Rate。简单说在永续合约机制下为了让合约价格始终锚定现货价格每隔一段时间多头和空头之间要互相付钱。费率为正说明多头占优多头持仓者需要付钱给空方费率为负则反过来空头要向多方付费。这个指标在 CoinGlass 上可以实时看到但它是判断市场情绪是否过热、是否出现极端恐慌的重要参考量化分析里经常把它当作情绪因子。我这次抓的“原始资金流”指的是 CoinGlass 聚合的各交易所 BTC 永续合约资金费率、持仓量和结算价格快照。字段看着不多一个交易所一小时一条但一旦涉及多交易所、多时间段、多合约周期数据就会迅速膨胀。更重要的是不同交易所有不同的结算周期有的每 8 小时结算三次有的每小时结算一次结算起始时间还互相错开。这种细微差异如果不处理后面做跨交易所对比时数据根本对不齐。这个模块解决的核心问题就是把这种“能看但没法直接用”的原始数据变成一张字段统一、时间对齐、缺失值处理好、异常值有标记的干净表。下游策略回测、信号分析、情绪指标计算拿到的都应该是同一套标准化数据而不是各算各的、互相打架的“脏数据”。1.2 原始数据到底脏在哪里先别急着写代码我在动手前把 CoinGlass 返回的原始 JSON 翻了一遍归纳出三个层面的脏。时间层面同一张表里会出现三种时间表达有标准的 UTC 毫秒时间戳例如 1700000000000有 ISO8601 字符串例如“2025-01-15T08:00:00.000Z”还有直接把北京时间“2025-01-15 16:00:00”写进来的情况。如果不在入口统一成同一个基准时间下游就完全没法排序和合并更别说跨交易所对齐。数值层面资金费率有时是浮点数 0.0001有时是字符串“0.01%”有时候还是“-0.0100%”这种带百分号和正负号的展示格式。如果只做 float() 强制转换百分号字符串直接报错如果只做 replace(“%”)、忘掉除以 100清洗完的数值就会整体扩大 100 倍这个错特别隐蔽不打印样本根本发现不了。记录层面重复拉取、网络重试、时间窗口边缘重叠都会导致同一条数据出现多次。更麻烦的是有些重复行的其他字段比如最新价由于两次快照时间不同并不完全一致直接用 drop_duplicates 根本去不掉。这个问题我放在后面“踩坑记录”章节里细说。为什么强调“清洗模块”而不是“抓取模块”因为抓取本质上是网络请求加 JSON 解析是一条流水线数据清洗才是决定数据质量的核心环节。我的经验是清洗工作往往占整个项目八成以上的工作量这也是这篇文章想重点展开的部分。2. 抓取模块把 CoinGlass 原始资金流拉下来2.1 为什么我选择官方 API而不是写爬虫动手之前我纠结过到底是直接爬 CoinGlass 网页还是走官方 API。最后选了 API原因非常现实。网页爬虫的路径是抓 HTML 页面、定位表格节点、解析前端展示格式。CoinGlass 这类站点的数据是前端动态加载的接口地址和字段名隔一段时间就可能调整而且页面里的数字都已经过格式化比如“1.2K”“0.0100%”你必须先还原成纯数值再喂给清洗流程这相当于平白多了一层解析工作还会把展示层的脏污带进核心流水线。官方 API 返回的是结构化 JSON字段名、数值精度、时间格式相对稳定个人研究级的调用频次也完全够用。API 文档通常会把每个接口的响应示例列得比较清楚字段含义不用靠猜。对资金费率这种对精度极其敏感的数据我宁愿信接口吐出的原始数值也不相信网页格式化后的展示字符串。抓取的核心诉求是稳定不是炫技这一点上 API 的收益几乎是碾压性的。2.2 请求签名与抓取核心代码CoinGlass API 的授权方式跟大多数交易平台类似核心是 API Key 加签名。签名的意义在于防止请求参数在传输过程中被篡改服务端收到后会重新计算签名并校验时间戳时间戳偏差太大会直接拒绝请求这是防重放攻击的常规设计和交易所下单接口是同一套思路。我封装了一个极简的客户端只保留跟资金费率相关的部分。实际使用中你直接在环境变量里配置好 Key 和 Secret 就行不要硬编码在源码里。这里按 HMAC-SHA256 签名方式封装具体请求头字段名以你拿到的 API 文档为准。import time import hmac import hashlib import requests from urllib.parse import urlencode class CoinGlassClient: BASE_URL https://open-api.coinglass.com def __init__(self, api_key: str, api_secret: str): self.api_key api_key self.api_secret api_secret def _sign(self, params: dict) - str: query urlencode(sorted(params.items())) return hmac.new( self.api_secret.encode(), query.encode(), hashlib.sha256 ).hexdigest() def get_funding_rate(self, symbol: str BTC, interval: str 1h): endpoint /api/funding-rate/history params { symbol: symbol.upper(), interval: interval, time: int(time.time() * 1000), } params[sign] self._sign(params) headers { CG-API-KEY: self.api_key, Content-Type: application/json, } resp requests.get( self.BASE_URL endpoint, paramsparams, headersheaders, timeout15 ) resp.raise_for_status() return resp.json()代码不复杂但有三个细节值得注意。第一参数里的 time 用毫秒时间戳不用秒很多签名校验失败都是因为时间戳单位不对签名不一致。第二签名前要把参数排序urlencode 之后所有参数按字典序排好服务端才能用同样的方式重算一遍否则两边签名永远对不上。第三不同版本的 API 可能有不同的请求头名称比如签名串是放在 Header 还是放在 Body你拿到自己的文档后照着改字段名即可封装思路是通用的。2.3 分批拉取、限频与断点续跑资金费率历史数据不是一次能拉完的尤其要覆盖近一年的数据单次接口往往只返回有限的记录条数。我的做法是按时间窗口分批拉取从起始日期开始每 7 天一个窗口逐窗口请求最后把每个窗口的返回合并成一个完整集合。批量请求时最容易被限流。接口返回 429 时其实是在明确告诉你“请求太快了”这时候再拼命发只会加重限制。我的节奏是每个请求之间 sleep 至少 1.2 秒单个窗口失败后用指数退避重试第一次等 2 秒、第二次 4 秒、第三次 8 秒最多重试 4 次。这样既不会把自己账号额度打爆也能保证任务能正常跑完。为了让任务中断后能接着跑我会先维护一个“已完成窗口”记录表。每次抓完一个窗口就把这个窗口的起止时间写进 SQLite重新启动时先查一下哪些窗口已完成自动跳过。这样即使跑到一半程序崩了也不需要全部重来。抓到的原始 JSON 不直接进清洗流程而是先以文件或表的形式落盘保留最原始的样貌。每次抓取时给每条记录打上抓取时间戳这个标记在后续去重时非常有用。核心原则是抓取层只负责把原始数据完整带回来清洗层以后想怎么改就怎么改绝对不要为了“节省存储”而丢弃原始版本。import pandas as pd def raw_to_df(resp_json, sourcecoinglass_api): rows resp_json.get(data, {}).get(list, []) df pd.DataFrame(rows) df[_fetch_time] pd.Timestamp.now(tzUTC).isoformat() df[source] source return df3. 数据清洗模块的核心流程3.1 清洗前先把字段规范定下来拿到原始数据后不要急着调用 pandas 一顿操作。我在清洗之前会先画一张目标表结构明确每列的字段名、类型、含义、来源。这个习惯能省掉后面大量返工也让队友或未来的自己拿到表时不用猜字段。我定义的最终表结构大致如下字段名类型说明tsint64UTC 毫秒时间戳唯一键的一部分exchangestring交易所名称symbolstring交易对如 BTCcontract_typestring合约类型永续或交割funding_ratefloat64清洗后资金费率小数形式pricefloat64结算时最新价open_interestfloat64持仓量sourcestring数据来源标记is_outlierbool是否被标记为异常这张表最重要的设计思路是 ts、exchange、symbol 三者共同构成唯一键后面所有去重、对齐、合并都围绕这个唯一键展开。source 字段用来区分同一时刻数据来自哪个接口或哪次抓取排查问题时特别有用。is_outlier 我选择保留标记而不是直接删行是为了给下游分析留选择权。有的策略对异常值很敏感需要在进入模型前过滤有的研究却需要单独查看极端样本所以标记比删除更灵活。3.2 主清洗流程时间统一、去重、类型修正下面这段是清洗函数的核心部分我按逻辑把流程拆成了六步。每一段代码后面我都会说明为什么这么写踩过的坑也一并标出来。import pandas as pd def to_utc_ms(x): if isinstance(x, str): return int(pd.to_datetime(x, utcTrue).timestamp() * 1000) x int(x) return x if x 10**12 else x * 1000 def clean_funding_rates(raw: pd.DataFrame) - pd.DataFrame: df raw.copy() # step1 时间统一成 UTC 毫秒 df[_ts] df[funding_time].apply(to_utc_ms) # step2 按唯一键去除重复 df df.drop_duplicates(subset[exchange, symbol, _ts]) # step3 核心字段缺失则整行剔除 df df.dropna(subset[funding_rate]) # step4 百分比字符串转小数 if df[funding_rate].dtype object: df[funding_rate] df[funding_rate].apply(convert_rate) # step5 精度统一为 8 位小数 df[funding_rate] df[funding_rate].round(8) # step6 异常值标记绝对值超过 1% 视为可疑 df[is_outlier] df[funding_rate].abs() 0.01 return df第一步的 to_utc_ms 函数很重要。pd.to_datetime 带 utcTrue会把 ISO 字符串统一转成 UTC 时间对象再转成时间戳对数值类型则需要先判断位数13 位是毫秒10 位是秒。这里常见的失误是拿毫秒数据直接当成纳秒处理时间会差到离谱后面排序、对齐全部出错。第二步的去重必须放在时间统一之后因为“2025-01-15T08:00:00Z”和“2025-01-15 16:00:00”虽然文本不同但表示的是同一个 UTC 时刻只有先归一化时间才能正确去重。第三歩直接删除缺失的资金费率而不是填充是因为资金费率是观测值不是平稳序列向前或向后填充都会把未来信息泄漏到样本里。缺失一个结算时间点宁可让下游数据稀疏一点也不能填出假数据。第四步处理字符串类型。如果你的原始数据是 API 返回大概率不会遇到但你从某个页面导出文件或者数据经过人工转存这步八成会碰到。百分号字符串转小数时最容易忘掉除以 100一旦漏掉所有费率都会被放大 100 倍清洗模块却没有明显报错属于最安静的杀手。第五步的意义是消除浮点数末尾噪声让多次清洗结果可以做一致性校验。第六步的异常阈值我按绝对值大于 1% 来标记正常行情资金费率很少超过这个范围极端行情出现超高费率时标记比删除更稳妥。3.3 跨交易所对齐让不同交易所的数据站在同一行资金费率单个交易所清洗完只是做完了一半。实际分析里经常要把 Binance、OKX、Bybit 放在同一张表里横向对比看哪家市场情绪更极端。但不同交易所的结算时间点天然存在错位直接 inner join 会把大量时间点丢掉。我的处理方式是先把清洗后的表透视成宽表行是 ts列是 exchange值是 funding_rate然后按 ts 排序对缺失的交易所数据做前向填充但限制最多填充 3 个周期避免用太旧的数据填充当前时刻。wide cleaned.pivot_table( index_ts, columnsexchange, valuesfunding_rate, aggfunclast ) wide wide.sort_index().ffill(limit3)这个步骤可以打个比方。三个食堂师傅每天轮流出菜你要统计每天各家都做了多少菜不能因为 A 食堂那天休息就把那天的统计作废可以用最近的参考数据顶上但如果 A 已经三天没开业再填就没有意义了。pandas 的 ffill(limit3) 干的就是这件事。这里我要强调一个容易被忽略的点ffill 是对时间序列的局部缺失做插补它只在宽表的相邻周期内有效。如果你要计算跨交易所的费率差用插补后的值是可以接受的但如果做严格的事件研究最好还是把原始稀疏表一起保留让下游方法选择是按可用样本计算还是基于插补后的连续对齐表计算。3.4 存储与质量校验清洗完成不等于可以用了清洗后的数据我优先存成 Parquet而不是 CSV。Parquet 是列式存储压缩率比 CSV 高很多读写速度快而且天然保留字段类型读出来不会出现“时间变成字符串”“数字变成对象”这种二次脏化。数据量不大的也可以直接存 SQLite方便用 SQL 做快速查询。每次清洗任务跑完我会生成一份简单的质量报告记录清洗前总行数、清洗后总行数、去重删除行数、缺失删除行数、异常标记行数、时间范围覆盖情况。这份报告是判断清洗流程是否稳定的关键。如果某天去重删除行数突然从 200 变成了 2 万基本可以断定抓取逻辑或时间归一化出问题了。质量报告能第一时间提醒你数据管道出毛病而不是等下游模型跑出离谱结果才回头找原因那个代价就大了。4. 实战踩坑记录与排查技巧这一章是我最想写的内容因为这些坑在官方文档里基本看不到但实际跑数据管道时几乎每个人都会撞上。4.1 时间戳混用毫秒、秒与 ISO 字符串并存我第一次跑清洗任务时以为 funding_time 都是毫秒时间戳直接用 astype(int64) 处理结果当天返回的数据里有几条是字符串格式脚本直接报错。后来把所有数据打出来看发现同一个字段至少有三种表达13 位毫秒时间戳、10 位秒时间戳、ISO8601 字符串。处理办法就是前面代码里写过的自适应函数先判断是不是字符串如果是字符串就走 pd.to_datetime如果是数值再看位数13 位按毫秒处理10 位按秒处理。别嫌这个函数啰嗦数据管道只要跑几个月什么格式都可能冒出来一次踩了坑后面就会学乖。4.2 429 限流拼命重试反而死得更快有一段时间我的抓取任务总在后半程挂掉日志里全是 429。一开始我用了简单的重试机制失败后立即重发结果一分钟内把限流额度彻底打满服务端开始返回更长的封禁提示。后来改成严格限频加指数退避才稳定下来。具体做法是正常情况下每个请求间隔 1.2 秒遇到 429 时先停 30 秒再按 2 秒、4 秒、8 秒的间隔重试最多重试 4 次。用 tenacity 库写这段逻辑会非常清爽它能通过装饰器把重试条件、等待策略、最大次数都配置清楚。from tenacity import retry, stop_after_attempt, wait_exponential retry(stopstop_after_attempt(4), waitwait_exponential(multiplier1, max8), reraiseTrue) def safe_request(client, **kwargs): return client.get_funding_rate(**kwargs)这里还想补充一个容易被忽略的点限流不只是请求频次问题还是服务端保护自身资源的手段。你的批处理场景里如果一次启动太多并发线程去请求很可能被误判为滥用。个人研究项目老老实实单线程加 sleep往往比并发轮询更省心也更容易维护。4.3 百分比字符串与浮点数混在一起的类型陷阱这是资金费率清洗里碰到过最隐蔽的 bug。字段部分值是 0.0001 浮点数部分值是“0.01%”字符串pandas 读进来后整列被推断成 object 类型。我一开始只做了 str.replace(“%”, “”)忘了除以 100结果所有带百分号的数据都比实际大了 100 倍而浮点数行的数值又是对的清洗完的均值和方差完全失常但报错却没有发生。排查时我用了一个笨但有效的办法把清洗前后的极小样本打印出来对比。一看带 % 的行和不带 % 的行差了 100 倍马上就想到了单位换算。这个坑教给我两个习惯第一单位转换函数单独写并加注释别和主流程揉在一起第二清洗完成后至少打印一条样本记录人工过目一眼机器认为“清洗成功”和数据真正正确是两回事。def convert_rate(x): s str(x).strip() if s.endswith(%): return float(s[:-1]) / 100.0 return float(x)4.4 边缘重叠导致的伪重复数据分批拉数据时相邻两个时间窗口的边缘时刻会被拉两遍。按理说 drop_duplicates 能解决但同一条数据在两次抓取里可能因为瞬时行情导致 price 字段不同所以按全部字段去重是去不掉的必须按唯一键去重。我的唯一键是 exchange、symbol、ts去掉重复后保留 source 优先级高或抓取时间更新的那行。pandas 的 drop_duplicates 默认保留第一条如果你想保留“最新抓取的版本”要先按抓取时间倒序排序再去重。很多人会发现“我明明去重了怎么数据量还是不对”多半是没调整保留顺序。加一个 _fetch_time 排序问题就消失了。df df.sort_values(_fetch_time, ascendingFalse) df df.drop_duplicates(subset[exchange, symbol, _ts], keepfirst)4.5 存储格式选不好清洗一遍又要重来我第一次用 CSV 存清洗结果跑了一周才发现打开文件后时间列全是字符串数字列也有部分变成 object害得我把所有数据重新洗了一遍。这不是清洗代码的问题是 CSV 本身不保留类型信息。换成 Parquet 后pandas 的 dtype 能原样保存读出来还是 float64、int64省掉了二次清洗的麻烦。如果项目必须用 CSV 和同事对接至少要加 encoding“utf-8-sig”否则 Excel 打开会乱码。不过如果要服务量化流程我的建议是中间层用 Parquet最后一层再导出 CSV 给人看。不要让 CSV 成为数据管道的主存储。4.6 清洗前后到底差多少给数据质量一个量化交代光说“清洗干净了”没有说服力我习惯每次任务结束后输出一张清洗效果表。这张表直接告诉我数据管道是否健康。下面是一份典型的质量报告样式指标清洗前清洗后总行数8124676320唯一键行数8124676890缺失核心字段24560异常标记行037时间覆盖2024-01-01 至 2025-01-01同左表格里的关键判断不是“行数少了很多”而是“该少的少了、该留的留了”。缺失核心字段降为 0异常标记出现 37 行说明清洗逻辑生效。如果某天清洗后行数大量减少而异常标记数量也在增加就要警惕是不是时间归一化把某个交易所的时间全部错开了。这种问题靠数据质量报告一眼就能看出来而不是等下游跑完才发现。5. 模块化设计带来的扩展能力5.1 抓取、清洗、存储三层如何解耦这套清洗模块我特意做成三层结构抓取层、清洗层、存储层。每一层只对上层提供接口不互相掺和。抓取层只负责把 CoinGlass 原始数据拉到本地不管字段干不干净。清洗层只负责把 DataFrame 整理成目标 Schema不管数据从哪来。存储层只管写入 Parquet 或 SQLite不关心上游逻辑。这样设计的好处是下一次想加入爆仓数据、持仓量变化、多空对比这些新指标只需要新增一个抓取类和一个清洗函数存储和质量校验的代码完全复用。5.2 模块化对项目维护的实际价值对个人项目来说这种模块化看似多写了几个文件但项目一旦跑起来你能明显感觉到维护成本的差别。改一个清洗规则不需要动抓取代码也不会影响已经落盘的历史数据抓取目标从 BTC 扩展到 ETH 时只需要在配置里加一个 symbol 参数。这些“后期省事”的舒适感都是前期多花一点时间设计模块边界带来的回报。我个人在实际操作中的体会是数据清洗模块看起来是体力活但它才是量化分析项目里最值得反复打磨的部分。数据管道稳定了分析想法才能真正落地数据管道天天出问题再好的策略逻辑都是在沙滩上盖房子。如果你也打算做类似的加密货币数据分析项目优先把时间统一、类型修正、唯一键去重这三个基本功打扎实后面会省心很多。这套结构我已经在本地跑通了完整流程下一步计划把清洗规则做成可视化配置界面让不懂代码的协作伙伴也能自己调整字段映射和异常阈值。
返回列表