
简介这是一套用Delphi完整开发的股票分析系统主站与客户端源码面向希望研究桌面端大型金融软件架构的Delphi开发者以及需要搭建行情分析工具的技术人员。系统实现了股票行情分析、实时财务数据展示与实时信息地雷等核心功能且基本未依赖第三方控件对理解原生Delphi在复杂业务系统中的工程组织方式很有参考价值。压缩包共87个文件约2.32MB包含60个cel模板文件、14个dat数据文件、5个dll动态库、3个ini配置、2个exe可执行程序及xml、txt等辅助文件覆盖行情、财务、基金、债券、期货等多类数据模板与业务模块。目前已有984人学习下载。使用者可借此研究主站与客户端的通信结构、数据字典组织、模板化报表设计以及DLL模块拆分思路登录时随意填写用户名即可进入便于快速上手调试与二次开发。1. 用 Delphi 搓一套股票分析系统从行情接入到指标绘制的完整落地很多人觉得 Delphi 只能做做进销存、写写工控上位机真拿它去接行情、算指标、画 K 线第一反应是“这玩意儿能行吗”。我一开始也这么想直到把一套主站加客户端的股票分析系统从头拆了一遍——客户端全用 Delphi 写图形处理控件、指标计算、行情刷新全在本地跑跑起来之后那个流畅度说实话比不少 Web 套壳的方案要跟手得多。这套系统适合两类人一类是手里有 Delphi 老底子、想接金融数据做二次开发的熟手另一类是想找一个完整 C/S 架构项目练手的新手因为它把“数据接入 → 指标计算 → 图形渲染 → 客户端展示”这条链路走全了。下面我就按自己拆项目的顺序把这份源码怎么跑、怎么改讲清楚。2. 项目定位与技术选型为什么主站和客户端都压在 Delphi 上2.1 这套系统到底解决什么问题先把功能边界说清楚。这套股票分析系统分两块主站负责行情数据的采集、清洗和对外分发客户端负责把数据拉下来做本地计算和图形展示。客户端不是简单的表格罗列而是带 K 线图、分时图、成交量柱、均线叠加、指标副图的分析工具用户能切换周期、缩放、拖动、看十字光标数值。主站这边更像一个数据中转层把上游行情源的数据规整成统一格式再通过自定义协议推给客户端。为什么主站和客户端都用 Delphi我拆下来觉得核心原因是图形处理控件的复用。Delphi 生态里有一批成熟的图形处理类控件画 K 线、画坐标轴、处理鼠标交互都很顺手主站和客户端共用同一套绘图基础库省掉了跨语言序列化和渲染不一致的麻烦。另一个原因是部署简单Delphi 编译出来是原生可执行文件客户端发给用户直接双击就能跑不用装运行时、不用配环境这在金融工具场景里是实打实的优势。2.2 技术栈拆解从数据层到渲染层把技术栈按层拆开看会更清楚每一块在干什么层级技术选型职责数据接入层Delphi 原生 Socket / Indy 组件连接行情源接收原始数据包数据处理层Delphi 动态数组 自定义记录类型解析、缓存、周期聚合指标计算层纯 Pascal 实现的 MA/MACD/KDJ/RSI本地计算不依赖外部库图形渲染层图形处理类控件 GDI/GDI 封装K 线、分时、副图绘制客户端交互层VCL 窗体 自定义消息缩放、拖动、十字光标这里有个关键点指标计算全部放在客户端本地。主站只推原始行情客户端拿到之后自己算 MA、MACD 这些。这么做的好处是主站压力小坏处是客户端要有一份完整的指标计算代码。我拆的时候发现指标计算模块是独立单元不依赖窗体可以单独拿出来测试这对二次开发很友好——你想加个新指标只要在这个单元里加一个函数就行。2.3 目录结构拿到源码先看哪里拿到源码别急着编译先看目录结构能省很多时间。这套项目的目录大致是这样组织的StockAnalysis/ ├── Common/ # 主站和客户端共用的基础单元 │ ├── uDataType.pas # 行情数据结构定义 │ ├── uIndicator.pas # 指标计算函数库 │ └── uDrawBase.pas # 图形绘制基础封装 ├── Server/ # 主站 │ ├── uServerMain.pas # 主站主窗体 │ ├── uDataFeed.pas # 行情接入与解析 │ └── uDispatch.pas # 数据分发逻辑 ├── Client/ # 客户端 │ ├── uClientMain.pas # 客户端主窗体 │ ├── uKLine.pas # K 线图绘制 │ ├── uTimeLine.pas # 分时图绘制 │ └── uCrossHair.pas # 十字光标交互 └── Bin/ # 编译输出目录重点看Common/uDataType.pas和Common/uIndicator.pas这两个文件定义了整个系统的数据契约和计算逻辑看懂它们后面改哪儿心里都有数。uDrawBase.pas是图形处理控件的封装层如果你要换绘图库或者改渲染方式改这一个文件就能影响全局。3. 跑起来主站和客户端的编译与联调步骤3.1 环境准备与依赖检查这套项目用的是 Delphi 的 VCL 框架没有第三方商业控件依赖但有几个地方要注意。第一确认你的 Delphi 版本支持项目里用到的语法特性项目里用了动态数组和泛型容器太老的版本比如 Delphi 7可能编译不过常见做法是用 Delphi 2010 之后的版本。第二检查Common目录有没有加到搜索路径里主站和客户端都要引用它路径不对会报找不到单元。打开项目组文件.groupproj之后先别急着编译在 IDE 里检查一下每个项目的搜索路径设置。我一般会先把Common目录加到所有项目的搜索路径里再统一编译避免一个个改。3.2 主站启动行情接入与分发主站的核心是行情接入和分发。启动主站之前要确认行情源的地址和端口配置正确。这套项目把配置放在一个独立的配置单元里常见做法是读 ini 文件或者注册表。我拆的时候看到配置项包括行情源地址、监听端口、最大连接数这几个。主站启动的关键代码在uDataFeed.pas里数据接入的逻辑大致是这样// uDataFeed.pas - 行情接入核心逻辑 procedure TDataFeed.Connect; begin FSocket : TIdTCPClient.Create(nil); FSocket.Host : FConfig.SourceHost; // 行情源地址 FSocket.Port : FConfig.SourcePort; // 行情源端口 FSocket.ConnectTimeout : 5000; // 连接超时 5 秒 try FSocket.Connect; FConnected : True; // 启动接收线程避免阻塞主界面 FRecvThread : TRecvThread.Create(FSocket); FRecvThread.OnData : HandleRawData; // 数据回调 FRecvThread.Start; except on E: Exception do LogError(行情源连接失败: E.Message); end; end;这段代码的逻辑是创建 TCP 客户端设置行情源地址和端口连接成功后启动一个独立的接收线程。参数说明SourceHost和SourcePort来自配置文件改这两个值就能切换行情源ConnectTimeout设成 5000 毫秒网络不好的时候可以调大OnData是数据回调收到原始数据后会触发HandleRawData做解析。这里用独立线程接收是必须的不然主界面会卡死这是血泪经验——我见过有人把接收逻辑写在主线程里行情一密集界面就假死。3.3 客户端连接与数据拉取客户端启动后要做两件事连主站、拉历史数据。连接逻辑和主站类似但客户端多了一个“请求历史数据”的步骤。客户端连上主站后会先发一个历史数据请求包主站收到后把缓存的历史行情批量推回来客户端拿到之后先渲染一次再进入实时刷新模式。// uClientMain.pas - 客户端连接与历史数据请求 procedure TClientMain.ConnectToServer; begin FSocket : TIdTCPClient.Create(nil); FSocket.Host : FServerHost; FSocket.Port : FServerPort; FSocket.Connect; // 发送历史数据请求股票代码 周期 数量 FSocket.IOHandler.WriteLn( Format(HIST|%s|%d|%d, [FStockCode, FPeriod, FHistoryCount])); // 接收历史数据并渲染 RecvHistoryData; // 进入实时刷新 StartRealtimeRefresh; end;参数说明FStockCode是股票代码FPeriod是周期1 分钟、5 分钟、日线等FHistoryCount是请求的历史 K 线数量。这三个参数决定了客户端首次渲染的数据范围。StartRealtimeRefresh会启动一个定时器或者独立线程定期向主站请求最新数据。常见做法是用定时器间隔设成 1 到 3 秒太频繁会给主站压力太慢又不够实时。3.4 联调验证怎么确认跑通了主站和客户端都编译好之后先启动主站看日志有没有“行情源连接成功”的输出。然后启动客户端输入股票代码看 K 线图有没有出来。如果 K 线出来了但不动说明实时刷新没生效检查定时器或者刷新线程有没有启动。如果 K 线完全没出来先看客户端日志有没有收到历史数据再查主站有没有正常分发。我一般会先用一个简单的测试行情源手动发几条固定数据确认整条链路通了再接真实行情源。这样排查问题的时候能快速定位是接入层的问题还是渲染层的问题。4. 核心模块拆解指标计算与图形绘制的实现细节4.1 指标计算MA、MACD、KDJ 的纯 Pascal 实现指标计算模块是整个系统的计算核心放在Common/uIndicator.pas里。这个单元不依赖任何窗体输入是行情数组输出是指标数组可以单独测试。先看 MA移动平均的实现// uIndicator.pas - 移动平均线计算 function CalcMA(const Close: array of Double; Period: Integer): TDoubleArray; var I, J: Integer; Sum: Double; begin SetLength(Result, Length(Close)); for I : 0 to Length(Close) - 1 do begin if I Period - 1 then begin Result[I] : 0; // 数据不足周期时置 0 Continue; end; Sum : 0; for J : I - Period 1 to I do Sum : Sum Close[J]; Result[I] : Sum / Period; end; end;逻辑说明这个实现是朴素累加法每个点都重新求和时间复杂度是 O(n×period)。对于日线级别、几千条数据来说够用但如果要做分钟级、几万条数据建议改成滑动窗口累加每次减掉最老的值、加上最新的值复杂度降到 O(n)。参数说明Close是收盘价数组Period是周期比如 5 日线传 510 日线传 10。返回数组和输入等长前Period-1个位置是 0画图的时候要跳过这些点。MACD 的计算依赖 EMA指数移动平均EMA 的递推公式是EMA(today) EMA(yesterday) × (N-1)/(N1) Close(today) × 2/(N1)。MACD 由 DIF、DEA、MACD 柱三条线组成DIF 是快慢 EMA 之差DEA 是 DIF 的 EMAMACD 柱是 (DIF-DEA)×2。KDJ 的计算稍微复杂一点要先算 RSV再对 RSV 做平滑得到 K 和 D最后算 J。这些指标的计算逻辑都是标准公式源码里实现得比较直白改参数比如把 MACD 的 12、26、9 改成其他值只要改函数入口的常量就行。4.2 图形绘制K 线图的分层渲染图形绘制是这套系统的重头戏也是图形处理类控件发挥作用的地方。K 线图的渲染分成几层背景网格层、K 线层、均线层、副图层、十字光标层。分层的好处是每层独立刷新拖动或者缩放的时候只重绘变化的层性能会好很多。// uKLine.pas - K 线绘制核心逻辑 procedure TKLineChart.DrawKLine(ACanvas: TCanvas); var I: Integer; X, YOpen, YClose, YHigh, YLow: Integer; BarWidth: Integer; begin BarWidth : FChartWidth div FVisibleCount; // 每根 K 线宽度 for I : FStartIndex to FEndIndex do begin X : FChartLeft (I - FStartIndex) * BarWidth BarWidth div 2; YOpen : PriceToY(FData[I].Open); YClose : PriceToY(FData[I].Close); YHigh : PriceToY(FData[I].High); YLow : PriceToY(FData[I].Low); // 画影线 ACanvas.MoveTo(X, YHigh); ACanvas.LineTo(X, YLow); // 画实体涨用空心跌用实心 if FData[I].Close FData[I].Open then DrawHollowRect(ACanvas, X - BarWidth div 3, YOpen, X BarWidth div 3, YClose) else DrawFilledRect(ACanvas, X - BarWidth div 3, YOpen, X BarWidth div 3, YClose); end; end;逻辑说明这段代码遍历可见范围内的 K 线先算每根 K 线的 X 坐标再把开高低收四个价格转成 Y 坐标然后画影线和实体。涨的时候画空心矩形跌的时候画实心矩形这是国内股票软件的惯例。参数说明FStartIndex和FEndIndex是当前可见的 K 线索引范围由滚动条位置决定PriceToY是价格到屏幕坐标的转换函数依赖当前视图的最高价和最低价BarWidth是每根 K 线的宽度由图表宽度除以可见 K 线数量得到。这里有个性能优化的点PriceToY如果在循环里反复计算最高最低价会很慢。常见做法是在进入循环之前先把可见范围内的最高最低价算出来缓存起来循环里直接用。我拆的时候看到源码里做了这个优化说明作者是踩过坑的。4.3 十字光标与交互鼠标消息的处理链路十字光标是股票分析软件的标配鼠标移动的时候显示一条横线和一条竖线同时在边上显示当前价格和日期。这套项目的十字光标实现放在uCrossHair.pas里核心是处理鼠标移动消息把屏幕坐标转成数据索引和价格。// uCrossHair.pas - 鼠标移动处理 procedure TCrossHair.OnMouseMove(Sender: TObject; Shift: TShiftState; X, Y: Integer); var DataIndex: Integer; Price: Double; begin // 屏幕 X 坐标转数据索引 DataIndex : FChart.XToIndex(X); if (DataIndex 0) or (DataIndex Length(FChart.Data)) then begin Hide; // 超出数据范围时隐藏 Exit; end; // 屏幕 Y 坐标转价格 Price : FChart.YToPrice(Y); // 更新十字线位置 FVLineX : X; FHLineY : Y; // 更新信息面板 FInfoPanel.UpdateInfo(FChart.Data[DataIndex], Price); // 触发重绘 FChart.Invalidate; end;逻辑说明鼠标移动时先把屏幕 X 坐标转成数据索引如果超出范围就隐藏十字光标再把屏幕 Y 坐标转成价格然后更新十字线位置和信息面板最后触发重绘。参数说明XToIndex和YToPrice是坐标转换函数和绘制时的PriceToY互为逆运算FInfoPanel是显示当前数据的面板会显示日期、开高低收、成交量这些信息。这里要注意的是重绘频率。鼠标移动消息触发很频繁如果每次都全量重绘会卡。常见做法是用双缓冲先把图绘到内存位图上再一次性刷到屏幕。源码里用了双缓冲所以拖动的时候不闪。5. 避坑与常见问题编译、连接、渲染的排查记录5.1 编译报“找不到单元”现象打开项目组编译报一堆“Unit xxx not found”。原因Common目录没有加到搜索路径里或者路径用了相对路径但项目移动过位置。解决在 IDE 的 Project Options 里把Common目录的绝对路径加到 Search Path 里所有项目都要加。如果项目要换机器编译建议用相对路径并且把Common和项目文件放在同一级目录下。5.2 客户端连不上主站现象客户端启动后一直显示“连接中”或者报连接超时。原因主站没启动、端口被占用、防火墙拦截或者客户端配置的主站地址不对。解决先确认主站进程在跑再用netstat看监听端口有没有起来。如果端口被占用改主站配置里的监听端口客户端同步改。防火墙的问题常见做法是把主站和客户端都加到白名单里或者临时关掉防火墙测试。5.3 K 线图空白或者错位现象客户端连上了数据也收到了但 K 线图是空白的或者画出来的 K 线挤在一起。原因价格到屏幕坐标的转换有问题通常是最高价和最低价算错了或者可见范围设置不对。解决先检查PriceToY函数里的最高最低价是不是从当前可见数据里算的如果用了全量数据的最高最低价缩放之后就会错位。再检查FStartIndex和FEndIndex有没有超出数据范围。我一般会在绘制之前打日志把最高最低价和可见范围打出来一眼就能看出问题。5.4 实时刷新导致界面卡顿现象行情刷新频繁的时候客户端界面卡顿鼠标拖动不跟手。原因刷新逻辑在主线程里跑或者每次刷新都全量重绘。解决把数据接收放在独立线程里收到数据后只更新数据缓存然后通过消息通知主线程重绘。重绘的时候用双缓冲并且只重绘变化的层。如果还是卡降低刷新频率比如从 1 秒改成 3 秒。5.5 指标计算结果和别的软件对不上现象自己算的 MA、MACD 和行情软件里显示的不一样。原因数据对齐方式不同或者指标参数的起始点处理不一样。解决先确认用的收盘价数据是不是复权后的很多行情软件默认用前复权数据。再检查指标计算的起始点比如 MA 的前Period-1个点有的软件显示为空有的显示为收盘价本身。对齐这些细节之后结果就能对上。6. 二次开发加指标、换数据源、改渲染的实操思路6.1 加一个新指标以布林带为例加指标是这套系统最常见的二次开发需求。布林带BOLL由中轨、上轨、下轨三条线组成中轨是 N 日移动平均上轨是中轨加两倍标准差下轨是中轨减两倍标准差。在uIndicator.pas里加一个函数就行// uIndicator.pas - 布林带计算 procedure CalcBOLL(const Close: array of Double; Period: Integer; Deviation: Double; out Upper, Middle, Lower: TDoubleArray); var I, J: Integer; Sum, Avg, StdDev: Double; begin SetLength(Upper, Length(Close)); SetLength(Middle, Length(Close)); SetLength(Lower, Length(Close)); for I : 0 to Length(Close) - 1 do begin if I Period - 1 then begin Upper[I] : 0; Middle[I] : 0; Lower[I] : 0; Continue; end; Sum : 0; for J : I - Period 1 to I do Sum : Sum Close[J]; Avg : Sum / Period; // 算标准差 StdDev : 0; for J : I - Period 1 to I do StdDev : StdDev Sqr(Close[J] - Avg); StdDev : Sqrt(StdDev / Period); Middle[I] : Avg; Upper[I] : Avg Deviation * StdDev; Lower[I] : Avg - Deviation * StdDev; end; end;参数说明Period是周期默认 20Deviation是标准差倍数默认 2。加完计算函数之后还要在副图或者主图的绘制逻辑里加三条线的绘制并且在指标切换的下拉框里加一个选项。这套项目的指标切换是配置驱动的常见做法是在一个指标注册表里加一条记录指定指标名称、计算函数、绘制函数界面会自动生成选项。6.2 换数据源改接入层还是改解析层换数据源分两种情况。如果新数据源的协议和原来的类似只是地址和端口不同改配置就行。如果协议不一样就要改解析层。解析层的入口是HandleRawData原始数据进来之后先按协议拆包再转成统一的行情记录。我一般会保留原来的解析函数新写一个解析函数然后在配置里指定用哪个解析器这样切换数据源的时候不用改代码改配置就行。6.3 改渲染换图形库或者加新图型图形处理类控件的封装在uDrawBase.pas里如果你想换一个图形库改这个文件里的绘制原语就行。比如把画线、画矩形、画文字这几个基础操作换成新库的调用上层的 K 线绘制、分时绘制不用动。加新图型比如加一个成交量副图、加一个资金流向图的思路是先加数据字段再加绘制函数最后在布局里分配一块区域。6.4 验证改造效果我常用的三步法改完之后怎么验证我一般走三步。第一步用固定数据测试计算函数把输入和输出打出来和手工算的对一遍。第二步用历史数据回放看图形绘制对不对缩放拖动有没有问题。第三步接实时数据跑一段时间看内存有没有泄漏、界面会不会卡。这三步走完基本就能确认改造没问题了。从那以后我每次改指标计算或者渲染逻辑都强制走一遍这三步尤其是第一步——手工对一遍计算能省掉后面很多玄学问题。希望帮到你。本文还有配套的精品资源点击获取