简介:面向C#开发者的Tu Share财经数据接口调用指南,解决在.NET Core项目中快速获取股票交易信息的难题。Tu Share提供HTTP API,一次调用即可获取全市场数据,比传统逐股爬取效率提升明显。这份PDF文档系统梳理了从平台注册、Token获取、HTTP API参数说明,到C#端安装Microsoft.Extensions.Http与Newtonsoft.Json包、封装HttpClient POST请求的完整流程,并附带可直接套用的接口封装类与调用示例,帮助读者快速落地。资源为单份PDF文档,约212KB,轻量便于随时查阅。已有2346人学习下载,适合需要接入股票行情数据或构建量化分析基础的C#开发者。
1. C#接入TuShare:为什么说这是获取A股行情最稳的一条路
我接触过一个做选股工具的个人开发者,他最初靠网页抓取拿行情数据,结果对方页面一改版,整个解析逻辑全部作废,前后折腾了两周才缓过劲。后来他切到TuShare这类接口服务,发现用C#调HTTP接口拿股票交易信息,反而比爬网页省心得多。这就是本文要讲清楚的事:C#中利用TuShare获取股票交易信息,本质上是一个「HTTP请求 + JSON解析 + 本地落地」的过程,没有黑匣子,不需要特殊运行环境,更不用跟反爬机制搏斗。TuShare把A股日线、每日指标、交易日历、复权因子等数据整理成标准接口,你只需要按照约定格式发请求、收响应、做数据清洗。这篇文章适合那些想在C#项目里拿到真实A股行情做分析或做策略验证的开发者,也适合刚接触量化想找个可靠数据源的新手。
2. 对接前的三个基础准备:接口形态、token与最小请求骨架
2.1 接口风格:HTTP + JSON,C#天然好接
TuShare对外提供服务的方式是HTTP POST请求,请求体和响应体都是JSON。这一点非常关键,因为C#生态里处理JSON和HTTP的工具链非常成熟,System.Text.Json和HttpClient都是内置库,不需要额外引入第三方包,也不需要依赖某个特定的SDK。很多人在网上搜「TuShare C#」会找到各种各样的包装库,但那些库很多已经停止维护,接口参数也不一定跟得上,自己用HttpClient直接调反而更可控。
请求的基本结构是固定的:一个JSON对象里包含api_name、token、params、fields四个字段。api_name表示你要调用的接口名,比如daily表示日线行情,trade_cal表示交易日历;token是你注册账号后拿到的密钥;params是该接口的具体参数,比如股票代码、起止日期;fields是你想返回的字段列表,用逗号分隔。响应同样是一个JSON对象,里面code表示状态码,msg是错误信息,data里才是真正的数据。
2.2 注册拿token、搞清积分与限流的关系
使用TuShare之前必须先注册账号并创建应用,拿到一串token字符串。token就是你的身份凭证,每次请求都要带上。有一点需要提前知道:TuShare的接口频次跟账号积分挂钩,积分越高,每分钟允许的请求次数越多。基础积分刚注册时就能用,但频次限制比较紧,比如拉全市场几千只股票的数据时,如果不做控制,很容易在几十秒内就触到上限。
拿到的token要妥善保管。我一般会把token放到配置文件里,而不是写在代码里,这样代码提交到仓库时不会把密钥带出去。后面第3章会详细讲配置分离的做法。另外,token是不能分享的,别人拿到你的token就可以消耗你的调用额度,严重时还可能被平台封禁账号。
2.3 一个最小可跑的C#请求骨架
先看一个最基础的请求示例,它调用daily接口拉取某只股票一天的行情:
using System.Text; using System.Text.Json; var token = "你的token"; var apiUrl = "http://api.tushare.pro"; var payload = new { api_name = "daily", token = token, params = new { ts_code = "000001.SZ", trade_date = "20240115" }, fields = "ts_code,trade_date,open,high,low,close,vol,amount" }; using var client = new HttpClient(); var json = JsonSerializer.Serialize(payload); var content = new StringContent(json, Encoding.UTF8, "application/json"); var response = await client.PostAsync(apiUrl, content); var responseText = await response.Content.ReadAsStringAsync(); Console.WriteLine(responseText);这段代码的逻辑很直白:构造请求体,序列化成JSON,POST到接口地址,然后把响应当成字符串打印出来。关键参数有三个。api_name决定调哪个数据接口,这里是daily;params里的ts_code要写成6位代码加交易所后缀,000001.SZ是平安银行,.SZ表示深圳交易所,.SH表示上海;trade_date的格式是YYYYMMDD,注意这里传的是字符串"20240115",但响应里的trade_date字段返回的是整数。
跑通这个最小请求后,你已经完成了C#中利用TuShare获取股票交易信息的第一步。接下来要解决的是封装、解析和持久化的问题。
3. 封装一个可复用的TuShare客户端:请求方法、响应解析与配置管理
3.1 封装通用POST请求方法,避免重复写HttpClient逻辑
最小骨架跑通之后,如果每个接口都写一遍HttpClient逻辑,代码会非常冗余。更常见的做法是封装一个通用的请求方法,把api_name、params、fields作为参数传进去,方法内部负责序列化、发送、接收和错误码检查。
public class TuShareClient { private readonly HttpClient _httpClient; private readonly string _token; private readonly string _apiUrl = "http://api.tushare.pro"; public TuShareClient(string token) { _token = token; _httpClient = new HttpClient(); _httpClient.Timeout = TimeSpan.FromSeconds(30); } public async Task<JsonElement> RequestAsync(string apiName, object paramsObj, string fields) { var payload = new { api_name = apiName, token = _token, params = paramsObj, fields = fields }; var json = JsonSerializer.Serialize(payload); var content = new StringContent(json, Encoding.UTF8, "application/json"); var response = await _httpClient.PostAsync(_apiUrl, content); var responseText = await response.Content.ReadAsStringAsync(); using var doc = JsonDocument.Parse(responseText); var root = doc.RootElement; if (root.GetProperty("code").GetInt32() != 0) { throw new Exception($"TuShare接口错误: {root.GetProperty("msg").GetString()}"); } return root.GetProperty("data").Clone(); } }这个封装有几个点值得说明。HttpClient设置了30秒超时,因为TuShare某些接口在拉大量数据时会慢一些,超时太短容易误判失败。返回的JsonElement直接取自data节点,这样调用方只需要处理纯数据部分。code非0时抛出异常,msg里带具体错误原因,比如积分不足或参数非法。另外,把HttpClient建在类的内部,每次实例化会新建连接,如果调用很频繁,建议把HttpClient提升为单例或静态字段复用,避免端口耗尽的问题。
用一个异步方法作为入口还有一个额外的好处:在并发拉取多只股票的数据时,你可以用Task.WhenAll并行发起请求,同时配合频控逻辑限制并发数。这个后面第5章避坑部分会细讲。
3.2 从JSON到C#对象:TuShare的fields与items结构解析
TuShare返回的数据结构跟很多开放平台不一样,它不直接给对象数组,而是给两个平级数组:fields是字段名列表,items是数据二维数组。items里每一行是一条记录,行内每个元素对应fields里同下标字段。第一次拿到这种结构的人容易懵,以为需要猜字段名才知道数据顺序,实际上这是一一对应的。
解析思路有两种。一种是动态解析,直接把fields和items组装成List<Dictionary<string, object>>,适合快速看数据;另一种是定义实体类,把items里的元素按顺序填充到实体属性,适合后续入库或计算。
public List<Dictionary<string, object>> ParseData(JsonElement data) { var fields = data.GetProperty("fields") .EnumerateArray() .Select(f => f.GetString()) .ToList(); var items = data.GetProperty("items").EnumerateArray(); var result = new List<Dictionary<string, object>>(); foreach (var row in items) { var dict = new Dictionary<string, object>(); for (int i = 0; i < fields.Count; i++) { dict[fields[i]] = row[i].GetRawText(); } result.Add(dict); } return result; }这里把每个单元格统一用GetRawText转成原始文本,是因为TuShare返回里既有整数也有字符串,还有可能为空值,直接用GetInt32或GetString容易遇到类型不符抛异常。取到文本之后按需再做转换就行。字段顺序以fields数组为准,千万不要硬编码下标,因为同一个接口在不同时间点可能增加字段。
解析完成之后,数据就能映射到自定义的日线实体类了。实体类里trade_date建议用DateTime或int存原始整数都行,open、high、low、close价格用double,vol是成交量手数,amount是成交金额千元。vol和amount的精度问题后面实际入库时要注意。
3.3 把token放进配置:密钥分离是底线习惯
把token直接new在构造函数里只适合本地测试,工程化之后要放到配置文件。.NET里常见做法是放进appsettings.json,或者用环境变量,不让token出现在源码里。
{ "TuShare": { "Token": "你的token", "ApiUrl": "http://api.tushare.pro" } }读取时用ConfigurationManager或者IOptions注入都行。最朴素的做法是:
var config = new ConfigurationBuilder() .AddJsonFile("appsettings.json") .Build(); var token = config["TuShare:Token"];这样做的好处非常实际:代码提交到Git仓库时,token不会跟着源码泄露;换一台机器部署时,只需要改配置文件,不需要重新编译。如果团队协作,配置文件还能按环境区分,测试环境和生产环境用不同的token。另外,不要把token打到日志里,有些新手在处理异常时习惯把请求体整个打印出来,token就这么顺着日志流出去了。
4. 实战拉取:日线行情、复权因子与交易日历的配合使用
4.1 拉取日线数据并将字段对齐到本地实体
拿到了通用请求方法和解析逻辑,接下来拉日线数据就只是配置参数的问题。
var client = new TuShareClient(token); var data = await client.RequestAsync( "daily", new { ts_code = "600519.SH", start_date = "20240101", end_date = "20240131" }, "ts_code,trade_date,open,high,low,close,vol,amount" ); var rows = ParseData(data); foreach (var row in rows) { Console.WriteLine($"{row["trade_date"]} {row["open"]} {row["close"]}"); }这段代码拉取贵州茅台在2024年1月的日线行情。start_date和end_date都传YYYYMMDD格式的字符串,接口会返回闭区间内所有交易日数据。字段列表里没要的字段不会返回,可以减少传输体积。日常使用中,最常用的字段就是这七个:ts_code、trade_date、open、high、low、close、vol、amount。
有人在拉数据时容易忽略大小写,ts_code里的股票代码后缀必须大写,SH/SZ/BJ,否则接口直接报参数错误。还有start_date和end_date如果不传,默认返回最近一个交易日的数据,这个默认行为在写增量更新逻辑时很有用。
4.2 复权因子与前后复权:daily接口返回的是未复权价
有一个特别容易踩的坑:daily接口返回的开高低收是未复权价格。什么意思?就是上市公司分红送股之后,历史价格不会自动调整。如果你直接用这个价格画K线、算涨跌幅,遇到有除权除息的日子,K线图上会出现跳空,技术指标也会失真。
TuShare提供单独接口获取复权因子,也可以直接用pro_bar接口,它内部帮你算了复权逻辑。pro_bar不是通过api_name调用的通用接口,而是需要按特定参数组合调用。在C#里有两种做法:一是每次拉daily后额外拉一次复权因子,自己计算前复权价;二是直接调pro_bar接口。我一般用后者,简单直接:
var data = await client.RequestAsync( "pro_bar", new { ts_code = "600519.SH", start_date = "20230101", end_date = "20240301", adj = "qfq", asset = "E", freq = "D" }, "ts_code,trade_date,open,high,low,close,vol,amount" );参数说明:adj=qfq表示前复权,hfq是后复权,不传则返回未复权数据;asset=E表示股票,这个参数用于区分股票、指数、ETF;freq=D表示日线。前复权以最新价格为基准调整历史价格,后复权以最早价格为基准。做回测时用前复权更贴近实际交易视角,做除权除息研究时用后复权更方便。
4.3 交易日历:不要用自然日判断数据缺口
股票市场有休市日、节假日,A股交易日跟自然日并不重合。判断某一天该不该有数据,不能靠「今天是不是周末」这种逻辑,必须查交易日历。
var cal = await client.RequestAsync( "trade_cal", new { exchange = "SSE", start_date = "20240101", end_date = "20240401", is_open = "1" }, "exchange,cal_date,is_open" );这个接口返回指定交易所的日历状态,is_open传1表示只要开市日。拿到日历后,可以本地维护一个交易日列表,用来校验拉回来的日线数据是否有缺席。增量更新时也可以先查日历确定下一批要拉哪些日期,而不是盲目按自然天去请求。
实战里,把日历、日线、复权因子三者合并,才能构建出一份干净的本地方案。从TuShare拿到单价数据只是第一步,落库和校验才是让数据真正可用的关键。
5. 避坑:调用TuShare最常见的5个翻车现场与排查套路
5.1 返回成功却数据为空:先查限流再查日期
现象:请求返回code=0,但data里items为空数组,看起来像是当天没有交易数据。
原因有两种可能。第一种是撞上了接口限频,TuShare在高频请求时不会直接拒绝,而是静默返回空数据,这种情况最容易迷惑人。第二种是日期设置问题,比如传了周末或者非交易日,或者start_date填成了20240000这种不可能存在的日期,虽然通过了参数校验,但查不到任何记录。
解决:先在代码里把响应里的完整JSON打出来,确认data.fields和data.items是否存在突然为空。然后核对日历,确认你请求的日期确实是交易日。如果两个都没问题,大概率就是限流,降低请求频率或者等一分钟再试。
5.2 返回错误提示积分不足:不是代码问题,是账号权限问题
现象:请求日线接口返回「抱歉,您每分钟最多访问该接口X次」或者「权限不足」。
原因:TuShare不同积分对应不同的接口权限和频次权限。有些接口需要更高积分才能调用,或者每分钟调用次数受限。同一个token在不同时段也可能出现频次抖动。
解决:错误信息里会明确说限额是多少,先做自我保护,在客户端里加一个简单的令牌桶限速器,比如每次请求间隔不小于1秒。如果确实需要高频调用,只能通过官方渠道提升积分。代码层面能做的是把单次批量请求做大,减少请求次数,比如一次拉一整年的日线数据,而不是一天一天拉。
5.3 日期字段类型不一致:请求时字符串,响应时整数
现象:解析响应时用GetString读取trade_date,结果抛异常,或者字符串拼接时发现trade_date是数字。
原因:TuShare的请求参数里日期一般接受字符串,但响应数据里trade_date、cal_date这些日期字段返回的是整数。比如响应里trade_date是20240115,不是"20240115"。
解决:解析时统一用GetRawText先拿原始文本,再按需求转换。往数据库存的时候,建议直接存整数原始值,查询时用大于小于比较更高效。如果业务需要DateTime,可以自己写一个int转DateTime的辅助方法,把它格式化成DateTime之后再做日期计算。
5.4 vol和amount的精度问题导致入库数据错位
现象:把TuShare返回的vol字段存成int,发现某些股票的成交量数字跟行情软件对不上,或者amount字段小数部分丢了。
原因:TuShare的日线接口里,vol单位是手,amount单位是千元,而且这两个字段有时返回的是浮点数而不是整数。用int接收会导致精度丢失或溢出,尤其量大的股票。
解决:实体类里vol和amount都用double或者decimal定义。另外,跟行情软件对比时要确认单位一致,很多软件显示成交量是股,而TuShare给的是手,需要乘以100才等于股数。这个换算关系不搞清楚,算出来的换手率、成交额指标全是错的。
5.5 并发请求被限频:并行拉取数据时频繁遭遇空响应
现象:写了一个并发拉取全市场日线数据的程序,用Parallel.ForEach或Task.WhenAll同时发起几百个请求,跑着跑着数据就断了,大量返回空。
原因:TuShare服务端对单token的并发连接数有限制,超过一定并发,多余的请求就会被丢弃或排队超时。C#里Parallel.ForEach默认开的线程数可能远超接口容忍的并发量。
解决:用SemaphoreSlim限制最大并发数,比如控制在5以内,再配合每批次之间的延迟。不要图快一次性全怼上去。常规做法是把股票代码分批,每批50只,每只串行请求,批与批之间估算时间再启动下一批。宁可跑得慢一点,保证数据完整性。
6. 增量更新与本地校验:把TuShare数据沉淀成自己的K线库
数据落库之后,下一步就是让它持续更新。全量数据每次重拉不仅浪费时间,还容易触发限流。增量更新的思路很简单:本地记录每只股票的最新交易日,下次更新时把最新交易日+1作为start_date,拉到接口返回空为止。
var lastDate = GetLastTradeDate("600519.SH"); // 例如 20240115 var start = lastDate + 1; var data = await client.RequestAsync("daily", new { ts_code = "600519.SH", start_date = start.ToString(), end_date = DateTime.Now.ToString("yyyyMMdd") }, "ts_code,trade_date,open,high,low,close,vol,amount");这里的核心是GetLastTradeDate要可靠,如果本地库里没有这只股票的数据,就退化成拉全量。校验方面,我习惯每周做一次完整性检查:用trade_cal拿回应该存在的交易日列表,再跟本地库逐日比对,缺哪一天就补拉哪一天。
我还养成了个习惯:每次拉完数据,先把响应里最后一行的日期跟日历核对一遍,再把amount和收盘价跟行情软件抽查对比。数据源偶尔也会有小修小正,做量化回测之前最好把整段区间重新拉一遍,不要长期用旧数据将就。这一点是之前做模拟项目X时吃过亏的,数据里有一个月的除权日期没更新,回测结果偏差大得离谱,后来每次更新后都强制重拉最近三个月的日线,才算把这个问题压住。
希望这篇实战笔记能帮你在C#里顺利接上TuShare的数据,少踩几个我踩过的坑。
本文还有配套的精品资源,点击获取