资讯动态

C#股票交易模拟系统源码解析:行情、撮合与账本实现

发布时间:2026/10/21 22:44:44 来源:尧图企业网站定制
简介这是一份面向C#初学者与.NET开发者的股票交易模拟系统源码适合作为网络编程、数据库操作与桌面应用开发的综合实战练习。项目以C#语言构建围绕虚拟股票交易场景涵盖行情数据获取与解析、用户信息与交易记录存储、图形界面交互以及订单撮合等业务逻辑帮助读者理解从数据层到界面层的完整实现思路。压缩包共63个文件约278KB以21个cs源码文件为核心配合8个resx与9个resources资源文件、8个accdb数据库文件以及csproj、sln等工程配置另有少量exe、pdb与缓存文件结构紧凑、便于直接打开运行与调试。目前已有1765人学习下载可作为课程设计或自学练手参考从中了解Windows Forms界面搭建、ADO.NET数据访问与MVC分层组织方式并在此基础上扩展行情接口或完善交易规则。1. 从一份 C# 股票交易模拟系统源码说起它到底能跑出什么很多人第一次接触交易类项目都是被 K 线图吸引结果下载完源码发现跑不起来或者跑起来只有一堆乱码数字。这份 C# 实现的股票交易模拟系统解决的就是这个尴尬它把行情推送、委托下单、撮合成交、持仓盈亏、账户资金这几条链路串成了一个能直接运行的桌面程序。适合两类人——想学 C# 桌面开发又不想写增删改查的以及想理解交易系统内部账本怎么记、订单怎么撮合的。技术栈是 WinForms 或 WPF 加 ADO.NET数据库常见用 SQLite 或 SQL Server不依赖任何外部行情接口所有价格波动由内置模拟器生成。换句话说你拿到的是一个能自己造行情的沙盘而不是一个只能看不能动的空壳。2. 架构拆解行情、订单、撮合、账本四条线怎么串2.1 分层结构与目录约定这类模拟系统通常不会上微服务但也不会把所有逻辑塞进一个 Form。我拆过的同类项目里比较合理的目录长这样StockSim/ ├── Models/ # 实体类Account、Order、Position、Tick ├── Data/ # 数据访问SqliteHelper、Repository ├── Services/ # 业务核心MatchingEngine、MarketSimulator ├── Forms/ # 界面MainForm、OrderForm、PositionForm ├── Utils/ # 工具PriceRandom、TradeTimeHelper └── Program.csModels 层只放 POCO 类不掺业务逻辑Data 层负责建表和读写Services 层是重点撮合和行情模拟都在这里Forms 只做展示和事件转发。这样分的好处是你换 UI 框架时 Services 几乎不用动。2.2 行情模拟器价格怎么“动”起来真实行情接不进来就得自己造。常见做法是用随机游走加一点趋势约束避免价格瞬间归零或暴涨。public class MarketSimulator { private readonly Random _rand new Random(); private decimal _lastPrice 100.00m; // 生成下一个 Tick波动幅度受 volatility 控制 public Tick NextTick(string symbol, decimal volatility 0.02m) { // 在 [-volatility, volatility] 之间取随机偏移 var offset (decimal)(_rand.NextDouble() * 2 - 1) * volatility; _lastPrice Math.Max(0.01m, _lastPrice * (1 offset)); return new Tick { Symbol symbol, Price Math.Round(_lastPrice, 2), Volume _rand.Next(100, 10000), Timestamp DateTime.Now }; } }volatility控制单次波动比例默认 0.02 表示上下 2%。Math.Max(0.01m, ...)是防止价格被随机到负数。Volume只是模拟成交量不参与撮合但可以用来做成交量柱状图。如果你想让行情更“像真的”可以把随机游走改成带均值回归的模型但那是二次开发的事跑通阶段不用动。2.3 撮合引擎限价单怎么成交撮合是交易系统的心脏。模拟系统一般只做限价单规则简单买单价格大于等于卖单价格就成交成交价取先挂单的价格。public class MatchingEngine { private readonly ListOrder _buyOrders new ListOrder(); private readonly ListOrder _sellOrders new ListOrder(); public void PlaceOrder(Order order) { if (order.Side OrderSide.Buy) { // 尝试与卖盘撮合 var match _sellOrders.FirstOrDefault(s s.Price order.Price); if (match ! null) { Execute(order, match); _sellOrders.Remove(match); } else { _buyOrders.Add(order); // 未成交则挂单 } } else { var match _buyOrders.FirstOrDefault(b b.Price order.Price); if (match ! null) { Execute(order, match); _buyOrders.Remove(match); } else { _sellOrders.Add(order); } } } private void Execute(Order incoming, Order resting) { // 成交价取挂单方价格成交量取两者较小值 var price resting.Price; var qty Math.Min(incoming.Quantity, resting.Quantity); // 这里触发成交事件更新持仓和资金 OnTradeExecuted?.Invoke(new Trade { Symbol incoming.Symbol, Price price, Quantity qty, BuyOrderId incoming.Side OrderSide.Buy ? incoming.Id : resting.Id, SellOrderId incoming.Side OrderSide.Sell ? incoming.Id : resting.Id }); } public event ActionTrade OnTradeExecuted; }FirstOrDefault找的是第一个满足价格条件的对手单真实交易所会按价格优先、时间优先排序这里简化了。Execute里成交量取Math.Min是因为可能出现部分成交但模拟系统为了简单通常直接全成或全挂。OnTradeExecuted事件是解耦的关键持仓和资金模块订阅它而不是撮合引擎直接改账户。2.4 账本与持仓钱和股票怎么记成交之后要更新两样东西账户现金和持仓数量。常见做法是用一个AccountService统一处理避免多处修改导致对不上账。public class AccountService { public void ApplyTrade(Trade trade, Account account) { var amount trade.Price * trade.Quantity; if (account.Id trade.BuyOrderId) // 简化判断实际应按账户ID { account.Cash - amount; AddPosition(account, trade.Symbol, trade.Quantity, trade.Price); } else { account.Cash amount; ReducePosition(account, trade.Symbol, trade.Quantity); } } private void AddPosition(Account acc, string symbol, int qty, decimal price) { var pos acc.Positions.FirstOrDefault(p p.Symbol symbol); if (pos null) { acc.Positions.Add(new Position { Symbol symbol, Quantity qty, AvgCost price }); } else { // 加权平均成本 var totalCost pos.AvgCost * pos.Quantity price * qty; pos.Quantity qty; pos.AvgCost totalCost / pos.Quantity; } } }AvgCost用加权平均算这是持仓盈亏计算的基础。ReducePosition里卖出时通常按先进先出或加权平均扣减模拟系统用加权平均更简单。注意这里没有考虑手续费和印花税二次开发时可以加一个FeeCalculator在ApplyTrade里扣。3. 跑起来环境、建库、启动顺序和第一个委托3.1 环境准备与依赖还原这份源码大概率是 .NET Framework 4.7.2 或 .NET 6 的 WinForms 项目。先确认本机有对应运行时然后用 Visual Studio 打开.sln或者命令行dotnet restore。数据库如果是 SQLite通常不需要额外安装System.Data.SQLite或Microsoft.Data.Sqlite会随 NuGet 还原。如果是 SQL Server需要改连接字符串并手动建库。常见做法是在App.config里放连接串第一次运行时自动建表。connectionStrings add nameStockSimDb connectionStringData Sourcestock_sim.db;Version3; providerNameSystem.Data.SQLite / /connectionStringsData Source指向数据库文件SQLite 会在首次连接时自动创建。如果换成 SQL Server连接串要改成Server.;DatabaseStockSim;Trusted_ConnectionTrue;并且需要先执行建表脚本。3.2 建表与初始化数据模拟系统至少需要账户表、订单表、成交表、持仓表。下面是最小化的建表语句CREATE TABLE Account ( Id INTEGER PRIMARY KEY, Name TEXT NOT NULL, Cash REAL NOT NULL DEFAULT 1000000 ); CREATE TABLE Orders ( Id INTEGER PRIMARY KEY AUTOINCREMENT, Symbol TEXT NOT NULL, Side INTEGER NOT NULL, -- 0 买 1 卖 Price REAL NOT NULL, Quantity INTEGER NOT NULL, Status INTEGER NOT NULL, -- 0 挂单 1 成交 2 撤单 CreatedAt TEXT NOT NULL ); CREATE TABLE Trades ( Id INTEGER PRIMARY KEY AUTOINCREMENT, Symbol TEXT NOT NULL, Price REAL NOT NULL, Quantity INTEGER NOT NULL, TradeTime TEXT NOT NULL ); CREATE TABLE Positions ( Id INTEGER PRIMARY KEY AUTOINCREMENT, AccountId INTEGER NOT NULL, Symbol TEXT NOT NULL, Quantity INTEGER NOT NULL, AvgCost REAL NOT NULL );Cash默认给 100 万方便你随便下单测试。Status字段用来区分挂单和已成交撤单时改成 2。Positions表按账户和股票代码唯一更新时先查再改。3.3 启动顺序与第一个委托程序启动后一般先初始化数据库再启动行情模拟器最后打开主窗体。顺序错了会出现界面已显示但行情没数据的情况。static void Main() { Application.EnableVisualStyles(); DatabaseInitializer.Init(); // 建表 var simulator new MarketSimulator(); var engine new MatchingEngine(); var accountService new AccountService(); engine.OnTradeExecuted trade accountService.ApplyTrade(trade, CurrentAccount); var mainForm new MainForm(simulator, engine); Application.Run(mainForm); }DatabaseInitializer.Init()负责执行建表语句重复执行不会报错。OnTradeExecuted订阅必须在窗体显示前完成否则第一笔成交可能丢事件。启动后在界面上选股票、输入价格和数量点“买入”如果价格高于卖一就会立刻成交并在持仓里看到。提示第一次跑建议先用默认账户和默认股票代码不要急着改数据库路径避免连接串写错导致启动就崩。4. 避坑与排查跑不起来时先看这几处4.1 现象点击买入没反应订单也没进列表原因通常是撮合引擎的PlaceOrder没有被 UI 事件调用或者调用了但没把订单加到界面绑定的集合里。解决方法是先在PlaceOrder入口打日志确认事件有没有触发再检查Orders列表是否用了BindingList并绑定了 DataGridView。如果是普通List界面不会自动刷新。4.2 现象成交后持仓数量对但现金没变多半是ApplyTrade里判断买卖方向时用了订单 ID 而不是账户 ID。模拟系统里一个账户对应多个订单正确做法是在Trade里带上BuyAccountId和SellAccountId或者直接在订单里存AccountId。改的时候注意别把买卖方向搞反否则现金会越买越多。4.3 现象行情价格一直不动检查MarketSimulator的定时器有没有启动。常见写法是用System.Windows.Forms.TimerInterval设 1000 毫秒在Tick事件里调NextTick并更新界面。如果用了System.Timers.Timer回调不在 UI 线程更新控件会抛异常需要Invoke。另外_lastPrice如果是只读字段每次NextTick都返回同一个值也会看起来不动。4.4 现象SQLite 报“database is locked”多个线程同时写同一个 SQLite 文件会锁库。模拟系统里行情线程和 UI 线程都可能写订单解决办法是给数据库操作加锁或者把写操作统一放到一个队列里由单线程执行。简单粗暴的做法是在SqliteHelper里对ExecuteNonQuery加lock但会影响性能测试阶段够用。4.5 现象切换 .NET 版本后编译报错从 .NET Framework 迁到 .NET 6 时System.Data.SQLite要换成Microsoft.Data.SqliteConfigurationManager要换成ConfigurationBuilderApplication.Run还在但EnableVisualStyles需要ApplicationConfiguration.Initialize()。别一次改太多先让项目编译通过再逐个替换数据库调用。5. 二次开发把模拟盘改成能验证策略的回测台跑通之后最有价值的改造方向是让它从“手动下单玩具”变成“策略回测台”。核心思路是把行情模拟器换成历史数据回放把手动下单换成策略信号驱动同时保留原有的撮合和账本逻辑。第一步抽象行情源。现在MarketSimulator是直接生成 Tick你可以定义一个IMarketDataSource接口让模拟器和 CSV 回放器都实现它。public interface IMarketDataSource { event ActionTick OnTick; void Start(); void Stop(); } public class CsvReplaySource : IMarketDataSource { private readonly string _filePath; public event ActionTick OnTick; public CsvReplaySource(string filePath) _filePath filePath; public void Start() { foreach (var line in File.ReadLines(_filePath)) { var parts line.Split(,); OnTick?.Invoke(new Tick { Symbol parts[0], Price decimal.Parse(parts[1]), Volume int.Parse(parts[2]), Timestamp DateTime.Parse(parts[3]) }); Thread.Sleep(10); // 控制回放速度 } } public void Stop() { } }OnTick事件让上层不用关心数据来源。Thread.Sleep(10)是回放节奏改成 0 就是全速回测。CSV 格式按代码,价格,数量,时间排列自己用 Excel 导一份就行。第二步加一个策略接口。策略只负责根据 Tick 产生买卖信号不直接碰账户。public interface IStrategy { void OnTick(Tick tick, MatchingEngine engine, Account account); } public class MaCrossStrategy : IStrategy { private readonly Listdecimal _prices new Listdecimal(); private readonly int _short 5; private readonly int _long 20; public void OnTick(Tick tick, MatchingEngine engine, Account account) { _prices.Add(tick.Price); if (_prices.Count _long) return; var shortMa _prices.Skip(_prices.Count - _short).Average(); var longMa _prices.Skip(_prices.Count - _long).Average(); if (shortMa longMa account.Positions.All(p p.Symbol ! tick.Symbol)) { engine.PlaceOrder(new Order { Symbol tick.Symbol, Side OrderSide.Buy, Price tick.Price, Quantity 100, AccountId account.Id }); } else if (shortMa longMa account.Positions.Any(p p.Symbol tick.Symbol)) { engine.PlaceOrder(new Order { Symbol tick.Symbol, Side OrderSide.Sell, Price tick.Price, Quantity 100, AccountId account.Id }); } } }_short和_long是均线周期改成 10 和 30 就是另一套参数。account.Positions.All(...)判断当前没持仓才买避免重复开仓。这个策略只是示例你可以换成布林带、RSI 或者自己写的信号。第三步把回测结果落成表。每次回测结束把账户最终现金、持仓市值、成交次数写进BacktestResult表方便对比不同参数。字段类型说明IdINTEGER主键StrategyNameTEXT策略名StartCashREAL初始资金EndCashREAL结束资金TradeCountINTEGER成交笔数RunTimeTEXT回测时间改造过程中最容易翻车的地方是时间对齐CSV 里的时间戳如果和系统时间混用持仓盈亏会算错。我一般会在回测模式下把DateTime.Now全部替换成tick.Timestamp包括订单创建时间和成交时间。从那以后我每次改回测逻辑都强制先跑一遍固定 CSV确认成交笔数和手工算的一致再换策略参数。希望帮到你。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑