Files
ggx/deploy/install-service.sh
T
simon fce725e13c 初始提交:高股息策略研究与回测系统
从 Point-in-Time 股票筛选到统一 Web 前端的完整链路:
筛选 → 画像 → 策略 → 回测 → Walk-forward → 绩效分析 → 报告/前端。

架构
- 数据层与策略层分离;策略代码不写 SQL,只经 data/repo.py 取数
- 所有业务阈值集中在 config/*.yml,代码零硬编码(字段写错直接报错)
- 报告只做「run_id → SQL → 渲染」,不做任何计算,数字可追溯
- 前后端分离:output/ 静态站点 + hdiv web 提供的 REST API

数据安全
- 只增不删:SQL 钩子拦截 DELETE/DROP/TRUNCATE,并有源码扫描测试守护
- qlib 原有表只读,本项目数据写入 hd_ 前缀表
- 回补使用 INSERT IGNORE,保证既有行零改动
- .env 存密钥且已 gitignore;output/、logs/、.venv/ 不入库

交付物
- 30 张 hd_* 表、7 个 YAML 配置、283 项自动化测试
- 统一 Web 前端(hash 路由 SPA)+ nginx 部署配置与 launchd 托管脚本

如实声明的限制
- 策略缺少稳定的样本外超额收益(Walk-forward 7 窗口均值 -0.95%,
  基准 +2.29%);其价值体现在回撤控制,而非超额收益
- 涨跌停/停牌约束仅覆盖 2019 年起;index_weight 尚未填充
- AI Agent 层(plan.md 第四版 P8)未实现

详见 docs/user-guide.md 与 docs/implementation-status.md。
2026-10-03 13:54:56 +08:00

96 lines
3.5 KiB
Bash
Executable File
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env bash
# ============================================================
# 把后端 API 服务注册为 macOS launchd 用户级服务(登录自启 + 崩溃自愈)
#
# 用法:
# ./deploy/install-service.sh install 安装并启动(幂等,可重复执行)
# ./deploy/install-service.sh uninstall 停止并移除
# ./deploy/install-service.sh status 查看状态与连通性
# ./deploy/install-service.sh reinstall 重新渲染 plist 并重启
#
# 为什么需要:nginx 由 brew services 托管、开机自启。若后端只是 nohup 进程,
# 机器重启后它就不在了 —— 页面能打开但会显示「API 不可用」。
#
# 若你不用 launchd,也可用 ./deploy/serve.sh start 手动启动(重启后需再次执行)。
# ============================================================
set -euo pipefail
PROJECT_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
TEMPLATE="${PROJECT_ROOT}/deploy/com.hddiv.web.plist.example"
TARGET="${HOME}/Library/LaunchAgents/com.hddiv.web.plist"
LABEL="com.hddiv.web"
PYTHON="${PROJECT_ROOT}/.venv/bin/python"
PORT="${HDDIV_PORT:-8099}"
HOST="${HDDIV_HOST:-127.0.0.1}"
die() { echo "错误: $*" >&2; exit 1; }
is_loaded() { launchctl list 2>/dev/null | grep -q "^[0-9-]*[[:space:]]*[0-9-]*[[:space:]]*${LABEL}$"; }
render() {
[[ -f "${TEMPLATE}" ]] || die "缺少模板:${TEMPLATE}"
[[ -x "${PYTHON}" ]] || die "虚拟环境不可用:${PYTHON}\n请先创建 .venv 并安装依赖"
mkdir -p "${PROJECT_ROOT}/logs" "${HOME}/Library/LaunchAgents"
# 路径含中文与空格,用 sed 的 | 分隔符避免转义问题
sed -e "s|__PYTHON__|${PYTHON}|g" \
-e "s|__PROJECT_ROOT__|${PROJECT_ROOT}|g" \
"${TEMPLATE}" > "${TARGET}"
# 占位符必须全部被替换,否则 launchd 会静默失败
if grep -q "__" "${TARGET}"; then
die "plist 中仍有未替换的占位符:$(grep -o '__[A-Z_]*__' "${TARGET}" | sort -u | tr '\n' ' ')"
fi
plutil -lint "${TARGET}" >/dev/null || die "plist 格式非法:${TARGET}"
echo "已渲染 ${TARGET}"
}
cmd_install() {
render
if is_loaded; then
echo "服务已加载,重新加载以应用新配置…"
launchctl unload -w "${TARGET}" 2>/dev/null || true
fi
launchctl load -w "${TARGET}"
sleep 3
cmd_status
}
cmd_uninstall() {
if [[ -f "${TARGET}" ]]; then
launchctl unload -w "${TARGET}" 2>/dev/null || true
rm -f "${TARGET}"
echo "已停止并移除 ${TARGET}"
else
echo "${TARGET} 不存在,跳过"
fi
}
cmd_status() {
echo "=== launchd 服务 ==="
if is_loaded; then
launchctl list | grep "${LABEL}" | awk '{printf " PID=%s 上次退出码=%s %s\n", $1, $2, $3}'
echo " (PID 为 - 表示当前未运行)"
else
echo " 未加载"
fi
echo "=== 后端直连 ==="
if out="$(curl -sS --max-time 3 "http://${HOST}:${PORT}/api/health" 2>&1)"; then
echo " http://${HOST}:${PORT}/api/health -> ${out}"
else
echo " 无响应:${out}"
fi
echo "=== 日志 ==="
echo " ${PROJECT_ROOT}/logs/web.log"
echo " ${PROJECT_ROOT}/logs/web.err.log"
[[ -f "${PROJECT_ROOT}/logs/web.err.log" ]] && \
tail -3 "${PROJECT_ROOT}/logs/web.err.log" 2>/dev/null | sed 's/^/ /' || true
}
case "${1:-install}" in
install) cmd_install ;;
reinstall) render; launchctl unload -w "${TARGET}" 2>/dev/null || true
launchctl load -w "${TARGET}"; sleep 3; cmd_status ;;
uninstall) cmd_uninstall ;;
status) cmd_status ;;
*) die "未知动作:$1(可用:install|reinstall|uninstall|status)" ;;
esac