简介:一套关于C#股票量化开发的资料包,面向具备基础C#语法、想要进入量化交易领域的开发者。资源以掘金量化接口为主线,讲解如何获取实时价格、历史K线、成交量等股票行情,并引入同花顺板块数据,涵盖板块指数走势、个股涨跌、成交额与成交量等维度,便于读者将行情与板块数据共同纳入策略分析。内容同时阐述了接口调用、JSON/XML数据解析、量化模型整合生成交易信号等关键步骤,对策略回测、风险限额、资金分配与异常处理等实战环节也有概括,帮助读者完整梳理从数据获取到交易决策的量化系统框架。整个rar压缩包约326MB,已有1140人学习,适合希望用C#对接掘金量化接口并应用同花顺板块数据的开发者,能为后续自行编写策略提供明确思路。
1. C#做股票量化:行情接口没搭好,策略全是空中楼阁
用C#编写股票量化策略,第一道坎往往不是策略模型,而是行情接口。你打开交易软件能看到的五档盘口、K线、板块涨幅,只要想拿来自动决策,就得有一个稳定的数据通道。自己去接交易所或券商的原始流,协议复杂且门槛高;去爬网页行情,又面临编码、反爬、字段频繁变更。掘金量化接口的价值在于,它把行情获取、K线拼接、Tick推送、历史数据补齐这些脏活封装成C#可以直接调用的API,让你把精力留在策略逻辑本身。这套资源围绕两条主线展开:一是用C#通过掘金接口订阅并组织行情数据,二是把同花顺版块数据转成板块强弱信号。适合有C#基础、想做板块轮动或事件驱动策略的开发者,也适合想从数据管道中解脱出来的个人研究者。
2. 先打通数据链路:掘金量化终端、C# SDK与Token配置
2.1 掘金量化终端在整条链路里的角色
很多第一次接触这套接口的人会误解它的架构,以为C#程序直连券商服务器。实际上,掘金量化终端才是整个数据链路的核心网关。你在终端里登录账号后,它负责与上游行情服务器保持长连接,同时在本机开启一个服务端口;你的C#策略进程通过SDK连上这个本地端口,订阅感兴趣的标的,终端就会把行情主动推送到策略进程里。
这个设计的直接好处是:你的策略不需要维护与行情服务器的连接、心跳、重连,终端掉了它会自己恢复。代价则是,程序启动前必须先打开终端并保持运行。常见做法是在同一台机器上跑终端和策略进程,回测时终端提供历史数据回放,实盘时终端对接券商柜台。所以整套链路启动顺序是固定的:先起终端,再启动你的C#策略项目。
2.2 引入SDK:引用DLL还是走NuGet
拿到这套资源包后,第一件事是让项目能引用到掘金SDK。我一般会先看包里的csproj文件,如果已经配好了依赖就直接编译;如果是从零新建工程,常见做法有两种。
第一,在Visual Studio里右键“依赖项”,选择“添加项目引用”,然后浏览到掘金量化终端安装目录下的SDK文件夹,选中对应的DLL文件确认即可。第二,如果你的版本支持NuGet包管理,可以在包管理器控制台里搜索掘金相关的SDK包名并安装。注意:不同SDK版本命名空间可能有差异,有的叫GMQuant,有的叫GMObjects,以你实际拿到的代码包为准。
添加引用后,在代码文件顶部写上命名空间:
using System; using GMQuant; // 掘金C# SDK命名空间,不同版本略有差异 public class DemoStrategy : StrategyBase { public override void OnInit() { // 示例代码用虚构标的占位,实盘换成你的真实证券代码 string symbol = "SHSE.800001"; Frequency freq = Frequency.Minute; Subscribe(symbol, freq); // 订阅分钟K线 Subscribe(symbol, Frequency.Tick); // 同时订阅Tick快照 } public override void OnBar(Bar bar) { // 每根K线形成后触发 Console.WriteLine($"[Bar] {bar.Symbol} {bar.Time} close={bar.Close}"); } public override void OnTick(Tick tick) { // 行情变动时触发,比OnBar更实时 Console.WriteLine($"[Tick] {tick.Symbol} last={tick.LastPrice}"); } }这段代码的逻辑很好理解:类必须继承掘金SDK里的StrategyBase基类,SDK启动后会调用OnInit方法,你在里面通过Subscribe订阅数据;之后K线和Tick数据会分别触发OnBar和OnTick回调。注意OnInit只执行一次,不要在OnTick里重复调用Subscribe。OnBar和OnTick的入参类型Bar和Tick由SDK封装,分别代表一根K线和一个行情快照,这些对象里的字段由SDK维护,不需要你手工拼装。
2.3 首次运行前的三处配置不能漏
第一处是Token。在掘金量化终端登录后,到终端设置页找到Token字段,复制出来,填到你的策略配置文件或环境变量里。不要把它直接硬编码进代码仓库,哪怕只是研究用,也要养成配置外置的习惯。
第二处是标的代码格式。掘金接口里上海标的带“SHSE.”前缀,深圳带“SZSE.”前缀。很多人第一次填代码忘了前缀,或把深圳写成“SH”,结果订阅后一直收不到数据。建议在配置层做一次格式校验,或者直接从掘金终端自带的标的列表里复制代码。
第三处是回测与实盘的周期参数。掘金SDK里的频率参数通常支持日线、分钟线、Tick等,命名在不同版本里稍有差异。资源包里一般会附带一个配置类,如果没有,建议自己维护一个全局配置对象,让回测和实盘共用同一份周期定义,避免两边参数不一致导致信号错位。
2.4 最小可运行示例:先跑通一个订阅
把上面的DemoStrategy编译运行之前,先把终端打开并登录。启动策略进程时,SDK会去连接本地终端端口,如果终端没开或Token不对,首条报错信息通常是连接超时或认证失败。跑通之后,控制台会持续输出K线和Tick信息,说明整条数据链路已经通了。
到这一步,你的C#量化工程已经具备了“获取股票行情”的基础能力。后面无论是做简单的均线策略,还是复杂的盘口分析,都建立在这个数据通道之上。所以这一章值得多花时间验证,不要急着写策略逻辑。
3. 行情接口的实时姿势:订阅K线与Tick,组装5秒级盘口快照
3.1 请求-响应与发布-订阅的差别
很多从爬虫转过来的开发者,习惯用“发起请求然后等返回值”的思路去理解行情接口。但掘金这套接口不是请求-响应模型,而是发布-订阅模型。你在OnInit里告诉SDK你想要什么数据,之后SDK和市场之间的数据流转完全由终端驱动,行情变动时会主动把数据包推到你的回调方法里。
这个差别非常重要。请求-响应模型适合低频查询,比如盘中每隔一分钟拉一次快照;但高频场景下,轮询的延迟不可控,而且频繁请求极易触发服务端的限流。发布-订阅模型则把延迟降到最低,每笔成交都能通过Tick回调第一时间送到你的策略里。代价是,如果你的策略处理速度跟不上行情推送速度,回调会产生堆积。常见解法是让回调里只做轻量更新,把计算和落盘放到独立线程。
3.2 订阅K线与Tick:代码对照
在掘金SDK里,K线和Tick是两种不同粒度的数据。K线是聚合数据,一根Bar包含开高低收和成交量;Tick是原始行情快照,包含最新价、买卖盘口等。二者在策略里的用途不同,但都可以通过Subscribe订阅。
// 订阅日线:适合做中低频趋势判断 Subscribe("SHSE.800001", Frequency.Daily); // 订阅1分钟K线:适合做日内择时 Subscribe("SZSE.800002", Frequency.Minute); // 订阅Tick快照:适合观察实时盘口变化 Subscribe("SHSE.800001", Frequency.Tick);K线的频率参数决定OnBar多久触发一次。常见的频率配置大概可以按下面的表来理解:
| 频率 | 数据粒度 | 典型用途 |
|---|---|---|
| Daily | 一根日K线聚合 | 波段策略、中期趋势 |
| Minute | 一根1分钟K线聚合 | 日内短线择时 |
| 自定义秒级/分钟级 | 按设定周期聚合 | 特殊周期策略 |
| Tick | 每笔行情快照 | 盘口监控、高频信号 |
命名细节不同版本的SDK有差异,但逻辑一致。回测时,K线聚合由终端根据历史数据现场生成;实盘时,K线是行情过来后终端实时聚合的,OnBar触发时机取决于当前K线是否走完。如果一个周期内没有成交,部分版本不会触发该周期的OnBar,这一点在算信号时候要格外小心,容易造成“看起来少了一根K线”的现象。
3.3 用Bar还是Tick组装盘中快照
如果你想做5秒级的盘口监控,只订阅K线是不够的,因为分钟K线最短也是一分钟一根。更实际的做法是订阅Tick数据,然后在策略内部自己聚合5秒快照。常见做法是维护一个ConcurrentDictionary保存每个标的最新Tick,再用一个定时器每隔5秒把当前字典内容固化成一张快照。
using System.Collections.Concurrent; using System.Timers; public class SnapshotBuilder { private readonly ConcurrentDictionary<string, Tick> _latestTicks = new(); private readonly Timer _timer; public SnapshotBuilder() { _timer = new Timer(5000); // 每5秒触发一次 _timer.Elapsed += (s, e) => { var snap = BuildSnapshot(); // 在这里把snap交给策略层计算信号 }; _timer.Start(); } public void OnTickReceived(Tick tick) { // 只保留每个标的最新一笔,防止字典无限膨胀 _latestTicks[tick.Symbol] = tick; } public Snapshot BuildSnapshot() { var snap = new Snapshot(); foreach (var item in _latestTicks) { snap.Rows.Add(new SnapshotRow { Symbol = item.Key, LastPrice = item.Value.LastPrice, Volume = item.Value.Volume, Amount = item.Value.Amount, BuyPrice = item.Value.BidPrice, SellPrice = item.Value.AskPrice }); } return snap; } }这段代码的关键在于OnTickReceived只做字典更新,不做任何计算,避免阻塞回调线程。定时器每5秒固化一次快照,策略层拿到的永远是一个完整的最新盘口状态。注意ConcurrentDictionary是线程安全的,OnTick回调和定时器回调在不同线程执行时不会出现集合被并发修改的异常。Volume字段是当日累计成交量,不是5秒内的增量;有些人误把它当作区间成交量,导致信号算错,这一点在盘口分析时尤其容易踩。
3.4 数据对齐与时间字段的处理
拿到Tick和Bar之后,第一个要养成的好习惯是统一时间基准。掘金接口回传的时间字段通常是Unix时间戳,直接转成C#的DateTime,默认是UTC时间。国内A股行情要的是北京时间,转换时一定要做时区校正,否则你记录的信号时间和K线时间会差8小时,复盘时对不上。
另一个是对齐问题。Tick的触发时间是不固定的,后一个Tick和前一个Tick之间可能间隔几十毫秒,也可能隔几分钟。你聚合5秒快照时,要记录快照的生成时间,而不是取某笔Tick的时间作为整张快照的时间。否则在高频行情下,同一张快照里的行可能带上好几秒的时间差,复盘时排序会乱。
4. 同花顺版块数据怎么接:从板块涨幅到行业权重打分
4.1 版块数据与行情接口的关系
掘金接口主要解决个股行情,而板块数据更多用于判断市场风格和行业热度。同花顺的板块体系分行业板块和概念板块,每个板块有当日涨幅、涨跌家数、领涨股等字段。做板块轮动策略时,与其自己在掘金接口里把几十只个股算出一个平均涨幅,不如直接拿到板块聚合数据,再映射到个股上。
这套资源里的做法是把板块数据源独立出来,通过公开的行情页面接口抓取。需要注意一点:板块接口的分页、字段类名和编码格式都可能随版本更新变化,所以抓取层要设计得足够薄,不要让策略核心逻辑依赖页面上某个冒号的位置。
4.2 用C#拉取板块快照并解析
板块抓取最常见的问题是乱码。很多行情站页面默认是GBK编码,用HttpClient的ReadAsStringAsync直接按UTF-8读,出来的全是“锟斤拷”。正确做法是先读原始字节,再指定编码解码。
using System.Net.Http; using System.Text; using System.Text.RegularExpressions; public class SectorFetcher { private static readonly HttpClient Client = new HttpClient(); public async Task<SectorQuote> FetchSectorAsync(string sectorCode) { // 占位URL:实际地址以你抓包得到的为准,不要写死页面结构 var url = $"http://example-quote-api.com/sector/{sectorCode}"; var request = new HttpRequestMessage(HttpMethod.Get, url); request.Headers.Add("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64)"); // 先取字节数组,避免ReadAsStringAsync默认按UTF-8解码 var response = await Client.SendAsync(request); var bytes = await response.Content.ReadAsByteArrayAsync(); var html = Encoding.GetEncoding("GB2312").GetString(bytes); // 用正则抽取板块名称和涨跌幅,具体字段以实际页面为准 var nameMatch = Regex.Match(html, "<span class=\"zq-name\">(.*?)</span>"); var pctMatch = Regex.Match(html, "<span class=\"zf-pct\">(.*?)</span>"); return new SectorQuote { Code = sectorCode, Name = nameMatch.Groups[1].Value, ChangePct = decimal.Parse(pctMatch.Groups[1].Value) }; } }这段代码里有两个关键决策。第一是User-Agent必须设置,很多页面会拦截无UA的默认请求,返回一个验证页而不是真实数据。第二是正则的规则越短越好,只抽取数值类字段,不要在同一个正则里同时匹配太多内容。如果页面结构复杂,推荐改用HtmlAgilityPack这类HTML解析库,配合XPath取节点,比正则更容易维护。但解析库会引入额外依赖,资源包如果没带,建议自己权衡。
4.3 把板块涨幅量化成策略权重
拿到板块涨幅数据后,不能直接拿它去下买单,还需要把“涨幅排名”翻译成“仓位权重”。这里我给出一套很简单的线性加权方案:把板块按当日涨幅从高到低排序,排名越靠前权重越高,最后归一化到总和为1。
public class SectorQuote { public string Code { get; set; } public string Name { get; set; } public decimal ChangePct { get; set; } } public class SectorWeight { public string Name { get; set; } public decimal ChangePct { get; set; } public decimal Weight { get; set; } } public static List<SectorWeight> CalcWeights(List<SectorQuote> quotes) { var sorted = quotes .OrderByDescending(q => q.ChangePct) .ToList(); int count = sorted.Count; var weights = new List<SectorWeight>(); // 线性打分:第一名得count分,最后一名得1分 for (int i = 0; i < count; i++) { weights.Add(new SectorWeight { Name = sorted[i].Name, ChangePct = sorted[i].ChangePct, Weight = count - i }); } // 归一化,让所有权重相加等于1 decimal total = weights.Sum(w => w.Weight); for (int i = 0; i < weights.Count; i++) { weights[i].Weight = weights[i].Weight / total; } return weights; }这个方法的逻辑很直观:从最强的板块到最弱的板块,权重依次递减。如果你想放大头部效应,可以把线性分改成平方分,也就是权重按(count - i)的平方计算,这样第一名会拿到更大权重。如果你的股票属于某板块,就用该板块的权重作为这只票的行业加分项,叠加到个股自己的技术信号上。注意板块归属要维护一张静态映射表,比如“股票代码 -> 板块代码”,这个映射关系不需要实时抓,能容忍半小时甚至一天的更新频率。
4.4 缓存与更新频率的权衡
板块数据不需要像Tick那样追求实时,每5分钟抓一次基本够用。但抓取动作要放在独立的后台任务里,不要让策略主流程直接发起HTTP请求,否则行情回调会等网络响应,造成信号延迟。
我一般会用CacheManager维护一个板块快照字典,后台任务定时更新,策略层只读字典里的最近一份快照。这样可以实现“行情实时、板块准实时”的组合,既保证了策略对个股行情的敏感度,又不给板块源增加不必要的请求压力。如果抓取时发现某个板块的涨幅缺失,不要用0填充,直接把该板块从本次权重计算里剔除,避免污染整个打分结果。
5. 避坑清单:接行情接口时最容易翻车的五个细节
5.1 一启动就报连接失败
现象是策略进程刚跑起来,控制台立刻抛出连接超时或认证错误,行情数据一条都收不到。原因通常不是代码错误,而是终端没启动,或者Token没有正确写入配置。掘金SDK默认连接本机终端端口,终端进程没起来,SDK连上的自然是个空地址。解决方法是把启动顺序固定:先启动掘金量化终端,确认终端界面进入正常登录状态,再启动C#策略进程。Token要检查末尾有没有误带入空格,复制时很容易带上换行符,直接导致认证失败。
5.2 订阅重复导致限频
现象是策略运行一段时间后突然收不到行情,终端侧出现频率限制提示。原因多是策略在重连或者OnInit被多次调用时重复执行Subscribe,服务端检测到同一标的重复订阅,触发了风控。解决方法是维护一个全局HashSet,每次订阅前先判断是否已存在。
private readonly HashSet<string> _subscribed = new HashSet<string>(); private void SubscribeOnce(string symbol, Frequency freq) { if (!_subscribed.Add(symbol)) { // 已经订阅过,避免重复订阅触发服务端限频 return; } Subscribe(symbol, freq); }这段代码利用HashSet.Add的返回值判断是否首次加入,如果是false就直接跳过,保证每个标的只被订阅一次。这里的判断粒度是字符串,不是频率;同一个标的如果要订阅不同周期,建议用“symbol + frequency”作为键。
5.3 板块数据中文乱码
现象是抓回来的板块名称变成乱码,最典型的是“锟斤拷”三个字反复出现。原因是页面本身是GBK编码,HttpClient默认解码用UTF-8,两套字符集对不上。解决方法是先拿到字节数组,再手工指定编码解码,也就是4.2节代码里的写法。另外请求头里的Content-Type不要和响应编码混为一谈,它只是告诉客户端这是什么类型,不代表就是UTF-8。出现乱码优先查响应实际编码,而不是反复换正则表达式。
5.4 K线没走完就触发信号
现象是策略在盘中频繁出现信号,收盘后复盘却发现信号质量很差,很多笔在K线末端被反转打掉。原因可能是部分SDK版本的OnBar在K线更新时触发,而不是等到K线闭合才触发。如果你拿到的是这类版本,OnBar里的Bar可能不是最终数据。解决方法是判断K线是否闭合,具体属性名不同版本有差异;拿不准时,宁可把信号延迟到下一根K线确认后再触发,也不要在一个未定型的Bar上开仓。实时性让给Tick策略,准确性留给K线策略,不用强求一个回调同时满足两种需求。
5.5 历史K线与行情站对不上
现象是用历史接口回测某只票,算出来的涨跌幅和行情站看到的不一样,差一点但不至于离谱。原因多出在复权设置上:前复权、后复权、不复权的价格序列差异很大,接口默认策略往往和行情站默认展示不一致。解决方法是显式指定复权参数,并在数据落地时把复权类型写进字段名或表名,例如文件名带qfq后缀。不要相信默认参数,显式传参本身就是一种日志记录。
6. 验证与进阶:当日行情落盘CSV,给实盘留后悔药
6.1 盘口快照与信号落盘
很多人在回测里跑得很漂亮,一上实盘就发现“为什么今天触发不了”。最大的问题不是策略逻辑,而是缺少一份盘中数据日志。回测时数据是静态的,可以反复验证;实盘时行情是一闪而过的,没有落盘日志,出了信号异常你连复盘的素材都没有。我的习惯是:所有进入策略层的快照,都落一份CSV备查。
public static void AppendToCsv(string path, SnapshotRow row) { var line = string.Format("{0:yyyy-MM-dd HH:mm:ss},{1},{2},{3},{4}", DateTime.Now, row.Symbol, row.LastPrice, row.Volume, row.Amount); File.AppendAllText(path, line + Environment.NewLine); }调用点放在定时器生成Snapshot之后,策略层计算信号之前。即使策略算错,你手里也有一份最原始的数据可以用来复盘。CSV文件按日期命名,每天一个,别把所有数据写进同一个文件,否则一个月后文件打开都费劲。字段数量也不要贪多,够复盘就行:
| 字段 | 含义 |
|---|---|
| time | 快照生成时间,北京时间 |
| symbol | 标的代码 |
| price | 最新价 |
| volume | 当日累计成交量 |
| amount | 当日累计成交额 |
6.2 用历史接口回放昨天的信号
落盘以后怎么用?我一般会在第二天开盘前,用掘金的历史接口把昨天同一时段的行情拉出来,把策略的优先级从高到底重新执行一遍,对比昨天实盘记录和回放结果的差异。如果实盘信号比回放多,多半是重复订阅或脏数据引发的问题;如果少,多半是漏了某个时段的Tick。
这套验证方法不复杂,但能有效帮助你在上实盘前挡住大部分数据层问题。从那以后我每次换新接口或改数据源,都会先把它写成静态落盘工具跑一晚上,第二天拿着CSV去和行情站逐条对,对上了再谈策略,对不上就换方案。整个过程很土,但帮我挡掉不少玄学数据事故,希望帮到你。
本文还有配套的精品资源,点击获取