2、实时监控系统架构设计:系统整体架构图、数据流设计、模块划分

好,咱们进入第二章。这一章我打算把整个系统的骨架给你搭起来。

你想想看,做价差回归策略,最怕什么?不是策略亏钱,而是系统跑着跑着突然挂了,你还在睡大觉。我早期做量化时吃过这个亏——半夜三点,服务器内存爆了,价差数据全丢了,第二天起来一看,账户亏了六个点。从那以后,我对架构设计就特别较真。

2.1 系统整体架构图

先上一张图,这是我个人习惯的架构风格——分层清晰,职责单一。说白了,就是每个模块只管自己的事,别越界。

价差回归策略实时监控系统架构图 数据层 交易所API 行情数据源 历史数据库 Redis缓存 计算层 价差计算引擎 回归信号检测 风险控制模块 展示层 实时仪表盘 K线图组件 告警通知面板 持久化存储层(MySQL + InfluxDB)

这张图我画得比较简洁,但核心逻辑都在里面了。从上到下分四层:数据层、计算层、展示层、存储层。每一层之间通过消息队列解耦,这样即使某一层挂了,其他层还能继续工作。

核心思路: 数据流是单向的,从数据源到展示,中间不绕路。这样做的好处是——排查问题的时候,你顺着数据流一路查下去就行,不用猜来猜去。

2.2 数据流设计

数据流这块,我踩过的坑最多。刚开始做的时候,我图省事,让每个模块直接去数据库拿数据。结果呢?数据库连接数爆了,查询慢得像蜗牛。后来我学乖了,用消息队列做缓冲。

具体的数据流是这样的:

  1. 行情数据接入:通过WebSocket实时接收交易所的tick数据,丢到Kafka里。
  2. 价差计算:计算引擎从Kafka消费数据,算出现货和期货之间的价差。
  3. 信号检测:把价差数据喂给回归模型,判断是否触发开仓/平仓信号。
  4. 结果推送:信号结果同时写入Redis(给前端展示用)和InfluxDB(给历史分析用)。

嗯,这里要注意一个细节——数据的时间戳必须统一。我见过有人用本地时间,有人用交易所时间,结果价差对不上,白白亏了手续费。我个人习惯全部用交易所的撮合时间,精确到毫秒。

小技巧: 在Kafka消息体里加一个字段叫 source_ts,记录数据产生时的原始时间戳。这样不管后面怎么处理,你都能回溯到最原始的时间点。

2.3 模块划分

模块划分这件事,说白了就是「高内聚、低耦合」。我见过不少新手把代码写成一个大泥球,所有逻辑揉在一起,改一个地方崩一片。咱们别那样干。

我把系统拆成六个模块,每个模块独立部署、独立升级:

模块名称 职责 技术选型
数据采集器 连接交易所,接收实时行情 WebSocket + asyncio
价差计算引擎 计算价差、统计指标 Pandas + NumPy
信号生成器 检测回归机会,生成交易信号 scikit-learn + 自定义规则
风险控制器 检查仓位、资金、最大回撤 独立进程,定时检查
Web展示端 实时仪表盘、K线图、告警 Flask + Socket.IO + ECharts
数据持久化 存储历史数据、交易日志 InfluxDB + MySQL

每个模块之间通过API或者消息队列通信。举个例子,价差计算引擎算完价差后,不是直接调用信号生成器,而是把结果写到Kafka的一个topic里。信号生成器自己去消费。这样做的好处是——如果信号生成器挂了,数据不会丢,等它恢复后继续消费就行。

避坑指南: 我曾经把风险控制器和信号生成器放在同一个进程里。结果信号生成器卡住了,风险控制器也跟着罢工,仓位失控了。后来我强制要求:风险控制器必须独立进程,而且优先级最高。哪怕其他模块全挂了,风险控制器也得活着。

2.4 模块间通信协议

通信协议这块,我推荐用Protobuf。为什么?因为JSON虽然方便,但解析起来慢,而且容易出错。我早期用JSON传行情数据,有一次字段名拼错了,整个系统静默地跑了一天错误数据。用Protobuf的话,字段类型和名称都是强约束的,编译期就能发现问题。

下面是一个简单的Protobuf定义示例:

syntax = "proto3";

message SpreadData {
    string symbol_pair = 1;      // 合约对,比如 "BTC-USDT"
    double spot_price = 2;       // 现货价格
    double futures_price = 3;    // 期货价格
    double spread = 4;           // 价差
    int64 timestamp = 5;         // 毫秒时间戳
    double z_score = 6;          // Z-score
}

message TradeSignal {
    string symbol_pair = 1;
    enum SignalType {
        ENTER_LONG = 0;
        ENTER_SHORT = 1;
        EXIT = 2;
    }
    SignalType type = 2;
    double price = 3;
    double quantity = 4;
    int64 timestamp = 5;
}

你看,字段名、类型、取值范围都定义得清清楚楚。消费方拿到数据后,直接反序列化就能用,不用做任何校验。这比JSON省心多了。

2.5 部署架构要点

最后聊两句部署。我个人习惯用Docker Compose来编排所有服务。每个模块一个容器,资源限制好,日志统一收集到ELK。这样出了问题,直接看Kibana的日志,不用一台台机器去翻。

还有一个容易被忽略的点——网络延迟。如果你的数据采集器部署在A机房,计算引擎在B机房,中间的网络延迟可能让你错过交易机会。我建议把核心模块(数据采集、价差计算、信号生成)部署在同一台物理机或者同一个K8s节点上,用localhost通信,延迟控制在1毫秒以内。

好了,架构设计这块就聊到这儿。下一章咱们开始动手写代码,先从数据采集器开始。


公众号:蓝海数据掘金营,微信deep3321