✅ 策略实例生命周期阶段
- 创建(Created)
- 用户创建策略配置(如币种、价格范围、策略类型等)
- 状态:未运行,存储在数据库中
- 启动(Running)
- 系统调度策略模板 + 参数生成策略实例
- 开始监听行情、风控、交易信号等
- 暂停(Paused)
- 用户主动暂停或系统触发暂停(如断网、限仓)
- 停止执行交易逻辑,但保留状态和内存
- 恢复(Resumed)
- 恢复已暂停策略的运行状态
- 终止(Stopped)
- 用户主动终止或策略运行结束(比如止盈止损)
- 完全释放资源,写入执行日志
- 异常(Error)
- 策略在运行中出现异常,如数据丢失、风控失败等
✅ Java 中如何管理这些生命周期?
可以用一个策略实例管理器来统一调度,比如:
public class StrategyInstanceManager {
private final Map<String, StrategyRunner> activeStrategies = new ConcurrentHashMap<>();
public void startStrategy(String strategyId, StrategyConfig config) {
if (activeStrategies.containsKey(strategyId)) return;
StrategyContext context = new StrategyContext(config);
Strategy strategy = StrategyFactory.createStrategy(config.getType(), context);
StrategyRunner runner = new StrategyRunner(strategy);
activeStrategies.put(strategyId, runner);
runner.start(); // 启动线程或调度器
}
public void stopStrategy(String strategyId) {
StrategyRunner runner = activeStrategies.remove(strategyId);
if (runner != null) runner.stop();
}
public void pauseStrategy(String strategyId) {
StrategyRunner runner = activeStrategies.get(strategyId);
if (runner != null) runner.pause();
}
public void resumeStrategy(String strategyId) {
StrategyRunner runner = activeStrategies.get(strategyId);
if (runner != null) runner.resume();
}
}
✅ 策略状态的持久化与恢复(可选)
- 每个策略状态可以保存在 Redis 或数据库中;
- 系统重启时从持久化层恢复;
- 某些策略(如马丁格尔)需要恢复中间状态,如持仓数量、上次下单价格等。
✅ 本地内存 + Redis 协同方案
✅ Redis 在量化交易系统中的适用场景
- 策略配置缓存共享
- 如果有多个节点,策略配置(如策略参数、交易对、限额)可以存储在 Redis 中,做到共享。
- 例如:
Key: strategy:config:strategyId123 Value: { "type": "Grid", "symbol": "ETH/USDT", "gridCount": 20, ... }
- 策略状态同步
- 如果一个策略实例需要在多个节点之间迁移或恢复,可以把它的运行状态保存在 Redis(如当前仓位、最近成交价等)。
- 避免单节点崩溃导致状态丢失。
- 分布式协调与调度
- 用 Redis 锁来协调多个实例之间的抢占式策略调度。
- 也可用于 Leader 选举、策略分片元数据存储等。
- 订阅推送
- 可以用 Redis 的
Pub/Sub
模块做行情推送、策略热更新等通知类广播。
- 可以用 Redis 的
❌ 不推荐的用途:
- 策略对象存储(如 StrategyInstance)
- Redis 是键值型缓存系统,不适合存储复杂、有状态、可执行的 Java 对象(如含线程、回调逻辑)。
- Java 的策略对象应始终保存在本地内存中,只有其**状态(持仓、参数、结果)**可以同步到 Redis。
- 高频调度核心逻辑
- Redis 是远程访问,虽然快,但网络调用 + 序列化还是比内存慢几个数量级。
- 高频策略触发、价格推送后的策略执行等必须本地完成,Redis 不能做中间调度层。
✅ 最佳实践:本地内存 + Redis 协同方案
类型 | 存储位置 | 用途 |
---|---|---|
策略逻辑代码 | 本地(Java 类) | 执行策略逻辑 |
策略运行实例 | 本地内存 | 快速响应行情,保存运行上下文 |
策略配置 | Redis(+DB) | 多节点共享加载,启动时拉取 |
策略状态快照 | Redis(或 DB) | 恢复、监控、同步 |
行情/通知 | Redis Pub/Sub | 广播新行情,热更新策略 |
任务分配元数据 | Redis 哈希 | 多节点负载协调 |
Redis 是分布式缓存的好帮手,但不是实时策略执行的核心容器。
- ✅ 适合存配置、状态、通信。
- ❌ 不适合存运行中策略对象或执行逻辑。
量化交易系统如果是分布式部署的,本地缓存确实会遇到几个关键问题:
❗ 本地缓存的问题(在分布式部署中)
- 策略数据不一致:不同节点各自持有本地缓存,策略更新、下线、修改参数时很难同步。
- 节点漂移问题:Kubernetes 等平台可能动态扩缩容,导致策略实例在某节点突然失效或迁移。
- 热更新困难:无法集中控制策略的启停、调整,必须广播通知所有节点。
- 内存负担大:每个节点可能需要缓存全部策略实例,造成资源浪费。
✅ 使用分布式缓存(如 Redis)的可行方案
👉 建议使用 分布式缓存 + 本地运行态解耦 的设计模式:
模块 | 描述 |
---|---|
Redis 分布式缓存 | 存储策略配置、参数、分配信息、状态快照。统一读写。 |
本地内存 | 仅保存当前节点所调度的部分策略的“运行时实例对象”(如 Java 类)。 |
策略注册表(Redis 哈希) | 记录哪个策略运行在哪个节点,由中心调度器维护。 |
策略负载中心 | 自动将策略按规则分片下发给节点(例如使用一致性哈希或分片算法)。 |
Redis Pub/Sub | 用于广播行情、策略更新、重新调度等事件。 |
🔧 示例架构(简化):
[ MySQL ] <--持久化-->
|
[ Redis ]
| (配置/状态/策略注册表)
↓
[调度中心](统一分配策略给各节点)
|
|--> 节点A:[持有 5万个策略实例,运行中,本地缓存]
|--> 节点B:[持有 5万个策略实例,运行中,本地缓存]
|
+--> Redis Pub/Sub:行情/指令广播
✅ 如何动态调度 + 分片?
- 使用 Redis 存储每个策略的分片信息(策略ID → 所属节点)
- 每个节点仅从 Redis 加载自己负责的策略
- 新节点加入或旧节点宕机时,重新分配策略并通知更新缓存
- 可参考:一致性哈希 / 分段式平均调度 / 分布式调度框架(如 Apache ShardingSphere)
🚫 直接把策略实例对象放入 Redis 是不行的
- Java 的策略类对象(包含业务逻辑和线程)不能存入 Redis(Redis 不支持 Java 对象图)
- 所以:Redis 存配置 + 本地运行状态对象 才是标准做法
✅ 最佳实践
内容 | 存储方式 |
---|---|
策略配置 / 参数 | Redis(可落盘到 MySQL) |
策略运行实例 | 本地内存,仅运行该策略的节点持有 |
策略状态 / 中间结果 | Redis(或数据库) |
策略分配元数据 | Redis 哈希或 ZSet |
策略事件通知 | Redis Pub/Sub / Kafka / MQ |
这种 “本地运行 + 分布式缓存协调” 的架构正是当今多数大型虚拟货币交易所、量化平台、策略托管服务中采用的主流设计方案。
✅ 为什么是主流?
🚀 满足性能 + 扩展性的双重要求:
需求 | 解决方案 |
---|---|
高频行情触发,低延迟策略执行 | 使用本地内存缓存策略实例,避免远程 I/O |
策略数据共享、多节点部署 | 用 Redis(或 etcd/ZooKeeper)集中管理配置、状态和调度元数据 |
自动扩缩容、弹性部署 | 支持 Kubernetes 等环境动态添加或移除节点,同时通过分布式协调机制(如 Redis 哈希表或一致性哈希)分配策略 |
策略热更新、灰度发布 | 使用 Redis Pub/Sub 或消息队列广播策略更新事件,各节点实时同步 |
容灾与恢复能力强 | 策略状态定期快照至 Redis 或数据库,支持容器或节点宕机恢复 |
✅ 实际案例参考
交易平台 / 框架 | 使用模式 |
---|---|
Binance、OKX、Coinbase | 使用微服务架构,行情与策略执行解耦,多语言组合部署 |
OneQuant、SuperQuant | 提供策略托管服务,用 Redis + Kubernetes + 本地内存做分布式策略调度 |
国内某大型交易平台(保密) | 将策略状态缓存在 Redis,策略对象驻留本地,通过 MQ 做行情广播与策略更新 |
🚫 非主流的老旧方案:
- 单体部署或策略执行全靠数据库调度(性能差、不可扩展)
- 策略全部驻留 Redis(不支持逻辑与线程)或文件(难维护)
现代量化交易系统(尤其在交易所场景)几乎都采用:分布式缓存协调 + 本地运行实例 + 消息广播机制 的组合架构。
哪怕你用了 Redis 这样的分布式缓存系统,如果策略运行时实例(即策略逻辑、状态)仍然保存在各节点的本地内存中,理论上仍然会存在一致性问题。但注意,这并不意味着“本地内存就错了”——恰恰相反,合理使用本地内存 + 分布式缓存协调机制,是经过权衡后的主流做法。
✅ 为什么仍然使用本地内存?
- 策略必须低延迟响应(毫秒级),Redis 的访问延迟(即便是 1ms)在高频场景下仍然太高。
- 策略对象是有状态的 Java 实例(例如持有定时器、线程、逻辑栈),不适合直接存入 Redis。
- 每个节点仅维护自己分配的部分策略,并不会所有节点都保存全部数据,数据本身是分片的,不是复制的。
❗ 那数据一致性怎么解决?
不是让所有节点都同步“策略实例”,而是通过以下机制让一致性“可控”:
🔸1. 策略分片调度机制(分布式分配策略到节点)
- 每个策略只被一个节点加载并运行,其他节点不会持有它,从源头避免并发冲突。
- 典型做法:使用 Redis 哈希表记录
{strategy_id -> node_id}
,调度中心统一分发。 - 当节点上下线时,重新分配策略并通知其他节点清除/加载缓存。
🔸2. 策略状态持久化
- 每个策略的关键状态(持仓、盈亏、网格位置等)定期快照保存到 Redis/MySQL。
- 一旦节点宕机,策略在其他节点恢复时可读取状态重启,达到最终一致性。
🔸3. 热更新与配置同步
- Redis Pub/Sub 或 Kafka 负责发布 “策略更新/下线/上线” 指令,所有节点监听后实时调整本地内存。
- 可设计 TTL + 校验机制避免策略“缓存污染”。
✅ 如何看待一致性与性能的权衡?
模型 | 描述 | 一致性 | 性能 |
---|---|---|---|
全部 Redis | 所有策略数据实时存 Redis | 强一致性 | 🚫 极差 |
本地缓存 + Redis 元信息 | 运行时状态本地,控制/配置走 Redis | 最终一致性 | ✅ 高 |
分布式状态机(Raft) | 极高一致性,复杂实现 | 强一致性 | 🚫 性能弱,复杂度高 |
现代交易所普遍接受“最终一致性”模型,以获得实时执行能力与高吞吐。
- 使用本地内存会带来一致性挑战。
- 但只要设计了清晰的分片调度机制 + 状态快照恢复 + 配置广播同步,问题是可以控制的。
- 本地内存换取的是 毫秒级性能,这是交易系统不可妥协的要求。
- 所以,这不是“违背一致性”,而是基于性能要求下的架构折中 —— 这正是现代交易所的主流做法。
Kubernetes 的自动扩容与缩容机制确实会给量化交易系统中“策略分布与运行状态”的一致性与调度带来挑战。但现代量化系统已经有一套成熟、稳健的设计模式来解决这个问题。
✅ 问题本质
Kubernetes 自动扩缩容会导致:
- 节点数量变化,策略运行实例需要动态迁移。
- 某个策略运行节点突然消失,其本地内存状态丢失。
- 新节点加入后需要分配新策略,避免策略“无人运行”或“多节点重复运行”。
✅ 解决方案:引入“策略调度中心”+“分布式状态协调”
🧠 1. 引入统一的策略调度中心(Strategy Coordinator)
这个组件通常作为一个单例服务运行,负责:
- 记录哪些策略运行在哪个节点(例如保存在 Redis 的
strategy_id -> node_id
映射中) - 监听 Kubernetes 节点变化(通过 ServiceRegistry 或 k8s API)
- 重新分配策略(例如采用一致性哈希或平均分布)
示例调度流程:
- 节点扩容:调度器感知新节点 → 抽取部分策略 → 更新分配表 → 通知老节点释放、新节点加载
- 节点缩容或挂掉:感知节点失联 → 重分配其策略 → 通知新节点加载状态 → 恢复运行
🔁 2. 策略实例状态支持“热迁移”(支持重启)
为实现“节点失效时的快速恢复”:
- 策略运行时的状态(如持仓、挂单、价格历史)定期存 Redis 或 MySQL 快照
- 每个策略类必须实现:
void restoreFrom(StateSnapshot snapshot); StateSnapshot snapshot();
这样新节点加载策略实例后,可以立即恢复状态并继续运行。
📣 3. 利用 Kubernetes 生命周期钩子 + Redis 广播机制
- 使用
PreStop
Hook:节点缩容前通知调度中心清理策略绑定 - 使用
PostStart
Hook:节点启动后自动注册自身能力、接收新策略 - Redis Pub/Sub 广播“策略迁移”、“行情同步”、“策略下线”等事件
✅ 实战架构图(简化)
┌─────────────┐
│ Strategy DB │◄────────────┐
└────▲────────┘ │
│ ▼
┌────┴────┐ Redis ┌──────────────┐
│调度中心 │◄──────────►│ Strategy Map │
└────▲────┘ └──────────────┘
│ ▲
│ Pub/Sub │
│ ┌──────────┐ │
└─────►│节点A(Pod)│◄────┘
│节点B(Pod)│
└──────────┘
在 Kubernetes 自动扩缩容环境中,只要你设计了策略“可迁移”、状态“可恢复”、调度“中心化”且同步“及时”,完全可以实现高可用、高一致、可扩展的量化系统。
在量化交易系统中运行的本地缓存必须是线程安全的,尤其是在满足以下条件之一时:
✅ 你需要线程安全缓存的典型场景:
- 多线程调度策略执行
如果你使用线程池(如ExecutorService
)异步地去执行多个策略实例,比如:
那么多个线程可能会同时访问或修改缓存(如策略注册表、运行状态、策略实例),此时必须线程安全。threadPool.submit(() -> strategy.onPrice(price));
- WebSocket 或行情服务在异步推送数据
多个异步推送线程/回调线程都可能触发策略调度,访问缓存。 - 策略动态热更新
例如,后台管理员下发指令让某个策略动态上线、下线或切换参数,会修改缓存结构。
✅ 常见线程安全缓存方案:
1. 使用 ConcurrentHashMap
private final ConcurrentMap<String, StrategyInstance> strategyMap = new ConcurrentHashMap<>();
这是最常见也最简单的线程安全缓存容器,适合读多写少的场景。
2. 使用 Guava Cache(线程安全 + 过期 + 最大容量)
Cache<String, StrategyInstance> cache = CacheBuilder.newBuilder()
.expireAfterAccess(10, TimeUnit.MINUTES)
.maximumSize(100_000)
.build();
适合做策略实例缓存或行情快照缓存。
3. 使用 Spring 的 @Cacheable
+ @EnableCaching
如果你是基于 Spring Boot 开发的,也可以接入 Spring Cache,配合 Caffeine 或 Redis 实现本地/分布式缓存一致性。
4. 使用Caffeine
Caffeine 是线程安全的,并且是目前 Java 中性能非常优秀的一种本地缓存库。
- Caffeine 内部基于高性能的并发数据结构(如分段锁、无锁 CAS 操作、Striped Locking 等),
- 所有公共方法(如
get()
,put()
,invalidate()
等)都是线程安全的, - 支持并发读写、高吞吐,是 Guava Cache 的性能继承者和升级版。
import com.github.benmanes.caffeine.cache.*;
Cache<String, StrategyInstance> strategyCache = Caffeine.newBuilder()
.maximumSize(200_000)
.expireAfterAccess(30, TimeUnit.MINUTES)
.build();
// 并发访问是安全的
strategyCache.put("strategy-123", new GridStrategy());
StrategyInstance instance = strategyCache.getIfPresent("strategy-123");
你可以放心在多线程环境中并发调用 put()
、get()
、invalidate()
等操作,无需额外加锁。
✅ Caffeine 还能干什么?
- 自动过期(基于时间或引用)
- 最大容量限制(LRU-like 淘汰)
- 支持
CacheLoader
的懒加载机制 - 异步刷新、监听缓存驱逐事件
Caffeine 是线程安全的,并且非常适合在量化交易系统中用作策略缓存、行情缓存或风控缓存。
❗ 如果不线程安全,会出现什么问题?
- 多线程同时注册或销毁策略,导致缓存不一致
- 多线程同时读取和写入策略状态,可能出现数据竞争
- 导致某些策略被重复执行或根本不执行
- 难以排查的并发 BUG,系统稳定性差
线程安全是本地缓存设计中的刚需。无论你采用哪种缓存方式,只要你启用了并发行情调度、策略线程池或分布式协调,就必须保证本地缓存的数据结构具有线程安全性。
是否可以只采用本地缓存的形式
在现代交易所中,完全只用本地缓存、完全不用 Redis 的设计很少见,即使它性能高,也会在一致性、扩缩容、故障恢复等方面留下致命隐患。
✅ 为什么不能只依赖本地缓存?
1. Kubernetes 环境中的 Pod 无状态 + 动态调度
- 每次部署、扩容、崩溃都会导致实例变化。
- 本地缓存中的策略信息无法跨节点同步。
- 一旦某个节点掉线或重启,策略就“丢失”了。
2. 多节点运行时策略分布一致性问题
- 多个节点运行,需要协调策略在哪个节点上执行。
- 不能保证一个策略只运行一次(避免重复执行或漏执行)。
3. 节点崩溃时策略状态无法恢复
- 如果策略持仓、挂单状态仅存在本地,一旦节点宕机就无法恢复。
✅ Redis 的作用(为什么现代系统几乎都会用)
功能 | Redis 扮演的角色 |
---|---|
策略分布表 | 哪些策略在哪个节点运行(strategy_id -> node_id ) |
策略状态快照 | 实时状态记录,供迁移和恢复 |
节点注册 | 可用节点列表(如心跳、负载) |
消息广播 | 策略迁移、更新、行情同步(Pub/Sub) |
保证一致性 | 所有节点统一获取策略、统一调度 |
✅ 典型部署模型
┌─────────────┐
│ Redis │ ← 策略映射表 & 快照 & Pub/Sub
└─────┬───────┘
│
┌─────▼────────────┐
│ Strategy Node A │ ← 本地缓存 + 策略线程池
├──────────────────┤
│ Strategy Node B │ ← 本地缓存 + 策略线程池
└──────────────────┘
每个节点用本地缓存提升运行时性能,但元数据和状态都依赖 Redis 保证全局一致性和容错性。
✅ 性能瓶颈问题如何解决?
担心使用 Redis 影响性能,现代系统的做法是:
- 只在初始化时查 Redis,运行时用本地缓存;
- 状态变更批量异步写 Redis,降低写压;
- 避免频繁依赖 Redis 调度,调度中心主动推送策略。
这样可以达到 高性能 + 高一致性 + 高容错 的平衡。
方案 | 是否主流 | 风险 | 场景 |
---|---|---|---|
纯本地缓存,不用 Redis | ❌ 否 | 数据丢失、重复执行、无法迁移 | 仅适合单节点开发/测试环境 |
本地缓存 + Redis 同步 | ✅ 是 | 控制得当无大风险 | 现代量化交易系统主流方案 |
本地缓存和 Redis 中的数据
✅ Redis 中存什么?
✅ 本地缓存中存什么?
✅ 二者如何协同工作?
✅ 1. Redis 中存储的内容(全局共享、跨节点可见)
Redis 存的是“全局元数据”和“轻量状态”,主要包括:
Redis Key | 类型 | 内容说明 |
---|---|---|
strategy:meta:{strategyId} | Hash | 策略基础信息(如参数、绑定symbol、执行类名、节点分配) |
strategy:node:{nodeId} | Set | 当前节点运行的策略 ID 列表(用于分配、迁移、下线) |
strategy:status:{strategyId} | Hash | 当前策略状态(如启用/暂停、持仓、止盈止损等) |
strategy:params:{strategyId} | Hash | 可修改的运行参数(如价格区间、网格数等) |
strategy:heartbeat:{nodeId} | String | 心跳时间戳或负载指标(用于节点健康检查) |
strategy:updates | Pub/Sub | 策略更新/上线/下线 通知(广播给各节点) |
✳️ Redis 是协调器,不负责高频行情调度
- Redis 不存行情数据(频繁更新太重);
- 也不存策略实例(运行时状态太大);
- 只记录必要的 状态、参数、节点分配、通知信息。
✅ 2. 本地缓存中存储的内容(高性能执行用)
本地缓存用于运行时快速访问,每个节点仅缓存它负责的策略。
缓存项 | 类型 | 说明 |
---|---|---|
Map<String, StrategyInstance> | 并发缓存 | 策略实例对象(含业务逻辑和状态) |
Map<String, StrategyMeta> | 轻量缓存 | 策略元信息(symbol、用户ID、状态等) |
Map<String, SymbolData> | 行情快照 | 当前行情,用于触发回调 |
Map<String, StrategyParam> | 策略参数 | 调整用的运行参数 |
✅ 3. 协同机制(运行模型)
Redis 节点 A(本地缓存) 节点 B
┌──────────────────┐ ┌────────────────────┐ ┌────────────┐
│ strategy:meta:* │ ───────→ │ 加载策略信息 │ │ │
│ strategy:node:* │ ←─────── │ 注册、回写执行节点 │ │ │
│ Pub/Sub 更新通知 │ ───────→ │ 热更新/停止策略 │ │ │
└──────────────────┘ └────────────────────┘ └────────────┘
✅ 举例说明:一个策略的生命周期流程
- 策略上线(新建)
- 后台写入 Redis 中的
strategy:meta
和strategy:params
- 分配给某个节点,更新
strategy:node:{nodeId}
集合 - 发布
Pub/Sub
通知该节点加载策略
- 后台写入 Redis 中的
- 节点接收到通知
- 拉取 Redis 的策略元数据 + 参数
- 本地构建
StrategyInstance
并注册到本地缓存 - 开始运行(通过线程池、事件驱动等方式)
- 策略运行中
- 每次收到行情,匹配本地缓存的策略
- 策略内部状态(如仓位、触发时间等)仅保存在内存中
- 定期异步写回 Redis(如果需要持久化)
- 策略更新
- 改变参数或状态(如暂停)
- 写入 Redis → 通知 → 节点更新本地缓存
- 策略下线
- 从 Redis 和本地缓存中移除
- 停止线程、释放资源
策略模版个数
在现代虚拟货币交易所或量化平台中,常用的策略模板大致在 10~50 个之间,这取决于平台的业务目标和用户层级。
✅ 常见策略模板分类
类型 | 策略模板示例 | 数量估算 |
---|---|---|
趋势类 | 均线交叉、MACD、动量、通道突破 | 5~10 个 |
震荡类 | 网格交易、区间套利、反转策略 | 5~8 个 |
量价类 | 成交量突破、OBV 策略、资金流量指标 | 3~5 个 |
风控型 | 马丁格尔、反马丁、金字塔加仓 | 3~5 个 |
套利类 | 跨期套利、跨品种套利、三角套利 | 3~6 个 |
统计类 | 均值回复、协整对冲 | 3~5 个 |
自定义脚本 | Python/DSL 自由策略 | 1 个(泛化) |
✅ 平台策略模板数量示意
平台类型 | 模板数量 | 特点 |
---|---|---|
专业机构交易所 | 10~30 个 | 偏重机构和 API 用户,策略灵活度高 |
面向散户平台 | 20~50 个 | 模板多样、可视化配置,策略简洁易懂 |
API 或策略托管平台 | 10~20 个内置 + 开放自定义 | 用户可上传策略代码或 DSL |
✅ 示例策略模板列表(部分)
- 网格策略(Grid)
- 马丁格尔策略(Martingale)
- 均线交叉(MA Cross)
- 趋势突破(Breakout)
- 均值回复(Mean Reversion)
- 双币套利(Stat Arb)
- 资金流量反应(Money Flow)
- 区间震荡识别
- 阶梯建仓
- 手动辅助策略(止盈止损)
- 限价挂单策略(Maker Taker 切换)
策略模板 ≠ 策略实例
一个模板(如网格)可以衍生出成千上万不同参数的策略实例。
所以在交易所内部,只需维护有限个“可插拔的策略模板类”,通过参数驱动生成大量策略实例。
对于像 Gate.io 这样的大型虚拟货币交易所,考虑到它提供的 交易产品 和 用户群体的多样性,策略模板的数量会更多一些,且会根据不同的用户需求提供多种定制化选项。
-
基础策略模板(核心模板):大概 15-30 个
这些模板覆盖常见的基础交易策略,用户可以直接选择并进行配置。
例如:- 网格交易(Grid Trading)
- 趋势追踪(Trend Following)
- 均值回归(Mean Reversion)
- 马丁格尔(Martingale)
- 跨期套利(Arbitrage)
- 动量交易(Momentum Trading)
- 高频交易(High Frequency Trading)
- 震荡区间(Range Trading)
- 跨品种套利(Cross-pair Arbitrage)
- 成交量策略(Volume-based)
- 动态止盈止损(Dynamic TP/SL)
-
高级策略模板:大概 5-10 个
这些策略适合更有经验的用户,涵盖一些复杂的逻辑和算法,需要较为灵活的参数配置。
例如:- 高频套利(High-Frequency Arbitrage)
- 机器学习策略(基于模型的交易)
- 对冲策略(Hedging Strategies)
- 自适应策略(Adaptive Strategies)
- 基于数据流的策略(Order Flow Strategy)
- 风险管理策略(例如止损、止盈动态调整)
-
用户自定义策略模板:1-5 个
提供脚本功能,用户可以通过 Python、DSL 或 其他语言 来实现完全自定义的策略。 -
风控策略模板:3-5 个
主要用于风险控制,比如:- 最大亏损控制(Maximum Drawdown Control)
- 最大持仓限制(Maximum Position Limit)
- 风险值调整(Risk Adjustment)
✅ 总策略模板数量估算:
- 基础策略模板:约 15-30 个
- 高级策略模板:约 5-10 个
- 用户自定义脚本:约 1-5 个
- 风控策略:约 3-5 个
总数:大约 24 ~ 50 个策略模板。这个数字可以根据不同交易所的产品需求、目标市场和平台的复杂性有所浮动。
- Gate.io 这样的交易所,预计会有 30~50 个策略模板,涵盖基本策略、风控、用户自定义脚本等多个方面,满足不同交易者的需求。
策略快照存储
策略执行快照(或状态)可以选择性地存储在 Redis 或 MySQL 中之一,并不是必须同时存储在两者中。
✅ 为什么不是必须“双写”到 Redis 和 MySQL?
这是因为这两种存储之间有明显的权衡:
存储位置 | 优点 | 缺点 | 适用场景 |
---|---|---|---|
Redis | 读取极快、支持高并发写入、适合实时计算 | 内存成本高、不持久(除非配置持久化) | 实时策略调度、频繁状态变更、缓存为主 |
MySQL | 持久存储、安全可靠、易于查询分析 | 读写性能相对低、更新频繁会拖慢系统 | 策略审计、历史追踪、冷启动恢复 |
✅ 常见设计方案:
✅ 方案 1:只用 Redis 存储快照(主流)
- 实时策略运行期间,快照状态全部保存在 Redis。
- 只有在特定事件(如停机、异常)时,将快照落盘至 MySQL。
- 适用于注重性能的系统。
✅ 方案 2:Redis + 定期持久化至 MySQL
- Redis 做实时存储,MySQL 做冷备。
- 每 N 秒 / 每笔关键操作 后异步写入 MySQL。
- 适合数据较敏感、有合规/审计需求的系统。
✅ 方案 3:只用 MySQL 存储(很少见)
- 所有快照存入 MySQL。
- 实时性较差,基本不用于高频策略系统。
✅ 所以是否“双写”依赖于具体需求:
- 如果系统对性能要求极高 → 用 Redis 即可。
- 如果系统对策略状态一致性 & 可追溯性要求高 → Redis + MySQL 双写。
- 如果系统不运行实时策略,仅用于回测或低频策略 → 直接用 MySQL 也可以。
策略实例设计模式
采用“无状态策略模板 + 实例运行时参数”这种设计模式,使用 Spring 默认的单例 Bean 完全可以搞定,这是目前在很多现代量化平台中广泛采用的方案。
✅ 为什么单例 Bean 没问题?
因为:
- 策略模板类(比如
GridStrategyTemplate
)只是逻辑载体,不持有任何实例状态。 - 所有状态(如买入价格、网格间距、最大持仓等)都封装在策略实例对象里。
- 实例在调度中执行时,会把自己的参数传入模板类方法,进行策略逻辑计算。
这种模式下,多个策略实例“共享”一个模板对象但不共享运行状态,所以不会冲突。
✅ 简化类图:
┌────────────────────┐
│ GridStrategyTemplate │ <-- 单例 Spring Bean,无状态
└────────────────────┘
▲
│
┌───────┴──────────────┐
│ StrategyInstance(id=001) │ <-- 各自保存不同参数、状态
│ StrategyInstance(id=002) │
│ StrategyInstance(id=003) │
└─────────────────────────┘
- Spring 管理简单:无须使用原型或手动创建 Bean
- 线程安全:所有运行状态不在共享 Bean 中,线程之间互不影响
- 性能高:避免频繁创建对象,节省 GC 和上下文切换
是的,只要策略模板类是无状态的,使用 Spring 默认的单例 Bean 就是主流、安全、性能最优的设计方案。你无需担心冲突或报错。
触发策略实例的运行
在分布式量化系统中,策略实例的运行通常由“调度系统”或“事件驱动系统”触发。
✅ 策略实例不会主动运行,而是被动等待触发
它们像是“待命状态”的逻辑单元,等着被市场数据、定时任务等“叫醒”。
👇 通常有哪些触发源?
1. 行情推送(最常见)
- 比如 ETH/USDT 最新成交价更新了
- 行情系统通过 WebSocket/Kafka/Redis pubsub 通知调度系统
- 调度系统查找订阅了 ETH/USDT 的所有策略实例
- 然后将事件派发到各个策略实例的
onPrice()
方法上
✅ 典型调用链:
行情系统 ➝ 调度系统 ➝ 策略实例.onPrice(price)
2. 定时触发(如:网格、马丁格尔补仓)
- 系统有定时任务模块(如 Quartz、Spring Task、Timer)
- 每分钟、每小时触发一次某些策略的
onTimer()
方法 - 比如:判断是否需要重新挂单、调整网格
3. 风控事件
- 系统检测到某个账户浮亏超过阈值
- 通知调度系统取消策略、调整仓位
- 调用策略的
onRiskTrigger()
方法
4. 用户操作事件
- 用户手动启停某个策略
- 页面调用后台 API,调度系统找到该策略实例并执行
start()
或stop()
调度系统
在现代虚拟货币交易所中,调度系统通常和量化交易系统属于同一个微服务体系,甚至就集成在同一个微服务中,作为内部模块协同工作,这样可以极大简化调度链路,提高实时性和可靠性。
绝大多数交易所(尤其是自研私有系统)使用这种方式:
方案 | 是否独立微服务 | 是否主流 | 适用场景 |
---|---|---|---|
✅ 集成在量化微服务中 | 否 | ✅ 是主流 | 中小型量化平台,大多数交易所 |
可独立部署 | 是 | 次主流 | 超大规模系统,或复杂 SaaS 架构 |
量化交易系统接入行情数据
行情接入服务独立于量化交易系统之外,在现代交易所中是分布式部署的,原因在于:
目标 | 原因 |
---|---|
💡 高可用 | 任意一个节点挂了,不影响整体行情接收 |
🚀 高性能 | 行情数据量巨大(每秒上千次 tick),需多节点并行处理、节流、广播 |
⚙️ 负载均衡 | 有的行情源连接数有限(如交易所 WS 限制),需多节点拆分连接 |
🔄 热更新 | 接入新交易对/新交易所时不影响老节点运行 |
⛓️ 分区处理 | 不同交易对分配到不同节点,减小每个节点压力 |
✅ 实际部署规模:参考范围
平台类型 | 接入服务节点数(估算) |
---|---|
🧪 中小型交易所 / 量化平台 | 2 ~ 5 个副本 |
⚙️ 中大型量化平台(如 Pionex、3Commas) | 5 ~ 10 个副本 |
💰 顶级交易所(如 Binance 内部系统) | 按交易对、市场区域分区部署,几十个节点以上 |
数量取决于以下因素:
- 接入的交易对数量(几百 vs 几万)
- 每秒行情更新频率
- 是否只做本地现货行情,还是还包括外盘期货、期权、DEX 等
- 是否支持多交易所行情聚合(Binance、OKX、Gate、Bybit…)
行情接入服务几乎总是分布式部署的。在中大型平台中,通常部署 3~10个副本,每个副本负责一部分交易对或接入源,并将统一格式的行情广播到系统中其他模块使用。
在现代量化系统中,行情接入服务通过 Kafka 将行情广播给所有量化系统节点,而每个节点根据自己的分配策略,从这些广播中筛选并执行真正相关的策略逻辑。这种架构既满足高性能,又满足可扩展性,是现代虚拟货币交易所中最主流、可扩展性最强的做法。
优点 | 说明 |
---|---|
🔁 可扩展 | 新增量化节点不影响现有系统结构 |
🎯 精准执行 | 每个节点只处理自己的策略实例 |
⚡ 实时性高 | Kafka 的高吞吐和低延迟保障实时策略响应 |
🧱 解耦架构 | 行情接入服务和量化系统解耦,职责分明 |
Kafka 广播设计
✅ Kafka 是可以做“广播”的 —— 取决于消费者组的配置方式。
Kafka 本身并不是“广播系统”,但通过合理使用 多个消费者组,可以模拟广播效果。
🎯 关键概念:Kafka 中“广播” vs “组消费”
模式 | 消费者组 ID | 消费效果 | 场景 |
---|---|---|---|
组消费(Group Consumption) | 相同组 ID | 多个消费者共享消息,每条消息只被1个消费者消费 | 多线程处理、负载均衡 |
广播消费(Fan-out) | 每个消费者用不同组 ID | 每个消费者都能消费同样的消息 | 多个系统或节点都需要处理同样数据 |
如果希望 Kafka 消息被所有量化交易系统节点都消费到,就不能将它们放到同一个消费者组中。
🔁 正确做法:每个节点使用不同的 group.id
Properties props = new Properties();
props.put("group.id", "quant-node-A"); // 每个节点不一样
✅ 应用到量化系统的场景
假设:
- 行情接入服务将 BTC/USDT 的价格推送到 Kafka topic:
tick.BTCUSDT
- 有 3 个量化节点,每个节点都负责独立的一批策略实例
- 希望所有节点都能收到该消息、并自行判断是否处理
那么应该这样做:
- 每个节点启动 Kafka consumer,使用不同的 group.id
quant-node-1
quant-node-2
quant-node-3
- Kafka 将同样的消息推送给所有三个 group
- 每个节点判断该 symbol 是否归属于自己,并决定是否执行策略
📌 Kafka 广播 vs 真正的广播中间件(如 NATS、RabbitMQ)
系统 | 是否真正支持广播 | 特点 |
---|---|---|
Kafka | ✅(通过多个 group 实现) | 可回溯、高可靠、分区控制 |
RabbitMQ(Fanout exchange) | ✅ | 原生广播支持 |
Redis Pub/Sub | ✅ | 轻量但不持久化,重启即丢 |
NATS | ✅ | 高性能消息广播中间件 |
Kafka 的广播虽然不是“原生”,但在大型系统中更稳定、易于管理、可回放重播,所以非常主流。
Kafka 支持广播消费,但需要 每个量化节点使用不同的消费者组 ID。如果多个节点在同一个组中,那就是负载均衡(每条消息只被消费一次),不是广播。
✅ 量化交易系统广播设计
- ✅ 所有量化系统节点运行的是相同的代码包;
- ✅ 每个节点都需要作为Kafka 的独立消费者(用于实现广播消费);
- ✅ 同时需要支持 Kubernetes 自动扩容/缩容 —— Pod 数量是动态的;
- ❗但 Kafka 的广播消费要求每个节点使用唯一的
group.id
,否则就会被当作负载均衡模式处理,导致消息只被一个节点消费。
✅ 解决方案:使用动态唯一的 group.id
,结合 Kubernetes Pod 身份标识
✅ 1. 每个节点使用唯一的 group.id
用以下这些方法生成唯一的 group.id:
方法 | 说明 |
---|---|
使用 Pod 名字 | 在启动脚本中注入:group.id = "quant-group-" + $(hostname) |
使用 UUID | 启动时生成唯一 UUID:group.id = "quant-node-" + UUID.randomUUID() |
使用配置模板 | application-quant.yaml 中自动占位:group.id = quant-#{random.value} |
推荐使用 Kubernetes 的 Pod 名或 StatefulSet index,这样即使重启也稳定。
✅ 2. 在 Kubernetes 中实现 group.id 的自动注入
🛠️ 方法一:使用 Pod 环境变量注入 hostname
env:
- name: GROUP_ID
valueFrom:
fieldRef:
fieldPath: metadata.name
然后在 Java 应用里读取:
String groupId = "quant-group-" + System.getenv("GROUP_ID");
props.put("group.id", groupId);
🛠️ 方法二:使用 init script 或 Helm 模板注入唯一参数
- name: GROUP_ID
value: "quant-{{ .Release.Name }}-{{ .Pod.Name }}"
✅ 自动扩容缩容怎么影响?
Kubernetes 扩容时:
- 新 Pod 自动获得唯一 group.id → 成为独立 Kafka 广播消费者 → 自然接入系统。
Kubernetes 缩容时:
- 被杀掉的 Pod 的 group.id 不再消费消息 → Kafka 自动回收该订阅者。
🎯 所以:这种模式天然适配 Kubernetes 动态扩缩容。
行情系统 Kafka topic 设计
行情接入服务将数据发送到 Kafka,topic 的划分方式直接决定了:
- 下游量化系统如何订阅(按币种、品种、频道等);
- Kafka 的分区数、性能、资源消耗;
- 消息路由和消费逻辑的复杂度。
✅ 主流做法:按“频道类型 + 分片”划分 topic,不是一个币种一个 topic
❌ 不推荐做法:一个币种一个 topic
例如:
tick.BTCUSDT
tick.ETHUSDT
tick.DOGEUSDT
这在币种较少时可以,但现代交易所动辄几千个币对,Kafka topic 可能高达几千个,管理复杂、性能下降、不可扩展。
✅ 推荐做法:聚合 topic + 分区控制 + 消息过滤
主流交易所(如 Binance、OKX)使用以下方式:
🟢 示例:统一 topic(如 tickers
) + 分区 + 消息体中包含 symbol 字段
Topic: tickers
Message payload:
{
"symbol": "BTCUSDT",
"price": 63682.1,
"ts": 1715151210000
}
特点:
优点 | 说明 |
---|---|
✅ 可控 topic 数量 | 一般 1~10 个,便于 Kafka 集群维护 |
✅ 分区可扩展 | 每个分区可能按字母/哈希对 symbol 分片 |
✅ 消费者过滤灵活 | 下游按 symbol 精准消费,Kafka topic 层结构简洁 |
✅ 支持广播 | 所有节点可订阅同一 topic,按需判断是否处理 |
✅ 支持 Kafka 高性能特性 | Compact、retention、replay 更容易实现 |
📌 Kafka 分区建议(配合聚合 topic 使用)
为了高性能,topic 通常配置多个分区(如 10、50、100),再按 symbol 的 hash 值将数据均匀分布到各个分区。
int partition = Math.abs(symbol.hashCode()) % numPartitions;
producer.send(new ProducerRecord<>("tickers", partition, key, value));
✅ 下游量化系统如何处理?
- 所有节点订阅
tickers
; - 消息体中包含 symbol;
- 节点本地过滤:若 symbol 属于我负责的策略实例 → 触发执行。
if (localCache.hasSymbol(record.value().getSymbol())) {
strategyDispatcher.dispatch(record.value());
}
✅ 常见行情类 Kafka Topics 示例:
Topic 名称 | 说明 | 消息内容示例 |
---|---|---|
tickers | 最新价格快照(每对一条) | BTCUSDT 最新成交价 |
depth.deltas | 委托薄增量更新 | 增加/取消挂单(level 2) |
depth.snapshots | 委托薄快照 | 每 x 秒一份全量订单簿 |
trades | 实时成交明细 | 成交价格、数量、方向等 |
klines.1m | 每分钟 K 线数据 | 开高低收、volume 等 |
klines.5m | 每 5 分钟 K 线数据 | 同上 |
mark_price | 标记价格推送 | 杠杆合约/永续合约专用 |
liquidations | 爆仓单信息 | 强平记录,风险管理用 |
funding_rates | 资金费率更新 | 合约策略常用 |
system.heartbeat | 心跳或延迟监控等 | 用于系统健康监控 |
✅ 数量控制建议
系统规模 | 推荐 topic 数量 | 划分维度说明 |
---|---|---|
中小型交易所 | 5~10 个 | ticker, depth, trade, kline 等 |
大型交易所 | 10~20 个(含备份、内部用途) | 可能为不同市场(现货、合约)分 topic |
这样下游可以按需订阅不同数据维度,节省流量和内存。
现代交易所主流做法是:聚合 topic(如
tickers
),消息中附带 symbol 字段,下游按需过滤。这样 Kafka 主题数少、扩展性强,完全支持行情广播和大规模量化系统。
Kafka 消费者组中只有一个消费者,消费有多个 partition 的 topic
如果一个 Kafka topic 有多个分区,而消费者组中只有一个消费者实例,这个消费者会自动消费该 topic 所有分区的消息,并且是并发轮询而不是“一个一个排队”。
✅ 场景说明:
- Kafka topic:
tickers
,有 4 个分区。 - 消费者组:
quant-group
,只有一个消费者(consumer-1
)。
➤ Kafka 会自动把所有 4 个分区都分配给 consumer-1
。
然后这个消费者会:
- 并发轮询(poll)多个分区的消息;
- Kafka 客户端内部有 多分区缓冲区 + 自动轮询机制,性能非常高;
- 最终消息处理由代码中线程池或逻辑控制。
条件 | 消费行为说明 |
---|---|
1 个 topic + 多个分区 | Kafka 会把所有分区分配给唯一的消费者 |
消费者组内只有 1 个消费者 | 该消费者将 轮询所有分区并发消费,不会漏消息 |
并不会“等一个分区消费完再消费另一个” | Kafka 是高吞吐流式设计,多个分区的数据会并发调度 |
并发调度 vs 并行调度
在 Kafka 客户端中,“并发调度”并不等于随机调度,也不是并行调度,而是指:
一个线程以轮询方式依次从多个分区中拉取消息,按批次交给业务线程池执行,形成“逻辑上并发”,**物理上依旧是单线程轮询 + 多线程处理”。
🧠 Kafka 消费客户端机制(比如 Java 的 KafkaConsumer
):
- KafkaConsumer 本身 不是多线程安全的,通常只会起一个线程进行轮询(
poll()
)。 - 这个线程会:
- 循环拉取它所分配的所有分区的数据;
- 将消息放入本地缓冲区;
- 然后将这些消息交由用户代码处理(通常是线程池异步处理);
- 处理过程不是“并行拉取”,而是逐个分区进行 poll 拉取,但由于消息处理逻辑可以异步,所以表现为“并发”。
📌 举个例子:
有 1 个消费者,被分配了 4 个分区(P0~P3
):
- KafkaConsumer 会轮询:
poll(P0) → poll(P1) → poll(P2) → poll(P3) → repeat...
- 每次 poll 会拉到一批消息(比如 100 条),可能会:
for (ConsumerRecord record : records) { threadPool.submit(() -> process(record)); }
- 这样就形成了 拉取是串行,处理是并发 的效果。
✅ 因此:
概念 | 在 Kafka 中表现 |
---|---|
并发调度(逻辑并发) | 单线程轮询多分区 + 异步处理 |
并行调度(物理并行) | 多个消费者实例消费不同分区 |
随机调度 | ❌不存在。Kafka 分区分配是有规则的(按轮询或 sticky) |