简介:国外基于C#的TradeLink量化交易平台是一套面向量化交易开发者的开源架构,主要服务有C#功底、希望自主构建自动化交易策略的中高级程序员,也适合金融IT从业者研究订单管理与风控模型。压缩包大小约32.44MB,内部文件统计当前为0项,文件类型明细暂未同步,建议下载后先查看根目录结构与编译说明。已有461人学习下载,对个人研究和小型团队选型有一定参考价值。该平台将行情接收、策略逻辑、订单发送与仓位管理解耦为独立层,通过事件驱动机制实现快速响应,阅读其接口定义与扩展点,有助于理解量化框架的常见实现思路,也能为自研系统提供模块划分和异常处理上的参考。无论是用于学术研究、个人量化实践,还是团队技术选型,都能从中获得可落地的设计参考。
1. 为什么说TradeLink是C#程序化交易绕不开的一套开源框架
TradeLink是一套源自海外的开源量化交易平台框架,核心是用C#把行情接收、策略计算、下单交易串成一条消息管道。我最早也是先学的Python回测,等到真要对接实盘券商时才发现,不少柜台网关的官方示例代码本身就是C#,而TradeLink的价值在于把那种割裂的对接方式统一成了事件驱动模型。它帮你解决的不是“怎么写策略”,而是“策略怎么安全地连通真实市场”:接收Tick、维护持仓、发送订单、处理回报。适合人群是至少会基础C#、想在Windows/.NET生态里跑自动化交易的开发者和量化爱好者。用它搭一个能收行情、能下模拟单的Demo,通常两三个小时能跑通,但后面的线程、时间戳、订单状态才是真正见功底的地方。
2. TradeLink的消息驱动拆解:报价、订单、账户状态是怎么在代码里流动的
2.1 五个关键对象先分清:Security、Tick、Order、Response、Portfolio
很多初次看TradeLink源码的人会被一堆命名空间吓住,其实核心对象不超过五个。先把它们当成“消息信封”来理解,后面所有代码都是这些信封的收发。
Security是品种定义,包含代码、交易所、合约乘数、最小变动价位。Tick是行情快照,包含bid、ask、last、volume和时间戳。Order是你要发出去的委托,包含方向、价格、数量。Response是券商或交易所回传的订单状态,可能是已提交、部分成交、完全成交或已拒绝。Portfolio是账户的持仓和资金快照。
它们之间的关系是单向流动的:行情进入策略,策略生成Order,Order通过Broker发出去,Broker收到回报后打包成Response通知策略,同时Portfolio被更新。TradeLink的“平台感”就来自这套固定协议,你换券商只需要换掉最底层的Broker实现,策略代码不需要动。
2.2 事件线程模型:为什么说TradeLink本质上是单线程消息泵
TradeLink的一个设计特征是事件驱动。它的Broker对象会开一个后台线程去拉取底层网关数据,把收到的Tick、Response、Portfolio变化都通过C#的event抛出来。你的策略代码则运行在另一个线程上,通常是主线程或你自己起的循环里。
理解这一点非常重要,因为这里埋了第一个经典坑:在事件处理函数里直接操作UI控件或共享集合,会碰到跨线程访问问题。但反过来讲,如果策略上下文不复杂,这种单线程消息泵反而安全,它天然避开了多线程并发下持仓被两个订单同时改坏的问题。
很多C#开发者会不自觉地用Thread.Sleep或阻塞调用来等数据,这会让消息泵卡死。正确做法是让事件函数尽快返回,把计算逻辑放到队列里异步处理。你可以在自己的策略类里维护一个Queue ,在Tick事件里只做入队,然后由主循环统一出队发送,这样线程模型就清晰了。
2.3 引用TradeLink的两种方式:NuGet包和源码编译怎么选
常见做法是用NuGet引入编译好的包,省时省力。打开Visual Studio的包管理控制台,或者直接用dotnet CLI,在我的经验里NuGet方式适合大部分人:
dotnet new console -n TradeLinkDemo cd TradeLinkDemo dotnet add package TradeLink --version "请替换成NuGet上可见的稳定版本号"这段命令创建了一个控制台项目,并引用了TradeLink的核心包。版本号这里我没有写死,因为不同时期的包版本对应不同的.NET框架,你需要在NuGet页面上看它支持的target framework。如果项目目标是.NET 6及以上,选包含net6.0/net8.0的那个包版本最稳妥。
如果你要自己改Broker源码,或者需要对接某个国内没有正式包的券商网关,那就得走源码编译。源码编译的坑主要在项目引用层级上:TradeLink包含多个子项目,有些子项目依赖第三方库,直接打开sln编译会报找不到引用。我的习惯是只把TradeLink.Common和TradeLink.XLib两个核心项目放进自己解决方案,再手动添加依赖,不要整个sln拖进来。判断到底需要哪个子项目,看你策略代码用了哪些命名空间。如果只用到TradeLink.API和TradeLink.Common,就只引用这两个。
3. 从零搭一个TradeLink实盘桥接:控制台项目、模拟账户与最小订单
3.1 先准备好连接参数:模拟网关通常监听本地端口
做实盘桥接前,我强烈建议先找模拟账户。很多国外券商会提供模拟交易网关,下载后是一个本地服务程序,它会监听你指定的端口。比如某模拟网关默认监听localhost的12345端口,账号是demo,密码是demo123。你先启动那个网关,然后再启动你的TradeLink程序,让它去连接这个端口。
连接参数通常包括服务器主机、端口、账号、密码,有的还有加速倍数。加速倍数是模拟盘特有的,用来把行情和成交时间压缩,方便你快测策略,但实盘没有这个参数。我用过的模拟网关里,参数大致如下表所示:
| 参数 | 示例值 | 说明 |
|---|---|---|
| 服务器地址 | 127.0.0.1 | 本地模拟网关通常回环地址 |
| 端口 | 12345 | 与网关设置保持一致 |
| 账号 | demo | 区分模拟和实盘账号 |
| 密码 | demo123 | 某些网关不校验 |
| 加速倍数 | 5 | 只有模拟网关支持,实盘必须设1 |
不要一开始就用真实资金账户来调试代码,这是量化开发的基本卫生习惯。模拟网关能模拟绝大多数成交回报,包括部分成交和拒单,足够暴露代码里的逻辑问题。
3.2 最小可运行代码:订阅Tick并打印行情
下面这个程序是TradeLink初体验的最短路径。它连接本地模拟网关,收到Tick后打一行字符。不要小看这一步,它能验证你的环境、网络、事件链路都是通的。
using System; using TradeLink.API; class Program { static void Main(string[] args) { // 创建Broker实例,参数对应模拟网关的地址、端口、账号、密码 var broker = new Broker("127.0.0.1", 12345, "demo", "demo123"); // 订阅Tick事件,每个行情更新都会触发这个回调 broker.Tick += (Tick t) => { // Tick里的Symbol是合约代码,Bid是买价,Ask是卖价,Trade是最近成交价 Console.WriteLine($"{t.Symbol} bid={t.Bid} ask={t.Ask} last={t.Trade}"); }; // 启动连接并进入事件循环 broker.Start(); Console.WriteLine("Press any key to stop..."); Console.ReadKey(); broker.Stop(); } }这段代码里的Broker类构造参数是最小集,真实网关可能还要求传账户类型或客户端ID。我的建议是先连上再谈细节,只要Start方法不抛异常,就说明TCP链路通了。Tick里Bid和Ask为0时,通常是某些品种只有最新价没有盘口,不要误判为断线。
3.3 发一笔最小订单:Order和Response的正确用法
行情通了之后,就可以试着下模拟单。下面代码在程序启动后3秒发出一个市价买单,并把返回状态打出来:
using System; using System.Threading; using TradeLink.API; class OrderDemo { static void Main() { var broker = new Broker("127.0.0.1", 12345, "demo", "demo123"); var order = new Order { Symbol = "AAPL", Side = Side.Buy, Qty = 100, Price = 0 // Price为0表示市价单 }; // 处理回报事件 broker.Response += (Response r) => { Console.WriteLine($"Order {r.OrderID} status={r.Status} filled={r.FilledQty}"); }; broker.Start(); Thread.Sleep(3000); // 等行情连接稳定后再下单 broker.SendOrder(order); Thread.Sleep(5000); broker.Stop(); } }这段代码里的Order对象是TradeLink.API中的标准结构。Side.Buy表示买入,Qty是股数,Price为0时按市价处理。实际使用中你会发现部分券商的市价单类型不完全一样,有的要求显式写明OrderType.Market。Response里的Status字段是个枚举,常见取值包括Submitting、Submitted、PartFilled、Filled、Cancelled、Rejected。调试时优先看Rejected和PartFilled两个状态,它们最能反映问题。
3.4 实盘桥接前为什么要先跑数据校验
很多新手做完上面两步就去接实盘,结果挂在合约代码和最小变动价位上。我的习惯是先跑30分钟模拟盘,记录每一次响应的OrderID和成交价,自己算一笔账:如果按键延迟超过某个阈值,就要考虑机器性能。这一步看着不起眼,却能在实盘前暴露90%的低级错误。
4. 用历史CSV驱动TradeLink回测:Bar事件、费用参数与滑点修正
4.1 回测模式的核心:把历史数据伪装成实时Tick喂给策略
TradeLink本身不提供回测引擎,但它给了你一套“数据管道复用”的能力。常见做法是读取历史CSV文件,按时间顺序生成Tick或Bar对象,然后直接调用策略的对应事件处理函数。这样你的实盘策略和回测策略是同一份代码,不会出现“回测用的是别的一套逻辑”这种尴尬情况。
我最常用的是Bar回放。Bar是指K线,包含Open、High、Low、Close、Volume和时间。如果你有1分钟K线的CSV,那么回测循环大概长这样:
using System; using System.IO; using TradeLink.API; class Backtest { static void Main(string[] args) { // 假设CSV格式:时间,开盘,最高,最低,收盘,成交量 var lines = File.ReadAllLines("aapl_1m.csv"); foreach (var line in lines) { var parts = line.Split(','); var bar = new BarImpl { Time = DateTime.Parse(parts[0]), Open = decimal.Parse(parts[1]), High = decimal.Parse(parts[2]), Low = decimal.Parse(parts[3]), Close = decimal.Parse(parts[4]), Volume = int.Parse(parts[5]) }; // 把Bar推送给策略处理函数,策略可能在这里判断并生成订单 MyStrategy.OnBar(bar); } } }这段代码没有依赖TradeLink的回放组件,而是直接用CSV驱动策略。好处是完全可控,坏处是你得自己处理Bar推送给策略的时机。如果策略内部依赖“前一根Bar收盘后再下一根Bar开盘前下单”的时序,你需要手动控制循环里的逻辑。
4.2 回测里的三个关键参数:手续费率、滑点与合约乘数
实盘和回测的差距,几乎都来自这三个参数设得不准。手续费率按你券商的实际标准填,例如每股0.005美元。滑点是市价单在行情快速移动时实际成交价和预期价的差,我一般保守地设为固定0.01美元或0.02个最小变动价位。合约乘数尤其重要,股指期货和加密货币永续合约的乘数差异极大,设错会让盈亏金额差好几个数量级。
| 参数 | 我的默认值 | 调参建议 |
|---|---|---|
| 手续费率 | 0.005 USD/股 | 按实际结算单核对 |
| 滑点 | 0.015 USD | 高波动品种加大到0.03 |
| 合约乘数 | 1(股票) | 期指按交易所规定填 |
| 最小变动价位 | 0.01 USD | 从Security信息读取 |
4.3 回测后先看订单流而不是只看权益曲线
我见过太多人盯着回测净值曲线兴奋半天,却不检查回测里的订单有没有被不合理成交。正确做法是打印每一笔订单的时间和价格,手动确认:如果策略在Bar收盘价上成交,而收盘价用的是当根Bar的Close,那么这就是典型的未来函数。TradeLink回放模式下你完全可以做到严格累积数据:只让策略看到当前Bar及更早的数据,永远不要让它看到“下一根Bar的Close”。
5. TradeLink接入不顺的三个根因:连接、时间戳与订单状态排查
5.1 行情事件就是不触发?检查连接启动顺序和线程
现象:绑定Tick事件后,控制台没有任何输出,程序也不报错。
原因:最常见的是Broker.Start()调用前没有等到底层网关完全就绪,或者Tick事件里的代码抛了异常却被吞掉。其次是网关设置了固定端口漂移,你连的端口不对。
解决:先在Start之后调用一个探测方法,例如手动请求一次账户信息,如果也没有响应,基本可以确定连接没建立。另外,在Tick回调里加上try/catch,把异常打印出来,避免静默失败。我排查这类问题时,会先去掉一切事件逻辑,只打印一行“tick received”,如果这行都没有,那问题在连接层。
5.2 断线重连后持仓数量对不上,策略和账户出现偏差
现象:策略跑了一整天也没报错,结果收盘发现策略认为有200股,账户实际只剩100股。
原因:断线期间成交回报丢失,策略没有收到部分成交或撤单成功的消息,自动同步机制没有触发。
解决:断线重连后主动拉取一次Portfolio完整快照,用自己的持仓对象去覆盖本地缓存,而不是基于增量回报不断加减。下面是一段典型的同步逻辑:
broker.Error += (string msg) => { // 检测到连接错误时,记录状态 Console.WriteLine("Connection error: " + msg); }; broker.Portfolio += (Portfolio p) => { // 完整快照来了,直接覆盖本地持仓表 localPositions = p.Positions; };注意这里的Portfolio事件是推送完整快照,不是增量。如果连的是某些模拟网关,这事件可能只在连接成功后触发一次,你要主动请求才可以。我的习惯是每次重连后调用一次RequestAccountStatus,再做核对。
5.3 时间戳用时区混乱导致回测和实盘行为不一致
现象:同一套策略在历史回测上非常稳定,接到实盘数据后就频繁交易,完全变了个样。
原因:CSV里的时间是交易所本地时间,而实时Tick用的是电脑本地时间,比较条件在跨越夏令时或时区时出现偏移。
解决:统一在策略内部使用UTC时间。读取CSV时把“2024-01-05 09:30”显式指定为交易所时区再转UTC,绝不能和DateTime.Now直接比较。TradeLink的Tick结构里自带Time字段,很多网关传的是UTC毫秒戳,建议直接用它。
5.4 限价单部分成交后,策略还按照原数量计算可用资金
现象:下单200股只成交了80股,策略却以为全部成交,后来继续加仓导致可用资金不足被拒。
原因:Response返回时只处理了完全成交,忽略了PartFilled事件。
解决:每次响应都先读CumQty(累计成交量),再用原始Order.Qty减去CumQty得到剩余量。对于未完全成交的订单,可以选择撤单后重下剩余量。一个稳妥的模板是:只要Response.Status是PartFilled或Filled,就用累计量核对策略内部的订单字典,不要依赖状态字段做减法。
5.5 订单被拒:最小变动价位设错是最高频原因
现象:提交限价单总是得到Rejected,查询原因时显示价格不符合步进要求。
原因:我们常把价格硬编码成0.01,但某些品种最小变动价位是0.25或0.5。TradeLink里的Security对象带有TickSize属性,但很多策略代码为了省事写死了步长。
解决:下单前用Security里的TickSize规范化价格,例如先取价格整数部分,再对TickSize做舍入。我用这段代码来判断价格是否合法:
bool IsValidTick(decimal price, Security sec) { // 价格必须是TickSize的正整数倍 decimal ticks = price / sec.TickSize; return Math.Abs(ticks - Math.Round(ticks)) < 0.0001m; }这个排查动作应该在模拟盘阶段完成,别留到实盘。尤其在外盘品种上,很多交易所对价格小数位数有严格限制,拒单就是最直接的成本。
6. 上线前最后一步:给TradeLink加一条心跳监控日志
实盘跑得久了你会发现,最危险的往往不是策略逻辑,而是连接“半死不死”的状态——表面上TCP没断,但行情和订单延迟越来越高。所以我上线前一定会给TradeLink加一个心跳监控:周期性向Broker发送一个账户状态请求,测量从发出到收到Response的往返延迟,并记录到日志。
using System; using System.Diagnostics; using System.Threading; using TradeLink.API; class HeartbeatMonitor { static long _lastPingTime; static void Main() { var broker = new Broker("127.0.0.1", 12345, "demo", "demo123"); broker.Response += (Response r) => { if (r.Status == ResponseType.RequestAccountStatus) { var latency = Stopwatch.GetTimestamp() - _lastPingTime; Console.WriteLine($"Heartbeat latency: {latency} us"); } }; broker.Start(); var timer = new Timer(_ => { _lastPingTime = Stopwatch.GetTimestamp(); broker.RequestAccountStatus(); }, null, 0, 30000); Console.ReadKey(); } }这段代码每30秒发起一次账户状态请求,通过回复的时间差估算链路延迟。如果发现延迟超过500毫秒,就该考虑触发报警或暂停自动开仓。我喜欢把每次心跳延迟写入一个本地文本文件,这样第二天可以检查夜里是否发生过长时间延迟尖峰。
我有一次做模拟盘迁移,连续三晚的回测结果都正常,但上线后第一周就遇到某天数据源延迟突然从200毫秒涨到5秒,策略还在继续按旧的Tick触发订单,导致连续三次追单。最后就是靠心跳日志定位到那天晚上网关内部发生了重连。从那以后,所有TradeLink项目都把这个心跳监控当作标配,宁可策略少赚一点,也不要因为链路延迟亏掉控制感。希望这个习惯也能帮到你。
本文还有配套的精品资源,点击获取