0、前言
好板对速度要求极高,普通人比不过大户的专用通道,只能在有限的资源条件下尽量提升速度,0.1秒可能都排在几万手后面去了。
如果要监控大量的股票标的,采用 go 协程性能会更好,可以看我另一篇性能对比测试:
JDK21虚拟线程、OS线程、GO协程性能对比测试
一、云服务器硬件层面
跑程序可以用云服务器或者个人电脑,不过个人电脑的后台程序太多了,占用CPU资源,除非电脑性能足够好,否则还是推荐使用云服务器。
如我在A股微型低频套利交易-Java版本结尾提到的一样:服务器逻辑处理单元数量 > 监控标的数量那就是最好的。
对CPU线程数量的要求取决于个人的策略,比如我最近帮朋友开发的这个策略,每天监控的标的最多不超过七八个,所以选择了8C密集计算型 独占式的服务器,服务器地址要么选上海,要么选深圳。
千万别图便宜买到了共享版本的云服务器(比独占式的便宜一倍),那个性能太差了。咨询客服买性能最好的即可
[root@iZwz9gmcys6qhe2mtmig34Z ~]# lscpu
Architecture: x86_64
CPU op-mode(s): 32-bit, 64-bit
Byte Order: Little Endian
CPU(s): 8
On-line CPU(s) list: 0-7
Thread(s) per core: 2
Core(s) per socket: 4
Socket(s): 1
NUMA node(s): 1
Vendor ID: GenuineIntel
CPU family: 6
Model: 85
Model name: Intel(R) Xeon(R) Platinum 8269CY CPU @ 2.50GHz
Stepping: 7
CPU MHz: 2500.002
BogoMIPS: 5000.00
Hypervisor vendor: KVM
Virtualization type: full
L1d cache: 32K
L1i cache: 32K
L2 cache: 1024K
L3 cache: 36608K
NUMA node0 CPU(s): 0-7
4个核心,每个核心支持2个线程,所以总共有 8个逻辑处理单元(也可以理解为8个CPU)。监控每天的七八个标的足够了。
二、软件层面
线程池要隔离,一个线程池-MOMENTUM_POOL用来盯每个标的,一个线程池-OTHER_POOL用来处理其他的低频的工作。
盯盘线程池
public static ThreadPoolExecutor MOMENTUM_POOL = new ThreadPoolExecutor(
10 // corePoolSize: 核心池大小,即即使线程空闲,也会保持的线程数。
10, // maximumPoolSize: 线程池允许的最大线程数。
100L, // keepAliveTime: 允许线程池中空闲的线程最大空闲时间。
TimeUnit.SECONDS, // timeUnit: keepAliveTime 的时间单位,这里是秒。
new SynchronousQueue<>(), // workQueue: 任务队列,SynchronousQueue 是一个特殊的队列,它不存储任务,任务会直接被分配到线程上。
new NamedThreadFactory("MX-", false) // threadFactory: 用于创建线程的工厂,NamedThreadFactory 会创建名称以 "MX-" 为前缀的线程(并且不是守护线程)。
);
由于某些策略可能9:30:00第一档成交数据跳出来就会触发买入,所以开盘前就必须创建好核心线程,省去9:30:00创建线程的时间。
MOMENTUM_POOL.prestartAllCoreThreads();
9:29:55秒拆分策略,按竞价结果把标的拆分到对应的策略缓存里
更新账户可用金额,不能等到下单时才去获取可用金额,节省时间,并且在OTHER_POOL里面新开一个线程每分钟更新一下可用金额。
、、、
private static final AtomicBoolean buy = new AtomicBoolean(false);//当日是否已经下过单
private AtomicInteger availableAmount = new AtomicInteger(0);//可用余额
、、、
@Scheduled(cron = "55 29 9 * * ?")
public void callAuction() {
if (!todayTrade.get()) {
return;
}
AccountVo accountVo = tradeApiService.queryAccount();
availableAmount.set(accountVo.getAvailableAmount().intValue());
log.info("【竞价结束】当前账户可用资金: {},{}", availableAmount.intValue(), JSONUtil.toJsonStr(accountVo));
、、、
、、、拆分策略
、、、
log.info("【竞价结束】策略1股票池:{}",
JSONUtil.toJsonStr(POLICY_1.values().stream().map(PolicyQue::getName).collect(Collectors.toList())));
log.info("【竞价结束】策略2股票池:{}",
JSONUtil.toJsonStr(POLICY_2.values().stream().map(PolicyQue::getName).collect(Collectors.toList())));
log.info("【竞价结束】策略3股票池:{}",
JSONUtil.toJsonStr(POLICY_3.values().stream().map(PolicyQue::getName).collect(Collectors.toList())));
log.info("【竞价结束】策略4股票池:{}",
JSONUtil.toJsonStr(POLICY_4.values().stream().map(PolicyQue::getName).collect(Collectors.toList())));
log.info("【竞价结束】策略5股票池:{}",
JSONUtil.toJsonStr(POLICY_5.values().stream().map(PolicyQue::getName).collect(Collectors.toList())));
log.info("【竞价结束】反向指标股票池:{}",
JSONUtil.toJsonStr(POLICY_CANCEL.stream().map(PolicyQue::getName).collect(Collectors.toList())));
}
开盘时把每个标的都丢到盯盘线程池里
@Scheduled(cron = "0 30 9 * * ?")
public void start() {
if (todayTrade.get()) {
log.info("【开盘->开启扫板策略监控】");
for (PolicyQue policyQue : POLICY_3.values()) {
MOMENTUM_POOL.submit(() -> policy3(policyQue));
}
for (PolicyQue policyQue : POLICY_2.values()) {
MOMENTUM_POOL.submit(() -> policy2(policyQue));
}
for (PolicyQue policyQue : POLICY_1.values()) {
MOMENTUM_POOL.submit(() -> policy1(policyQue));
}
OTHER_POOL.submit(this::updateAvailableAmount);
if (CollectionUtils.isNotEmpty(POLICY_CANCEL)) {
MOMENTUM_POOL.submit(this::policyCancel);
}
MOMENTUM_POOL.submit(this::policyIdle);
}
}
拿其中一个最简单的策略举例
public void policy4(PolicyQue policyQue) {
try {
while (true) {
if (DateUtil.hour(new Date(), true) >= 10) {
log.info("【POLICY_4】10点还未触发,取消 {} 监控策略4", policyQue.getName());
break;
}
Pankou pankou = stockProcessor.getPankou(policyQue.getCode());//获取盘口数据
if (pankou.getCurrentPrice().doubleValue() == policyQue.getLimitUp().doubleValue() && pankou.getSale1() == 0) {
order("【POLICY_4】完全上板", policyQue, pankou.getCurrentPrice(),
AutomaticTradingEnum.POLICY_QUE_4, null, new Date(), pankou);//下单方法
break;
}
、、、
、、、
、、、
//Thread.sleep(1);//追求速度,这行都直接注了
}
Thread.sleep(1);
} catch (InterruptedException interruptedException) {
log.info("【中断监控】取消策略4监控策略 {}", policyQue.getName());
} catch (Exception e) {
e.printStackTrace();
}
}
由于这个策略每天只会下单一次,在下单接口必须加锁+AtomicBoolean控制
private synchronized boolean buyOrder(Double currentPrice, String code, AutomaticTradingEnum automaticTradingEnum) {
if (buy.get()) {
throw new RuntimeException("【下单】今日已经下过单了,不再买入,中断策略");
}
、、、
、、、
、、、
}
这是2025-01-17的实盘数据
触发策略的历史成交数据时间戳 “timestamp”:1737077409000 = “2025-01-17 09:30:09”
2025-01-17 09:30:09.911 触发策略,下单
2025-01-17 09:30:10.013 交易所返回下单结果
基本上能1秒内触发+下单成功
最终成交时间大概是13:11分
当下单成功即可中断线程池内的其他所有盯盘线程
MOMENTUM_POOL.shutdownNow();
收盘后清除当日所有缓存,重置配置等等
@Scheduled(cron = "0 3 15 * * ?")
public void reset() {
todayTrade.set(false);
POLICY_5.clear();
POLICY_4.clear();
POLICY_3.clear();
POLICY_2.clear();
POLICY_1.clear();
STOCK_MAP.clear();
POLICY_IDLE.clear();
POLICY_CANCEL.clear();
policyQueMapper.delete(new LambdaQueryWrapper<>());
}