README 里原先写「执行器收到 SIGINT 会先平掉在场仓位再退出」,但代码里 完全没有信号处理——asyncio.run 外面连 except KeyboardInterrupt 都没有。 断言了一个不存在的行为,现在把它实现。 为什么停机要平仓而崩溃不用:崩溃后 systemd/docker 几秒内重启,reconcile 接着清掉遗留仓位,空窗期有交易所侧止损兜着。而主动停机后没人重启,仓位会 一直挂到止损或止盈,48 分钟超时腿丢了,跑的就不是回测那个出场结构。 必须显式挂 SIGTERM:docker stop 与 systemd stop 默认发的都是它,而 Python 对 SIGTERM 不抛 KeyboardInterrupt,不挂就是直接消失、没有任何清理。挂上后 systemd 单元里 KillSignal=SIGINT 那个绕法也去掉了。 主循环因异常退出时同样走停机流程,不把仓位留给已经没人管的进程。 顺带修掉 "Unclosed client session"(bitget_rest 本有 close() 但没人调, Restart=always 下会漏 socket)。 实测:发 SIGTERM 后走完停机流程、无泄漏警告。 Co-authored-by: Cursor <cursoragent@cursor.com>
50 lines
1.9 KiB
Desktop File
50 lines
1.9 KiB
Desktop File
[Unit]
|
||
Description=chan 小额实盘执行器(读信号总线,直接调 Bitget REST)
|
||
# 搬运挂了执行器仍要活着——它得继续管在场仓位的 48 分钟超时平仓。
|
||
# 所以这里只写 Wants(弱依赖),不写 Requires
|
||
Wants=network-online.target chan-live-ship.service
|
||
After=network-online.target chan-live-ship.service
|
||
# 频繁重启说明有真问题,别让它无限打交易所。5 分钟内起 5 次就停下等人。
|
||
# 注意这两项在 systemd 229+ 属于 [Unit],写在 [Service] 里会被忽略
|
||
StartLimitIntervalSec=300
|
||
StartLimitBurst=5
|
||
|
||
[Service]
|
||
Type=simple
|
||
User=chan
|
||
Group=chan
|
||
WorkingDirectory=/opt/chan
|
||
# 密钥与参数在这个文件里,权限必须 600。用 EnvironmentFile 而不是
|
||
# Environment=,后者会出现在 `systemctl show` 的输出里
|
||
EnvironmentFile=/etc/chan-live/live.env
|
||
ExecStart=/opt/chan/.venv/bin/python /opt/chan/live/live_exec.py
|
||
Restart=always
|
||
RestartSec=5
|
||
|
||
# 收到 stop 时给足时间:执行器接到 SIGTERM 会撤挂单 + 平掉在场仓位再退出
|
||
# (live_exec.shutdown(),内部平仓上限 60s)。默认的 90s 停机窗口留了余量。
|
||
# 不需要 KillSignal=SIGINT——SIGTERM 已在代码里显式挂了处理器
|
||
TimeoutStopSec=90
|
||
|
||
StandardOutput=journal
|
||
StandardError=journal
|
||
SyslogIdentifier=chan-live-exec
|
||
|
||
# ── 收紧权限 ──────────────────────────────────────────────────────
|
||
# 生产进程只需要读 /opt/chan 和读写状态目录,别的一概不给。这几条很廉价,
|
||
# 但真出了远程代码执行,爆炸半径小很多——而这个进程手里有交易权限的密钥
|
||
NoNewPrivileges=true
|
||
PrivateTmp=true
|
||
ProtectSystem=strict
|
||
ProtectHome=true
|
||
ReadWritePaths=/var/lib/chan-live
|
||
ProtectKernelTunables=true
|
||
ProtectKernelModules=true
|
||
ProtectControlGroups=true
|
||
RestrictSUIDSGID=true
|
||
LockPersonality=true
|
||
MemoryMax=512M
|
||
|
||
[Install]
|
||
WantedBy=multi-user.target
|