尧图网络 高端网站定制 · 原创设计
免费咨询热线
400-888-6620
免费获取方案
C#实战:从零构建股票交易模拟系统,掌握撮合引擎与账户结算
简介这是一份面向C#初学者与.NET开发者的股票交易模拟系统源码适合希望通过完整项目实战掌握桌面应用开发、数据库操作与业务逻辑实现的学习者。系统提供图形化界面与数据库支持可进行虚拟股票买卖模拟真实市场环境。压缩包共63个文件约278KB以21个cs源码文件为核心配合8个resx与9个resources资源文件构建界面8个accdb数据库文件存储用户与交易数据另含csproj、sln等工程配置及exe可执行文件结构完整可直接运行。目前已有1765人学习下载。内容涵盖登录注册、行情展示、用户信息管理、交易记录查询等模块涉及Windows Forms界面设计、ADO.NET数据库访问、订单撮合与买卖确认等业务规则并体现MVC分层与安全性处理思路是理解C#项目架构与交易系统实现的实用参考。1. 用 C# 搭一套股票交易模拟系统到底能练到什么程度很多人学 C# 卡在“语法都会、项目不会写”这一步。语法书翻了三遍LINQ 写得飞起真让搭一个带撮合、持仓、盈亏计算的完整系统立刻不知道从哪下手。股票交易模拟系统就是填这个坑的它把订单、撮合、账户、行情、风控串成一条完整链路逼着你处理并发、精度、状态一致性这些真实工程问题。这份资源适合两类人——刚学完 C# 基础想找个有业务厚度的项目练手的新手以及想看看交易类系统怎么分层、怎么解耦的熟手。下面按“能干什么 → 怎么跑 → 怎么改”的顺序拆开讲。2. 系统架构与目录结构订单、撮合、账户怎么分层2.1 整体分层设计这套系统采用经典的分层架构从上到下依次是表现层、业务服务层、领域模型层和数据访问层。表现层负责接收用户下单、撤单、查询持仓等操作业务服务层承载订单校验、撮合调度、账户结算等核心逻辑领域模型层定义订单、成交、持仓、账户这些实体数据访问层负责持久化常见做法是用 Entity Framework Core 配合 SQLite 或 SQL Server。分层的好处是撮合逻辑不依赖 UI你可以把控制台换成 WinForms、WPF 甚至 Web API业务代码一行不用动。我一般会把撮合引擎单独抽成一个服务类通过接口暴露给上层这样单元测试也好写。2.2 目录结构说明一个典型的目录结构长这样StockTradingSim/ ├── StockTradingSim.Core/ # 领域模型与接口 │ ├── Models/ │ │ ├── Order.cs │ │ ├── Trade.cs │ │ ├── Position.cs │ │ └── Account.cs │ ├── Enums/ │ │ ├── OrderSide.cs │ │ ├── OrderStatus.cs │ │ └── OrderType.cs │ └── Interfaces/ │ ├── IOrderService.cs │ ├── IMatchingEngine.cs │ └── IAccountService.cs ├── StockTradingSim.Services/ # 业务逻辑实现 │ ├── OrderService.cs │ ├── MatchingEngine.cs │ ├── AccountService.cs │ └── MarketDataService.cs ├── StockTradingSim.Data/ # 数据访问 │ ├── TradingDbContext.cs │ └── Repositories/ ├── StockTradingSim.Console/ # 控制台入口 │ └── Program.cs └── StockTradingSim.Tests/ # 单元测试Core 层不引用任何外部依赖Services 层引用 CoreData 层引用 CoreConsole 层引用 Services 和 Data。依赖方向单向不会出现循环引用。2.3 核心实体定义订单是整个系统的起点先看订单模型怎么定义// Models/Order.cs public class Order { public Guid Id { get; set; } Guid.NewGuid(); public string StockCode { get; set; } // 股票代码如 600000 public OrderSide Side { get; set; } // Buy / Sell public OrderType Type { get; set; } // Limit / Market public decimal Price { get; set; } // 委托价格市价单为 0 public int Quantity { get; set; } // 委托数量 public int FilledQuantity { get; set; } // 已成交数量 public OrderStatus Status { get; set; } // Pending / PartialFilled / Filled / Cancelled public DateTime CreatedAt { get; set; } DateTime.UtcNow; public int RemainingQuantity Quantity - FilledQuantity; public bool IsActive Status OrderStatus.Pending || Status OrderStatus.PartialFilled; }这里有几个关键点。decimal而不是double因为金额计算用浮点数会出现精度丢失这是血泪经验。RemainingQuantity和IsActive用只读属性暴露避免外部直接改状态导致数据不一致。Guid做主键而不是自增 int方便后续分布式扩展。2.4 撮合引擎的接口设计撮合引擎是系统的心脏接口定义决定了后续能不能替换实现// Interfaces/IMatchingEngine.cs public interface IMatchingEngine { // 提交订单返回产生的成交列表 IReadOnlyListTrade Submit(Order order); // 撤销订单 bool Cancel(Guid orderId); // 获取某只股票的当前盘口 OrderBookSnapshot GetSnapshot(string stockCode); }Submit返回成交列表而不是单个成交因为一笔订单可能吃掉对手盘多个价位产生多笔成交。GetSnapshot用于展示买卖五档是行情展示的基础。3. 从零跑起来环境准备与启动流程3.1 环境要求与依赖安装跑这个项目需要 .NET 6 或更高版本的 SDK数据库默认用 SQLite零配置适合本地跑也可以切到 SQL Server。先确认环境# 检查 .NET SDK 版本 dotnet --version # 应输出 6.0.x 或更高 # 克隆项目后进入根目录 cd StockTradingSim # 还原 NuGet 包 dotnet restore如果dotnet restore报找不到包常见原因是 NuGet 源配置问题。检查NuGet.config里的源地址或者临时用dotnet restore --source https://api.nuget.org/v3/index.json指定官方源。3.2 数据库初始化项目用 EF Core 的 Code First 模式首次运行需要创建数据库# 安装 EF Core 工具如果还没装 dotnet tool install --global dotnet-ef # 在 Data 项目目录下执行迁移 cd StockTradingSim.Data dotnet ef migrations add InitialCreate dotnet ef database update执行完会在项目目录下生成trading.db文件。如果想重新来过直接删掉这个文件再跑一次database update即可。注意迁移文件要提交到版本控制否则别人拉下来没法建库。3.3 启动与基本操作回到 Console 项目启动cd ../StockTradingSim.Console dotnet run启动后会看到交互菜单支持以下操作操作命令示例说明买入下单buy 600000 10.50 100代码 价格 数量卖出下单sell 600000 10.60 200代码 价格 数量查看持仓position列出所有持仓及盈亏查看订单orders列出当日所有委托撤单cancel orderId按订单 ID 撤销查看盘口book 600000显示买卖五档下单后如果价格能匹配上已有对手单会立即成交并打印成交流水。市价单用buy 600000 market 100这种写法价格填market。3.4 验证系统是否正常跑通后做几个验证先挂一笔卖单sell 600000 10.50 100再挂一笔买单buy 600000 10.50 100应该立即看到成交回报持仓里出现 600000 的 100 股多头。然后试试部分成交挂sell 600000 10.50 200再buy 600000 10.50 100卖单状态应变成PartialFilled剩余 100 股还在盘口挂着。4. 核心模块拆解撮合、结算、行情三条链路4.1 订单簿与价格优先撮合撮合引擎的核心是订单簿。买单按价格从高到低排卖单按价格从低到高排同价位按时间先后排。常见做法是用SortedDictionarydecimal, QueueOrder来组织// Services/MatchingEngine.cs private readonly Dictionarystring, OrderBook _books new(); public IReadOnlyListTrade Submit(Order order) { var book GetOrCreateBook(order.StockCode); var trades new ListTrade(); if (order.Side OrderSide.Buy) { // 买单去匹配卖盘卖盘价格从低到高 while (order.RemainingQuantity 0 book.Asks.Count 0) { var bestAsk book.Asks.First(); // 限价单价格不够停止撮合 if (order.Type OrderType.Limit order.Price bestAsk.Key) break; MatchAtPrice(order, bestAsk.Value, trades); if (bestAsk.Value.Count 0) book.Asks.Remove(bestAsk.Key); } } else { // 卖单去匹配买盘买盘价格从高到低 while (order.RemainingQuantity 0 book.Bids.Count 0) { var bestBid book.Bids.Last(); if (order.Type OrderType.Limit order.Price bestBid.Key) break; MatchAtPrice(order, bestBid.Value, trades); if (bestBid.Value.Count 0) book.Bids.Remove(bestBid.Key); } } // 未成交部分挂入订单簿 if (order.RemainingQuantity 0 order.Type OrderType.Limit) book.AddOrder(order); return trades; }MatchAtPrice负责逐笔撮合成交价取被动方先挂单方的价格这是交易所的通用规则。每笔成交同时更新买卖双方的FilledQuantity和Status。注意while循环里每次都要重新取First()或Last()因为队列可能被清空。4.2 账户结算与持仓盈亏成交之后要更新账户资金和持仓。持仓成本用移动加权平均法计算// Services/AccountService.cs public void ApplyTrade(Trade trade, Order buyOrder, Order sellOrder) { var buyer GetAccount(buyOrder.AccountId); var seller GetAccount(sellOrder.AccountId); decimal amount trade.Price * trade.Quantity; // 买方扣资金加持仓 buyer.Cash - amount; buyer.AddPosition(trade.StockCode, trade.Quantity, trade.Price); // 卖方加资金减持仓计算已实现盈亏 seller.Cash amount; seller.ReducePosition(trade.StockCode, trade.Quantity, trade.Price); } // Position 里的成本更新逻辑 public void AddPosition(string code, int qty, decimal price) { if (!Positions.TryGetValue(code, out var pos)) { Positions[code] new Position { Code code, Quantity qty, AvgCost price }; return; } // 移动加权平均 decimal totalCost pos.AvgCost * pos.Quantity price * qty; pos.Quantity qty; pos.AvgCost totalCost / pos.Quantity; }AvgCost是移动加权平均成本卖出时用(卖出价 - AvgCost) * 数量算已实现盈亏。这里要注意除零问题如果pos.Quantity qty为 0 要单独处理。另外资金扣减要检查是否足够否则会出现负余额。4.3 行情推送与盘口快照行情模块负责生成盘口快照和模拟价格波动。盘口快照直接从订单簿取前五档// Services/MarketDataService.cs public OrderBookSnapshot GetSnapshot(string stockCode) { var book _engine.GetBook(stockCode); return new OrderBookSnapshot { StockCode stockCode, Bids book.Bids .OrderByDescending(kv kv.Key) .Take(5) .Select(kv new PriceLevel { Price kv.Key, Volume kv.Value.Sum(o o.RemainingQuantity) }) .ToList(), Asks book.Asks .OrderBy(kv kv.Key) .Take(5) .Select(kv new PriceLevel { Price kv.Key, Volume kv.Value.Sum(o o.RemainingQuantity) }) .ToList() }; }买单要降序取前五价格最高的五档卖单升序取前五价格最低的五档。Volume是该价位所有订单剩余量之和。如果要做价格模拟可以起一个后台定时器每隔几秒随机生成一些买卖单注入订单簿让盘口看起来是活的。5. 避坑与常见问题精度、并发、状态这三座山5.1 金额用 double 导致对不上账现象几笔成交后账户余额出现0.00000001这种尾差累计多了差几毛钱。原因double是二进制浮点无法精确表示0.1这类十进制小数。解决所有金额、价格、数量字段一律用decimal数据库列类型用decimal(18,4)。EF Core 里用HasPrecision(18, 4)显式配置。5.2 多线程下单导致订单簿错乱现象并发下单时偶尔出现订单丢失或者同一笔订单被成交两次。原因Dictionary和SortedDictionary不是线程安全的多个线程同时读写会破坏内部结构。解决在撮合引擎入口加锁用lock (_bookLock)包住整个Submit方法。如果追求性能可以按股票代码分片加锁不同股票的撮合互不阻塞。我一般先用全局锁保证正确压测发现瓶颈再优化。5.3 订单状态更新遗漏现象订单明明全部成交了状态还显示PartialFilled撤单时提示“订单仍活跃”。原因撮合循环里只更新了FilledQuantity忘了同步Status。解决把状态更新封装成Order.UpdateFill(int qty)方法内部根据FilledQuantity和Quantity自动判断状态避免散落在各处手动改。5.4 数据库迁移后字段类型不对现象本地跑得好好的换台机器建库后金额变成整数小数部分被截断。原因decimal属性没配精度不同数据库默认精度不一样SQLite 宽松但 SQL Server 默认decimal(18,2)。解决在OnModelCreating里统一配置modelBuilder.EntityOrder() .Property(o o.Price) .HasPrecision(18, 4);所有金额字段都走一遍别偷懒。5.5 撤单后订单还在盘口现象撤单返回成功但盘口里还能看到那笔单。原因只改了订单状态没从订单簿的队列里移除。解决Cancel方法里除了改状态还要从对应价位的队列中Remove掉该订单。如果队列空了把整个价位从SortedDictionary里删掉否则盘口会显示空档。6. 二次开发加策略回测和风控的改造思路这套系统跑通之后最有价值的改造方向是加策略回测。思路是把MatchingEngine的时间源抽象成IClock接口回测时用历史数据驱动实盘时用系统时钟。具体做法是定义public interface IClock { DateTime Now { get; } } // 实盘实现 public class SystemClock : IClock { public DateTime Now DateTime.UtcNow; } // 回测实现时间可手动推进 public class BacktestClock : IClock { public DateTime Now { get; private set; } public void Advance(TimeSpan span) Now span; }然后把撮合引擎和订单服务里所有DateTime.UtcNow替换成_clock.Now。回测时读 CSV 历史行情按时间戳逐条推进BacktestClock注入订单跑完统计收益率、最大回撤、胜率。这样一套代码既能实盘模拟又能回测不用维护两份逻辑。风控模块可以挂在OrderService的PlaceOrder入口下单前做检查单笔委托金额是否超过限额、当日累计亏损是否触发熔断、持仓集中度是否超标。风控规则用策略模式组织每条规则实现IRiskRule接口方便增删。改造时有个习惯我每次都强制走一遍先写单元测试再动代码。撮合引擎的测试尤其重要构造几个典型场景——完全成交、部分成交、跨价位成交、撤单后重挂——确保改造没破坏原有逻辑。回测结果和手工计算对不上时先查时钟推进顺序再查成交价取值规则这两处最容易翻车。希望帮到你。本文还有配套的精品资源点击获取
RELATED

相关推荐

DEFCON决赛speedrun-001栈溢出漏洞利用复盘:从ROP到ret2libc

DEFCON决赛speedrun-001栈溢出漏洞利用复盘:从ROP到ret2libc

复盘DEFCON CTF Finals 2019的时候,我第一个拿出来重做的是speedrun-001。这个题目在当年的决赛里属于典型的"抢分题",但它的价值恰恰体现在"抢分"这两个字上:保护机制不是全开但足够模拟真实环境,漏洞点明显…

📅 2026/10/21 5:20:58
端侧AI Agent系统落地指南:模型之外的关键工程实践

端侧AI Agent系统落地指南:模型之外的关键工程实践

1. 为什么一个端侧模型能把Agent这个老概念重新点着AI Agent不是一个新词,从早期基于规则的任务脚本到后来RPA、工作流引擎,圈子内讨论Agent已经好几年了。但过去聊Agent的时候,大家默认一个前提——推理能力在云端,模型是上百亿甚…

📅 2026/10/21 5:15:58
4路加热烘箱流水线PID温控与V20 USS通讯实战调试笔记

4路加热烘箱流水线PID温控与V20 USS通讯实战调试笔记

实际调试过的4路加热烘箱流水线,配合V20变频器做USS通讯,这活儿不算大,但想把程序写得“结构清晰、稳定可靠”,确实有一些门道。很多朋友拿到类似项目,第一反应是先把PID向导拉出来,或者先调变频器&#xf…

📅 2026/10/21 5:15:58
MORE NEWS

更多资讯

📰

知识工作插件化:构建可编程、可组合的智能工作流底座

1. 项目概述:这不是一个插件,而是一套知识工作者的“操作系统增强层”“knowledge-work-plugins”这个标题乍看像某个开源仓库的命名,但拆开来看——knowledge work(知识工作)、plugins(插件)—…

📰

SQL题目面试避坑指南:从读题拆解到查询优化全链路

写SQL题目刷了几年,也帮不少同学备过面试、带过新人,我最大的感受是:多数人不是不会写SQL,而是根本不知道一道SQL题目到底在考察什么。同一个查询,有人三行搞定,有人磨叽半小时还漏了边界;同一张…

📰

知识工作插件:面向知识工作者的模块化增强协议

1. 项目概述:这不是一个插件,而是一套知识工作者的“操作系统增强层”“knowledge-work-plugins”——光看这个名字,很多人第一反应是“又一个浏览器插件合集”或者“某款笔记软件的扩展包”。但在我过去三年深度参与多个知识管理系统的搭建与…

📰

SchoolDB四表DDL全解析:数据库设计从字段选择到约束实践

最近给团队做内部培训,我又把一套用了很多年的教学示例库脚本翻了出来——SchoolDB。场景很简单:一个学校管理系统,只有4张表:学生、教师、课程、选课。很多人觉得写DDL语句没什么技术含量,敲几行CREATE TABLE谁不会&a…

📰

订单表慢SQL优化实战:联合索引、覆盖索引与深分页方案

1. 场景回顾:慢SQL是怎么暴露出来的做订单系统的同学应该都有体会:订单表往往是整个业务库里面数据量增长最快、查询模式最多样、索引也最容易“被用坏”的一张表。这个实战项目来自某电商平台的中台订单模块,核心表trade_order存放全渠道的订…

📰

Python实战英雄联盟胜率预测:从数据清洗到SHAP模型解释

简介:LeaguePredictor 是一套面向电子竞技数据分析初学者与机器学习实践者的 Python 项目源码,聚焦英雄联盟比赛胜率预测这一具体场景,帮助读者理解如何把分类模型落地到真实赛事数据上。资源包共 24 个文件,以 22 个 py 脚本为主…

TODAY

今日更新

THIS WEEK

本周精选

THIS MONTH

本月热门

读完文章,想聊聊您的网站?

告诉我们您的行业与需求,资深顾问一对一梳理方案与报价,全程免费。

📞 💬