feat(docker): 优化镜像支持中文字体及调度运行模式

- 基础镜像中添加多款中文字体,支持中文显示
- 主镜像安装中文字体并设置上海时区环境变量
- Dockerfile中创建日志目录并修改默认启动命令为定时调用调度器脚本
- 构建脚本支持动态镜像名,自动构建基础镜像,完善运行容器示例
- docker-compose修改为仅启动调度器服务,挂载相关配置、密钥、数据和日志目录
- 依赖更新,丰富金融数据、技术分析、绘图、机器学习及环境变量支持库
- 调度脚本参数调整,支持立即运行并退出及非后台模式运行切换
- 报告绘图中优先使用基础镜像预装的中文字体配置,提高字体兼容性和显示效果
This commit is contained in:
2026-03-19 22:53:06 +08:00
parent 1b8eba8aff
commit fb2f814111
7 changed files with 120 additions and 59 deletions

View File

@@ -3,6 +3,11 @@ FROM index-base:latest
# 设置工作目录
WORKDIR /app
# 安装中文字体(使用清华源加速)
RUN apt-get update && apt-get install -y --no-install-recommends \
fonts-wqy-microhei \
&& rm -rf /var/lib/apt/lists/*
# 复制依赖文件
COPY requirements.txt .
@@ -11,9 +16,14 @@ RUN uv pip install --system -r requirements.txt
# 仅复制除 data 目录外的应用代码, data 在 dockerignore 中已经被排除
COPY . .
# 创建日志目录
RUN mkdir -p /app/logs
# 暴露端口
# 设置时区为上海
ENV TZ=Asia/Shanghai
# 暴露端口如需Web服务
EXPOSE 80
# 运行应用
# CMD ["python", "update_data.py"]
# 运行定时任务调度器默认daemon模式
CMD ["python", "scripts/daily_scheduler.py", "--time", "09:00"]

View File

@@ -64,6 +64,13 @@ RUN apt-get update && apt-get install -y \
libopengl0 \
&& rm -rf /var/lib/apt/lists/*
# 安装中文字体
RUN apt-get update && apt-get install -y \
fonts-noto-cjk \
fonts-wqy-zenhei \
fonts-wqy-microhei \
&& rm -rf /var/lib/apt/lists/*
# 安装Playwright浏览器
RUN playwright install chromium
RUN playwright install-deps

View File

@@ -1,20 +1,35 @@
#!/bin/bash
# Value Investing App - 构建和推送脚本
# ETF策略 - 构建和推送脚本
# 使用 docker 构建镜像并推送到私有仓库
set -e # 遇到错误立即退出
# 配置变量
IMAGE_NAME="index-app"
IMAGE_NAME="${1:-etf-scheduler}"
REGISTRY="192.168.0.115:5000"
TAG="latest"
FULL_IMAGE_NAME="${REGISTRY}/${IMAGE_NAME}:${TAG}"
echo "========================================="
echo "Value Investing App 构建和推送脚本"
echo "ETF策略 构建和推送脚本"
echo "========================================="
echo "镜像名称: ${IMAGE_NAME}"
echo ""
# 检查并构建基础镜像(如果不存在)
echo "0. 检查基础镜像..."
if ! docker images | grep -q "index-base"; then
echo " 基础镜像不存在,开始构建..."
if [ -f "Dockerfile_base" ]; then
docker build --platform linux/arm64 -f Dockerfile_base -t index-base:latest .
echo " ✅ 基础镜像构建成功"
else
echo " ⚠️ 未找到 Dockerfile_base跳过基础镜像构建"
fi
else
echo " ✅ 基础镜像已存在"
fi
# 构建镜像
echo "1. 构建 Docker 镜像..."
@@ -64,8 +79,12 @@ echo "========================================="
echo "镜像地址: ${FULL_IMAGE_NAME}"
echo ""
echo "可以使用以下命令运行容器:"
echo "docker run -d -p 5000:5000 --name value-investing-container ${FULL_IMAGE_NAME}"
echo "docker run -d --name etf-scheduler-container \\"
echo " -v /path/to/.env:/app/.env \\"
echo " -v /path/to/hk_ecs.pem:/app/hk_ecs.pem \\"
echo " -v /path/to/data:/app/data \\"
echo " ${FULL_IMAGE_NAME}"
echo ""
echo "或者在其他机器上拉取镜像:"
echo "docker pull ${FULL_IMAGE_NAME}"
echo "docker pull k3d-quant-registry:5000/${IMAGE_NAME}:${TAG}"
echo "========================================="

View File

@@ -1,32 +1,26 @@
version: '3.8'
services:
postgres:
image: postgres:15-alpine
container_name: etf_postgres
etf-scheduler:
image: 192.168.0.115:5000/etf-scheduler:latest
container_name: etf-scheduler
restart: unless-stopped
environment:
POSTGRES_DB: etf_db
POSTGRES_USER: admin
POSTGRES_PASSWORD: admin
POSTGRES_INITDB_ARGS: "--encoding=UTF-8 --lc-collate=C --lc-ctype=C"
ports:
- "5432:5432"
- TZ=Asia/Shanghai
volumes:
- postgres_data:/var/lib/postgresql/data
networks:
- etf_network
healthcheck:
test: ["CMD-SHELL", "pg_isready -U admin -d etf_db"]
interval: 30s
timeout: 10s
retries: 3
start_period: 30s
volumes:
postgres_data:
driver: local
networks:
etf_network:
driver: bridge
# 挂载环境变量文件(必需)
- ./.env:/app/.env:ro
# 挂载 SSH 私钥(必需,用于 yfinance 数据下载)
- ./hk_ecs.pem:/app/hk_ecs.pem:ro
# 挂载数据目录(持久化)
- ./data:/app/data
# 挂载日志目录
- ./logs:/app/logs
# 挂载 results 目录(保存报告)
- ./results:/app/results
# 默认daemon模式运行只需简单命令即可
# command: ["python", "scripts/daily_scheduler.py"]
# 如需立即执行一次并退出:
# command: ["python", "scripts/daily_scheduler.py", "--run-now"]
# 如需执行一次后进入定时循环:
# command: ["python", "scripts/daily_scheduler.py", "--no-daemon"]

View File

@@ -1,29 +1,53 @@
# 核心依赖
# ==================== 核心依赖 ====================
pandas>=1.5.0
numpy>=1.24.0
# 数据库连接
# ==================== 数据库连接 ====================
psycopg2-binary>=2.9.0
sqlalchemy>=2.0.0
# 日志
# ==================== 日志 ====================
loguru>=0.7.0
# HTTP请求
# ==================== HTTP请求 ====================
requests>=2.28.0
urllib3>=1.26.0
# 进度条
# ==================== 进度条 ====================
tqdm>=4.65.0
# 时间处理
# ==================== 时间处理 ====================
python-dateutil>=2.8.0
schedule
akshare
TA-Lib
tabulate
schedule>=1.2.0
# ==================== 金融数据 ====================
tushare>=1.2.0
yfinance>=0.2.0
akshare>=1.10.0
# ==================== 技术分析 ====================
TA-Lib>=0.4.0
# ==================== 表格输出 ====================
tabulate>=0.9.0
# ==================== 自动化测试 ====================
playwright>=1.45.1
retry>=0.9.2
# ==================== 重试机制 ====================
retry>=0.9.2
# ==================== 环境变量 ====================
python-dotenv>=1.0.0
# ==================== 阿里云OSS ====================
oss2>=2.18.0
# ==================== YAML配置 ====================
PyYAML>=6.0
# ==================== 绘图 ====================
matplotlib>=3.7.0
# ==================== 机器学习 ====================
scikit-learn>=1.3.0

View File

@@ -258,7 +258,7 @@ def main():
"--time",
type=str,
default="09:00",
help="执行时间 (HH:MM),默认15:30",
help="执行时间 (HH:MM),默认09:00",
)
parser.add_argument(
"--config",
@@ -269,12 +269,12 @@ def main():
parser.add_argument(
"--run-now",
action="store_true",
help="立即执行一次(不启动定时任务)",
help="立即执行一次并退出(不启动定时任务)",
)
parser.add_argument(
"--daemon",
"--no-daemon",
action="store_true",
help="后台运行(持续执行定时任务)",
help="后台模式:执行一次后进入定时循环",
)
args = parser.parse_args()
@@ -282,19 +282,19 @@ def main():
(project_root / "logs").mkdir(exist_ok=True)
if args.run_now:
# 立即执行一次
# 立即执行一次并退出
daily_task(args.config)
elif args.daemon:
# 后台运行模式
setup_schedule(args.time, args.config)
run_scheduler_loop()
else:
# 默认:设置定时任务并执行一次(用于测试)
elif args.no_daemon:
# 后台模式:执行一次后进入定时循环
setup_schedule(args.time, args.config)
logger.info("执行一次任务用于测试...")
daily_task(args.config)
logger.info("测试完成,启动定时任务循环(按 Ctrl+C 停止)...")
run_scheduler_loop()
else:
# 默认后台daemon模式
setup_schedule(args.time, args.config)
run_scheduler_loop()
if __name__ == "__main__":

View File

@@ -292,7 +292,14 @@ def _plot_report_chart(
metrics: dict = None,
):
"""绘制报告图表"""
plt.rcParams["font.sans-serif"] = ["Arial Unicode MS", "SimHei", "DejaVu Sans"]
# 设置中文字体(优先使用基础镜像中已存在的字体)
plt.rcParams["font.sans-serif"] = [
"WenQuanYi Zen Hei", # 基础镜像已安装
"WenQuanYi Micro Hei", # 将要安装
"DejaVu Sans",
"SimHei",
"Arial Unicode MS"
]
plt.rcParams["axes.unicode_minus"] = False
strategy_nav = backtest_result["轮动策略净值"]