本文主要介绍了通过拆分数据请求和计算节点、挂载NAS、扩充swap空间,从而随时关闭高费用计算节点、避免thetadata取数据冲突、共享数据下载和执行结果以及节约内存等情况,最后节约量化参数研究过程中的预算损耗。

背景

量化交易策略不仅需要策略有效,而且其参数也需要对近期一段时间的回测在数学层面上证明有效。

QuantAda量化交易框架使用optuna框架进行参数挖掘,虽然本人的笔记本电脑是16C、32G配置,跑股票日线回测也绰绰有余,但对于期权策略的研究就捉襟见肘。尤其是以默认85%性能运行时,半夜的风扇声也很吵闹。

因此,对于期权策略的参数挖掘,决定租用美国弗吉尼亚节点的抢占式实例。但实际运行后,发现大量时间没有用满全部核心、thetadata单账户只能存在一个会话、64G内存不够用等情况。

服务器选型

节点

由于thetadata主服务器位于美国弗吉尼亚附近,另外弗吉尼亚节点的云服务器小时费用比硅谷节点的便宜不少,因此选中了该区域。

配置

量化框架的–n_jobs参数能够指定使用多少核心,理论上核心数量越多执行时间越短。但由于云服务器厂商的报价对于更多核数有溢价,因此32C的实例刚刚好。此外,32G内存和64G内存的实例价格差别较小,直接购买32C64G配置、128G磁盘的抢占式实例即可。

价格单

项目 CPU MEM 价格
弗吉尼亚 抢占式实例 32C 64G ¥1.502568/时
2C 2G ¥0.1062/时
NAS 容量型 ¥0.001元/GB/小时

拆分

由于量化参数训练挖掘需要先获取数据再执行,当获取thetadata超慢数据源时,32C64G的账单也在默默消耗着。

因此再创建一个2C2G的实例,再使用框架提供–fetch_only参数,这样相同的训练命令只会在2C2G的实例拉数据但不实际执行训练。当2C2G实例下载完毕后,即可随时创建32C64G抢占式按量付费实例,读取缓存的已下载数据直接开启训练。

image-20261022152533741
image-20261022152533741

NAS

共用数据

在日常部署、训练和数据下载时,之前总要进行不断配置CI目标、手动创建虚拟环境、安装依赖、回收日志等。

而NAS的费用相比于云服务器实例的磁盘或网盘的价格更低,因此直接使用NAS来存放公共环境和缓存下载数据即可该服务器部署区域内全局复用。

将创建的NAS实例挂载到两台服务器上的 /nas 目录下即可,CI目标也改为2C2G的 /nas目录。在任意一台机器对/nas目录下的项目创建.venv虚拟环境并安装依赖,然后通过下面的脚本创建软链接即可生效。

共用脚本

环境配置

env.sh

1
2
3
4
5
export PYTHONPATH=/root/QuantAda:/root/QuantCasper
export THETADATA_API_KEY='XXX'
ln -sfn /nas/QuantAda /root/QuantAda
ln -sfn /nas/QuantCasper /root/QuantCasper
cp /nas/.futursa /root/.futursa

使用方法

. env.sh

SWAP虚拟内存

由于最近内存价格暴涨,因此直接将SWAP扩展为内存相同即可。

当然,具体大小可以先执行少量job,然后从每个job消耗的内存可估算出。

使用方法(具体脚本逻辑请见文末附录)

./create_swap.sh 32G

执行时序

  • 2C2G 部署项目、创建环境、安装依赖、生效env.sh
  • 2C2G 挂载NAS,使用–fetch_only下载数据
  • 32C64G 数据就绪后,创建实例,并创建swap虚拟内存
  • 32C64G 挂载NAS并生效env.sh,将–n_jobs设定为31,从而创建31个job,让其全速训练,并将日志文件存放于/nas目录下
  • 2C2G 开始下载新的待训练回测数据
  • 32C64G 上一轮训练完毕,如果新数据下载完毕则直接开始,如果没有下载完毕可以销毁后等待其下载完毕
  • 2C2G 回收/nas目录下所有训练结果日志

当然也可以将2C2G做成镜像,把两个实例都释放掉。这样能既节约成本,又能快速启动下载和训练实例。

附:创建swap脚本

create_swap.sh

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
#!/bin/bash
# 按 G 创建或调整 /swapfile,并保证重启后仍会启用。
# 用法: /nas/create_swap.sh <大小G> [swappiness]
# 示例: /nas/create_swap.sh 64
# 示例: /nas/create_swap.sh 64G 10
set -euo pipefail

usage() {
echo "用法: $0 <大小G> [swappiness]" >&2
echo "大小只接受 G,例如 64 或 64G。swappiness 可选,范围 0-100,默认 10。" >&2
}

if [[ ${EUID:-$(id -u)} -ne 0 ]]; then
echo "请用 root 运行" >&2
exit 1
fi

if [[ $# -lt 1 || $# -gt 2 ]]; then
usage
exit 1
fi

size_arg=${1%[Gg]}
if [[ ! $size_arg =~ ^[0-9]+$ || $size_arg -lt 1 ]]; then
echo "大小必须是正整数,单位为 G: $1" >&2
usage
exit 1
fi

swappiness=${2:-10}
if [[ ! $swappiness =~ ^[0-9]+$ || $swappiness -gt 100 ]]; then
echo "swappiness 必须是 0 到 100 的整数: ${2:-}" >&2
exit 1
fi

exec 9>/var/lock/create_swap.lock
if ! flock -n 9; then
echo "已有 swap 创建任务在运行" >&2
exit 1
fi

swapfile=/swapfile
size_bytes=$((size_arg * 1024 * 1024 * 1024))
current_bytes=0
if [[ -f $swapfile ]]; then
current_bytes=$(stat -c %s "$swapfile")
fi

active=0
if swapon --show=NAME --noheadings 2>/dev/null | grep -qx "$swapfile"; then
active=1
fi

swap_used_kb=$(awk '/^\/swapfile / {print $4}' /proc/swaps 2>/dev/null || true)
swap_used_kb=${swap_used_kb:-0}
if [[ $current_bytes -ne $size_bytes && $swap_used_kb -gt 0 ]]; then
echo "/swapfile 仍有 ${swap_used_kb}KB 在使用,先不要换大小,避免 swapoff 把内存打满。" >&2
exit 1
fi

avail_kb=$(df -Pk / | awk 'NR==2 {print $4}')
need_kb=$((size_bytes / 1024))
free_after_kb=$((avail_kb + current_bytes / 1024))
if [[ $need_kb -gt $free_after_kb ]]; then
echo "磁盘空间不足: 需要 ${size_arg}G,删除旧文件后大约只剩 $((free_after_kb / 1024 / 1024))G" >&2
exit 1
fi

write_zero_file() {
dd if=/dev/zero of="$swapfile" bs=1M count=$((size_arg * 1024)) status=progress conv=fsync
}

if [[ $current_bytes -eq $size_bytes && $active -eq 1 ]]; then
echo "/swapfile 已是 ${size_arg}G 且已启用"
else
if [[ $active -eq 1 ]]; then
swapoff "$swapfile"
fi
rm -f "$swapfile"
if ! fallocate -l "${size_arg}G" "$swapfile"; then
write_zero_file
fi
chmod 600 "$swapfile"
mkswap "$swapfile" >/dev/null
if ! swapon "$swapfile"; then
echo "fallocate 文件不能作为 swap,改用 dd 重做 ${size_arg}G"
swapoff "$swapfile" 2>/dev/null || true
rm -f "$swapfile"
write_zero_file
chmod 600 "$swapfile"
mkswap "$swapfile" >/dev/null
swapon "$swapfile"
fi
fi

if ! grep -qE '^[[:space:]]*/swapfile[[:space:]]' /etc/fstab; then
echo "/swapfile none swap sw 0 0" >> /etc/fstab
fi

apsara=/etc/sysctl.d/99-apsara-sysctl.conf
if [[ -f $apsara ]] && grep -q '^[[:space:]]*vm.swappiness[[:space:]]*=' "$apsara"; then
sed -i "s/^[[:space:]]*vm.swappiness[[:space:]]*=.*/vm.swappiness = ${swappiness}/" "$apsara"
fi
echo "vm.swappiness = ${swappiness}" > /etc/sysctl.d/zz-swappiness.conf
sysctl -w "vm.swappiness=${swappiness}" >/dev/null

echo "完成: swap=${size_arg}G swappiness=${swappiness}"
swapon --show
free -h