This article mainly explains how separating data-request and compute nodes, mounting NAS, and expanding swap space make it possible to shut down expensive compute nodes at any time, avoid conflicts when fetching data from thetadata, share downloaded data and execution results, and save memory, ultimately reducing budget waste during quantitative-parameter research.

Background

Quantitative trading strategies must be effective, and their parameters must also be mathematically proven effective through backtests over a recent period.

The QuantAda quantitative trading framework uses the optuna framework for parameter exploration. Although my laptop has a 16C, 32G configuration and is more than capable of running daily stock backtests, it struggles with options-strategy research. In particular, when running at the default 85% performance, the fan noise is very loud at night.

Therefore, for options-strategy parameter exploration, I decided to rent a preemptible instance in Virginia, USA. After running it in practice, however, I found that all cores were not fully utilized for much of the time, a single thetadata account could have only one session, and 64G of memory was insufficient.

Server Selection

Node

Because thetadata’s main server is located near Virginia, and cloud-server hourly rates in Virginia are considerably lower than those in Silicon Valley, I chose that region.

Configuration

The quantitative framework’s –n_jobs parameter specifies how many cores to use. In theory, more cores mean shorter execution times. However, cloud providers charge a premium for higher core counts, so a 32C instance is just right. The price difference between 32G and 64G memory instances is also small, so a 32C64G preemptible instance with a 128G disk is sufficient.

Price List

Item CPU MEM Price
Virginia preemptible instance 32C 64G ¥1.502568/hour
2C 2G ¥0.1062/hour
NAS capacity tier ¥0.001/GB/hour

Splitting

Because quantitative-parameter training and exploration must fetch data before execution, the 32C64G instance keeps incurring charges while fetching thetadata’s extremely slow data source.

Therefore, create a separate 2C2G instance and use the framework’s –fetch_only parameter. The same training command will then fetch data on the 2C2G instance without actually running training. Once the 2C2G instance finishes downloading, you can create a 32C64G preemptible, pay-as-you-go instance whenever needed, read the cached downloaded data, and start training immediately.

image-20261022152533741
image-20261022152533741

NAS

Shared Data

During routine deployment, training, and data downloads, I previously had to repeatedly configure the CI target, manually create virtual environments, install dependencies, clean up logs, and so on.

NAS costs less than the disks or network drives of cloud-server instances, so it can be used to store the shared environment and cached downloaded data for reuse throughout the server’s deployment region.

Mount the NAS instance you created at /nas on both servers, and point the CI target to the /nas directory on 2C2G as well. On either machine, create a .venv virtual environment for the project under /nas and install the dependencies, then use the script below to create a symbolic link for the setup to take effect.

Shared Scripts

Environment Configuration

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

Usage

. env.sh

SWAP Virtual Memory

Because memory prices have surged recently, simply expand SWAP to match the amount of RAM.

Of course, you can first run a small number of jobs and estimate the required size from the memory consumed by each job.

Usage (see the appendix at the end for the detailed script logic)

./create_swap.sh 32G

Execution Sequence

  • 2C2G deploys the project, creates the environment, installs dependencies, and sources env.sh
  • 2C2G mounts NAS and uses –fetch_only to download data
  • Once the data is ready, create a 32C64G instance and create swap virtual memory
  • 32C64G mounts NAS and sources env.sh, sets –n_jobs to 31 to create 31 jobs, runs training at full speed, and stores log files under /nas
  • 2C2G starts downloading new backtest data for training
  • When the previous 32C64G training round finishes, start immediately if the new data is ready; otherwise, destroy the instance and wait for the download to finish
  • 2C2G collects all training-result log files under /nas

Alternatively, you can turn the 2C2G instance into an image and release both instances. This saves money while allowing the download and training instances to start quickly.

Appendix: Creating the swap Script

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