本文主要介绍了通过拆分数据请求和计算节点、挂载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
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
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
|