汇总三轮未提交的开发(每轮均在本机 MariaDB + 真实浏览器上验证):
1) 股息率案例(全市场股息率最高 n 只,默认 20,每 m 月择股)
- 新增日频估值表 daily_basic + 迁移;股息率因子(dv_ratio / dividend_yield / TTM)
- 名称历史表 stock_name_history:剔除 ST 按**择股日当时名称**判定,消除
「曾高股息后 ST」的股息陷阱(实测 3.70pp 偏差)
- 区间择股/调仓双周期(m 择股 / y 调仓)、指数成分与白名单、停牌近似剔除
- 复权因子口径核对(4,164,742 行、缺失 0.0%)、收盘价成交与涨跌停拦单
- 案例实测:2020-01-01~2026-09-04 总收益 +24.86%(年化 3.52%、回撤 -28.58%)
2) 策略库与前端统一
- strategy 表 + CRUD/PUT 原地更新 + `describe_strategy` 按 spec 真实推导
「一句话说明 + 计算公式 + 执行步骤 + 注意事项」(与引擎实执行规则同源)
- 任何出现股票代码处都成对显示名称且可点击进个股页
- 全站图表基座统一 TradingView Lightweight Charts(ECharts 依赖、
锁文件、组件与文档标注一并清除),买卖点标记只落在真实交易日上
3) 回测存档完整化(可往复查看)
- 同步端点(POST /api/backtests、/api/factor-tests)此前完全不落库 → 现在同样归档,
归档 id 经响应头 X-Experiment-Id 返回(不破坏 response_model)
- data_version 首次真实写入(数据快照指纹:最新交易日 + 各表规模)
- 个股收益曲线默认**全量保存**(此前硬截断 60 只);超出体积预算才裁剪,
并写 archive_meta(机器可读)+ unimplemented(人可读)如实标注
- 列表 kind/q 过滤 + X-Total-Count(此前 limit=50 静默截断)、DELETE 归档
- 只读归档页 /experiments/{id}(Server Component,SSR 直出**选股条件**与
**交易执行依据**);结果视图按 kind 分发(backtest/factor_test/selection),
非回测归档不套用回测口径
- 新增 CLI:prune_experiments(保留策略,默认 dry-run)、
restore_experiment_from_job(从 Job 副本按原 id 重建被删的历史归档,默认 dry-run)
门禁:pytest 388 passed、ruff All checks passed、tsc 0 错误、图表单测 7 passed、
next build 成功、契约脚本 verify_strategy_workspace 59/59(含按 kind 逐类验证归档页)。
218 lines
8.2 KiB
Bash
218 lines
8.2 KiB
Bash
#!/usr/bin/env bash
|
||
# ============================================================
|
||
# 本地 MariaDB:恢复 root 访问 + 创建与项目配置一致的 qlib 账号
|
||
#
|
||
# 背景
|
||
# - 本机 MacPorts mariadb-10.11 的 root@localhost 密码未知,
|
||
# 且 unix_socket 认证也不可用(以 _mysql 运行同样被拒),
|
||
# 因此无法创建项目所需的 qlib 账号。
|
||
# - 服务由 launchd 托管(KeepAlive=true),直接 kill 会自动重启。
|
||
#
|
||
# 做法(标准 root 密码找回流程)
|
||
# 1) 停掉 launchd 托管的正式实例(释放 datadir 咨询锁)
|
||
# 2) 以 _mysql 用户启动临时实例:--skip-grant-tables --skip-networking
|
||
# (不监听 TCP,仅本机私密 socket)
|
||
# 3) 在临时实例上重置 root@localhost 密码,并创建 qlib@localhost /
|
||
# qlib@127.0.0.1(密码取自项目根 .env 的 MYSQL_PASSWORD)
|
||
# 4) 关闭临时实例,重新启动 launchd 正式实例
|
||
# 5) 用正式实例验证 qlib 账号 TCP 登录
|
||
#
|
||
# 用法(需要 sudo,会短暂中断本机数据库服务)
|
||
# sudo bash scripts/mysql_recover_and_grant.sh
|
||
#
|
||
# 幂等:可重复执行;若 qlib 账号已可用则直接跳过,不做任何改动。
|
||
# ============================================================
|
||
set -uo pipefail
|
||
|
||
MDB=/opt/local/lib/mariadb-10.11/bin
|
||
DATADIR=/opt/local/var/db/mariadb-10.11
|
||
MYSQLUSER=_mysql
|
||
RUN=/tmp/qlib-mariadb-recover
|
||
TMP_SOCK=${RUN}/recover.sock
|
||
TMP_PID=${RUN}/recover.pid
|
||
MAIN_PID_FILE=${DATADIR}/summer.pid
|
||
MAIN_SOCK=/opt/local/var/run/mariadb-10.11/mysqld.sock
|
||
LAUNCHD_LABEL=system/org.macports.mariadb-10.11-server
|
||
PLIST=/opt/local/etc/LaunchDaemons/org.macports.mariadb-10.11-server/org.macports.mariadb-10.11-server.plist
|
||
SERVICE_STOPPED=0
|
||
# 口令一律不硬编码(AGENT.md §33/§34:密钥只放 .env,禁止提交):
|
||
# RECOVER_PW —— root 复位后的口令:取环境变量 MARIADB_ROOT_PW,未设则随机生成(见下)
|
||
# QLIB_PW —— 业务账号口令:运行时从根目录 .env 的 MYSQL_PASSWORD 读取(见下)
|
||
RECOVER_PW="${MARIADB_ROOT_PW:-}"
|
||
QLIB_PW=""
|
||
|
||
log() { echo "==> $*"; }
|
||
warn() { echo "!!! $*" >&2; }
|
||
|
||
if [ "$(id -u)" -ne 0 ]; then
|
||
warn "需要 root 权限:sudo bash $0"
|
||
exit 1
|
||
fi
|
||
|
||
# ---------- 凭据读取:不落盘、不硬编码 ----------
|
||
ENV_FILE="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)/.env"
|
||
if [ -f "${ENV_FILE}" ]; then
|
||
QLIB_PW="$(sed -n 's/^MYSQL_PASSWORD=//p' "${ENV_FILE}" \
|
||
| head -1 | tr -d '\r' \
|
||
| sed -e 's/^"//' -e 's/"$//' -e "s/^'//" -e "s/'$//")"
|
||
fi
|
||
if [ -z "${QLIB_PW}" ]; then
|
||
warn "未从 ${ENV_FILE} 读到 MYSQL_PASSWORD(见 config.yaml database.mysql.password_env)"
|
||
exit 1
|
||
fi
|
||
# SQL 里口令以单引号字面量拼接:含单引号会破坏语句,直接拒绝而不是生成坏 SQL
|
||
case "${QLIB_PW}" in
|
||
*"'"*)
|
||
warn "MYSQL_PASSWORD 含单引号,本脚本的 SQL 拼接不安全;请用 mysql 客户端手工授权"
|
||
exit 1
|
||
;;
|
||
esac
|
||
|
||
# root 复位口令:不设弱口令,未显式提供则随机生成并由脚本末尾打印一次
|
||
if [ -z "${RECOVER_PW}" ]; then
|
||
RECOVER_PW="$(openssl rand -base64 24 2>/dev/null | tr -dc 'A-Za-z0-9' | cut -c1-24)"
|
||
fi
|
||
if [ -z "${RECOVER_PW}" ]; then
|
||
warn "无法生成 root 口令(openssl 不可用):请显式设置 MARIADB_ROOT_PW 后重试"
|
||
exit 1
|
||
fi
|
||
|
||
# ---------- 步骤 0:若 qlib 账号已可用,直接结束(幂等、零改动) ----------
|
||
if "${MDB}/mariadb" -h 127.0.0.1 -P 3306 -u qlib -p"${QLIB_PW}" \
|
||
-N -e "SELECT COUNT(*) FROM information_schema.tables WHERE table_schema='qlib';" \
|
||
>/tmp/qlib-pregrant.out 2>/dev/null; then
|
||
log "qlib 账号已可登录本机 MariaDB,表数:$(cat /tmp/qlib-pregrant.out)"
|
||
log "无需任何改动,退出。"
|
||
rm -f /tmp/qlib-pregrant.out
|
||
exit 0
|
||
fi
|
||
|
||
stop_service() {
|
||
log "停止 launchd 托管的 MariaDB"
|
||
launchctl bootout "${LAUNCHD_LABEL}" 2>/dev/null || \
|
||
launchctl unload -w "${PLIST}" 2>/dev/null || \
|
||
warn "launchctl 停止未成功,继续尝试"
|
||
for _ in $(seq 1 60); do
|
||
pgrep -x mariadbd >/dev/null 2>&1 || pgrep -x mysqld >/dev/null 2>&1 || break
|
||
sleep 0.5
|
||
done
|
||
if pgrep -x mariadbd >/dev/null 2>&1 || pgrep -x mysqld >/dev/null 2>&1; then
|
||
warn "实例未在 30s 内退出,强制结束"
|
||
pkill -x mariadbd 2>/dev/null || true
|
||
pkill -x mysqld 2>/dev/null || true
|
||
sleep 3
|
||
fi
|
||
# 清掉残留 socket/pid,避免临时实例或重启冲突
|
||
rm -f "${MAIN_SOCK}" 2>/dev/null || true
|
||
[ -f "${MAIN_PID_FILE}" ] && mv -f "${MAIN_PID_FILE}" "${MAIN_PID_FILE}.pre-recover" 2>/dev/null || true
|
||
SERVICE_STOPPED=1
|
||
log "服务已停止"
|
||
}
|
||
|
||
start_service() {
|
||
log "重新启动 launchd 托管的 MariaDB"
|
||
launchctl bootstrap system "${PLIST}" 2>/dev/null || \
|
||
launchctl load -w "${PLIST}" 2>/dev/null || \
|
||
warn "launchctl 启动未成功"
|
||
for _ in $(seq 1 120); do
|
||
if "${MDB}/mariadb" -h 127.0.0.1 -P 3306 -u qlib -p"${QLIB_PW}" -N -e "SELECT 1;" >/dev/null 2>&1; then
|
||
SERVICE_STOPPED=0
|
||
log "服务已恢复,qlib 账号可登录"
|
||
return 0
|
||
fi
|
||
sleep 1
|
||
done
|
||
warn "服务未在 120s 内就绪,请检查:sudo launchctl print ${LAUNCHD_LABEL}"
|
||
return 1
|
||
}
|
||
|
||
# 任何情况下退出都要把正式服务拉起来
|
||
cleanup() {
|
||
local rc=$?
|
||
[ -f "${TMP_PID}" ] && kill "$(cat "${TMP_PID}")" 2>/dev/null || true
|
||
sleep 2
|
||
if [ "${SERVICE_STOPPED}" -eq 1 ]; then
|
||
warn "脚本异常中断,正在恢复正式服务…"
|
||
start_service || true
|
||
fi
|
||
rm -rf "${RUN}"
|
||
return $rc
|
||
}
|
||
trap cleanup EXIT
|
||
|
||
# ---------- 步骤 1:停正式实例 ----------
|
||
stop_service
|
||
|
||
# ---------- 步骤 2:起临时实例(skip-grant-tables) ----------
|
||
log "启动临时实例(--skip-grant-tables,不监听 TCP)"
|
||
rm -rf "${RUN}"
|
||
install -d -o "${MYSQLUSER}" -g "${MYSQLUSER}" -m 0770 "${RUN}"
|
||
sudo -u "${MYSQLUSER}" "${MDB}/mariadbd" \
|
||
--datadir="${DATADIR}" \
|
||
--socket="${TMP_SOCK}" \
|
||
--pid-file="${TMP_PID}" \
|
||
--skip-grant-tables \
|
||
--skip-networking \
|
||
>"${RUN}/server.log" 2>&1 &
|
||
for _ in $(seq 1 60); do
|
||
[ -S "${TMP_SOCK}" ] && break
|
||
sleep 0.5
|
||
done
|
||
if [ ! -S "${TMP_SOCK}" ]; then
|
||
warn "临时实例启动失败,日志:"
|
||
tail -30 "${RUN}/server.log" >&2
|
||
exit 1
|
||
fi
|
||
log "临时实例就绪:${TMP_SOCK}"
|
||
|
||
# ---------- 步骤 3:重置 root 密码 + 创建 qlib 账号 ----------
|
||
log "重置 root@localhost 密码并创建 qlib 账号"
|
||
"${MDB}/mariadb" --socket="${TMP_SOCK}" -u root <<SQL
|
||
FLUSH PRIVILEGES;
|
||
ALTER USER 'root'@'localhost' IDENTIFIED BY '${RECOVER_PW}';
|
||
CREATE USER IF NOT EXISTS 'qlib'@'localhost' IDENTIFIED BY '${QLIB_PW}';
|
||
CREATE USER IF NOT EXISTS 'qlib'@'127.0.0.1' IDENTIFIED BY '${QLIB_PW}';
|
||
ALTER USER 'qlib'@'localhost' IDENTIFIED BY '${QLIB_PW}';
|
||
ALTER USER 'qlib'@'127.0.0.1' IDENTIFIED BY '${QLIB_PW}';
|
||
GRANT ALL PRIVILEGES ON \`qlib\`.* TO 'qlib'@'localhost';
|
||
GRANT ALL PRIVILEGES ON \`qlib\`.* TO 'qlib'@'127.0.0.1';
|
||
SELECT user, host, plugin FROM mysql.user WHERE user IN ('root','qlib');
|
||
SELECT COUNT(*) AS qlib_tables FROM information_schema.tables WHERE table_schema='qlib';
|
||
FLUSH PRIVILEGES;
|
||
SQL
|
||
SQL_RC=$?
|
||
if [ "${SQL_RC}" -ne 0 ]; then
|
||
warn "授权 SQL 执行失败(rc=${SQL_RC}),将恢复正式服务"
|
||
exit 1
|
||
fi
|
||
|
||
# ---------- 步骤 4:关临时实例 ----------
|
||
log "关闭临时实例"
|
||
if [ -f "${TMP_PID}" ]; then
|
||
kill "$(cat "${TMP_PID}")" 2>/dev/null || true
|
||
for _ in $(seq 1 40); do
|
||
pgrep -x mariadbd >/dev/null 2>&1 || break
|
||
sleep 0.5
|
||
done
|
||
fi
|
||
pkill -x mariadbd 2>/dev/null || true
|
||
sleep 2
|
||
rm -f "${TMP_SOCK}" "${TMP_PID}" 2>/dev/null || true
|
||
|
||
# ---------- 步骤 5:重启正式实例并验证 ----------
|
||
start_service || exit 1
|
||
|
||
log "验证 qlib 账号(TCP 127.0.0.1:3306)"
|
||
"${MDB}/mariadb" -h 127.0.0.1 -P 3306 -u qlib -p"${QLIB_PW}" \
|
||
-e "SELECT CURRENT_USER() AS whoami; SELECT COUNT(*) AS qlib_tables FROM information_schema.tables WHERE table_schema='qlib';"
|
||
|
||
echo ""
|
||
echo "============================================================"
|
||
echo " 完成"
|
||
echo " root@localhost 密码已重置为:${RECOVER_PW}"
|
||
echo " qlib 账号:user=qlib db=qlib(密码与项目 .env 一致)"
|
||
echo ""
|
||
echo " 建议尽快改掉 root 密码:"
|
||
echo " ${MDB}/mariadb -h 127.0.0.1 -u root -p"
|
||
echo " MariaDB> ALTER USER 'root'@'localhost' IDENTIFIED BY '<新密码>';"
|
||
echo "============================================================" |