初始提交:高股息策略研究与回测系统

从 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。
This commit is contained in:
2026-10-03 13:54:56 +08:00
commit fce725e13c
127 changed files with 28001 additions and 0 deletions
+109
View File
@@ -0,0 +1,109 @@
#!/usr/bin/env bash
# ============================================================
# 后端服务管理脚本(macOS / Linux 通用)
#
# 用法:
# ./deploy/serve.sh start 启动(默认 127.0.0.1:8099,仅 API 模式)
# ./deploy/serve.sh start-dev 启动开发模式(API + 静态,可直接浏览器预览)
# ./deploy/serve.sh stop 停止
# ./deploy/serve.sh restart 重启
# ./deploy/serve.sh status 查看状态
# ./deploy/serve.sh log 跟踪日志
#
# 说明:生产环境建议用 systemd / launchd 托管;本脚本用于快速启停与排障。
# ============================================================
set -euo pipefail
PROJECT_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
cd "$PROJECT_ROOT"
PY="${PROJECT_ROOT}/.venv/bin/python"
PORT="${HDDIV_PORT:-8099}"
HOST="${HDDIV_HOST:-127.0.0.1}"
MODE="${HDDIV_MODE:-api-only}" # api-only | full
LOG_DIR="${PROJECT_ROOT}/logs"
PID_FILE="${LOG_DIR}/web.pid"
MODE_FILE="${LOG_DIR}/web.mode"
LOG_FILE="${LOG_DIR}/web.log"
mkdir -p "$LOG_DIR"
is_running() {
[[ -f "$PID_FILE" ]] && kill -0 "$(cat "$PID_FILE")" 2>/dev/null
}
start() {
if is_running; then
echo "已在运行(PID $(cat "$PID_FILE"))"
return 0
fi
if [[ ! -x "$PY" ]]; then
echo "找不到虚拟环境:$PY" >&2
echo "请先执行:uv venv --python 3.12 .venv && uv pip install --python .venv/bin/python pandas numpy sqlalchemy pymysql pydantic pyyaml jinja2 httpx python-dotenv" >&2
exit 1
fi
local args=(--host "$HOST" --port "$PORT")
[[ "$MODE" == "api-only" ]] && args+=(--api-only)
PYTHONPATH=src nohup "$PY" -m hdiv web "${args[@]}" >> "$LOG_FILE" 2>&1 &
echo $! > "$PID_FILE"
echo "$MODE" > "$MODE_FILE"
sleep 2
if is_running; then
echo "已启动(PID $(cat "$PID_FILE")) 模式=${MODE} 监听 http://${HOST}:${PORT}"
echo "日志:$LOG_FILE"
if [[ "$MODE" == "api-only" ]]; then
echo "注意:api-only 模式下静态文件需由 nginx 提供;本机预览请用 start-dev"
else
echo "本机预览:http://${HOST}:${PORT}/"
fi
else
echo "启动失败,日志尾部:" >&2
tail -20 "$LOG_FILE" >&2
exit 1
fi
}
stop() {
SAVED_MODE="$(cat "$MODE_FILE" 2>/dev/null || echo unknown)"
if ! is_running; then
echo "未在运行"
rm -f "$PID_FILE" "$MODE_FILE"
return 0
fi
local pid; pid="$(cat "$PID_FILE")"
kill "$pid" 2>/dev/null || true
for _ in $(seq 1 20); do
kill -0 "$pid" 2>/dev/null || break
sleep 0.2
done
kill -9 "$pid" 2>/dev/null || true
rm -f "$PID_FILE" "$MODE_FILE"
echo "已停止(PID ${pid})"
}
status() {
SAVED_MODE="$(cat "$MODE_FILE" 2>/dev/null || echo unknown)"
if is_running; then
local pid; pid="$(cat "$PID_FILE")"
echo "运行中 PID=${pid} 模式=${SAVED_MODE} 监听 http://${HOST}:${PORT}"
echo -n "健康检查:"
curl -s --max-time 3 "http://${HOST}:${PORT}/api/health" || echo "(无响应)"
echo
else
echo "未在运行"
fi
}
case "${1:-}" in
start) MODE=api-only start ;;
start-dev) MODE=full start ;;
stop) stop ;;
restart) SAVED_MODE="$(cat "$MODE_FILE" 2>/dev/null || echo api-only)"
stop; MODE="${SAVED_MODE}" start ;;
status) status ;;
log) tail -f "$LOG_FILE" ;;
*)
sed -n '2,15p' "${BASH_SOURCE[0]}" | sed 's/^# \{0,1\}//'
exit 2 ;;
esac