diff --git a/Dockerfile b/Dockerfile index 82872aa..c60bac9 100644 --- a/Dockerfile +++ b/Dockerfile @@ -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"] \ No newline at end of file +# 运行定时任务调度器(默认daemon模式) +CMD ["python", "scripts/daily_scheduler.py", "--time", "09:00"] \ No newline at end of file diff --git a/Dockerfile_base b/Dockerfile_base index 7cb5bf8..c82bb3d 100644 --- a/Dockerfile_base +++ b/Dockerfile_base @@ -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 diff --git a/build-and-push.sh b/build-and-push.sh index af40163..0e770e9 100755 --- a/build-and-push.sh +++ b/build-and-push.sh @@ -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 "=========================================" \ No newline at end of file diff --git a/docker-compose.yml b/docker-compose.yml index 5dec016..c8d6992 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -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"] diff --git a/requirements.txt b/requirements.txt index 0e88e56..a32877a 100644 --- a/requirements.txt +++ b/requirements.txt @@ -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 \ No newline at end of file + +# ==================== 重试机制 ==================== +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 \ No newline at end of file diff --git a/scripts/daily_scheduler.py b/scripts/daily_scheduler.py index 658219e..a1ffd65 100644 --- a/scripts/daily_scheduler.py +++ b/scripts/daily_scheduler.py @@ -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__": diff --git a/strategies/rotation/report.py b/strategies/rotation/report.py index 4a4dd13..6288e76 100644 --- a/strategies/rotation/report.py +++ b/strategies/rotation/report.py @@ -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["轮动策略净值"]