Compare commits

..

2 Commits

Author SHA1 Message Date
fb2f814111 feat(docker): 优化镜像支持中文字体及调度运行模式
- 基础镜像中添加多款中文字体,支持中文显示
- 主镜像安装中文字体并设置上海时区环境变量
- Dockerfile中创建日志目录并修改默认启动命令为定时调用调度器脚本
- 构建脚本支持动态镜像名,自动构建基础镜像,完善运行容器示例
- docker-compose修改为仅启动调度器服务,挂载相关配置、密钥、数据和日志目录
- 依赖更新,丰富金融数据、技术分析、绘图、机器学习及环境变量支持库
- 调度脚本参数调整,支持立即运行并退出及非后台模式运行切换
- 报告绘图中优先使用基础镜像预装的中文字体配置,提高字体兼容性和显示效果
2026-03-19 22:53:06 +08:00
1b8eba8aff chore(docker): 允许将 .env 文件打包进镜像
- 将 .env 文件从忽略列表中移除
- 注释说明容器化部署需要包含 .env 文件
- 保持 .env.local 及相关文件依旧被忽略
2026-03-19 22:52:21 +08:00
8 changed files with 121 additions and 60 deletions

View File

@@ -74,7 +74,7 @@ docker-compose*.yml
.dockerignore .dockerignore
# 其他 # 其他
.env # .env # 允许打包进镜像(容器化部署需要)
.env.local .env.local
.env.*.local .env.*.local

View File

@@ -3,6 +3,11 @@ FROM index-base:latest
# 设置工作目录 # 设置工作目录
WORKDIR /app 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 . COPY requirements.txt .
@@ -11,9 +16,14 @@ RUN uv pip install --system -r requirements.txt
# 仅复制除 data 目录外的应用代码, data 在 dockerignore 中已经被排除 # 仅复制除 data 目录外的应用代码, data 在 dockerignore 中已经被排除
COPY . . COPY . .
# 创建日志目录
RUN mkdir -p /app/logs
# 暴露端口 # 设置时区为上海
ENV TZ=Asia/Shanghai
# 暴露端口如需Web服务
EXPOSE 80 EXPOSE 80
# 运行应用 # 运行定时任务调度器默认daemon模式
# CMD ["python", "update_data.py"] CMD ["python", "scripts/daily_scheduler.py", "--time", "09:00"]

View File

@@ -64,6 +64,13 @@ RUN apt-get update && apt-get install -y \
libopengl0 \ libopengl0 \
&& rm -rf /var/lib/apt/lists/* && 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浏览器 # 安装Playwright浏览器
RUN playwright install chromium RUN playwright install chromium
RUN playwright install-deps RUN playwright install-deps

View File

@@ -1,20 +1,35 @@
#!/bin/bash #!/bin/bash
# Value Investing App - 构建和推送脚本 # ETF策略 - 构建和推送脚本
# 使用 docker 构建镜像并推送到私有仓库 # 使用 docker 构建镜像并推送到私有仓库
set -e # 遇到错误立即退出 set -e # 遇到错误立即退出
# 配置变量 # 配置变量
IMAGE_NAME="index-app" IMAGE_NAME="${1:-etf-scheduler}"
REGISTRY="192.168.0.115:5000" REGISTRY="192.168.0.115:5000"
TAG="latest" TAG="latest"
FULL_IMAGE_NAME="${REGISTRY}/${IMAGE_NAME}:${TAG}" FULL_IMAGE_NAME="${REGISTRY}/${IMAGE_NAME}:${TAG}"
echo "=========================================" echo "========================================="
echo "Value Investing App 构建和推送脚本" echo "ETF策略 构建和推送脚本"
echo "=========================================" 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 镜像..." echo "1. 构建 Docker 镜像..."
@@ -64,8 +79,12 @@ echo "========================================="
echo "镜像地址: ${FULL_IMAGE_NAME}" echo "镜像地址: ${FULL_IMAGE_NAME}"
echo "" echo ""
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 "或者在其他机器上拉取镜像:" echo "或者在其他机器上拉取镜像:"
echo "docker pull ${FULL_IMAGE_NAME}" echo "docker pull k3d-quant-registry:5000/${IMAGE_NAME}:${TAG}"
echo "=========================================" echo "========================================="

View File

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

View File

@@ -1,29 +1,53 @@
# 核心依赖 # ==================== 核心依赖 ====================
pandas>=1.5.0 pandas>=1.5.0
numpy>=1.24.0 numpy>=1.24.0
# 数据库连接 # ==================== 数据库连接 ====================
psycopg2-binary>=2.9.0 psycopg2-binary>=2.9.0
sqlalchemy>=2.0.0 sqlalchemy>=2.0.0
# 日志 # ==================== 日志 ====================
loguru>=0.7.0 loguru>=0.7.0
# ==================== HTTP请求 ====================
# HTTP请求
requests>=2.28.0 requests>=2.28.0
urllib3>=1.26.0 urllib3>=1.26.0
# 进度条 # ==================== 进度条 ====================
tqdm>=4.65.0 tqdm>=4.65.0
# ==================== 时间处理 ====================
# 时间处理
python-dateutil>=2.8.0 python-dateutil>=2.8.0
schedule schedule>=1.2.0
akshare
TA-Lib
tabulate
# ==================== 金融数据 ====================
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 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", "--time",
type=str, type=str,
default="09:00", default="09:00",
help="执行时间 (HH:MM),默认15:30", help="执行时间 (HH:MM),默认09:00",
) )
parser.add_argument( parser.add_argument(
"--config", "--config",
@@ -269,12 +269,12 @@ def main():
parser.add_argument( parser.add_argument(
"--run-now", "--run-now",
action="store_true", action="store_true",
help="立即执行一次(不启动定时任务)", help="立即执行一次并退出(不启动定时任务)",
) )
parser.add_argument( parser.add_argument(
"--daemon", "--no-daemon",
action="store_true", action="store_true",
help="后台运行(持续执行定时任务)", help="后台模式:执行一次后进入定时循环",
) )
args = parser.parse_args() args = parser.parse_args()
@@ -282,19 +282,19 @@ def main():
(project_root / "logs").mkdir(exist_ok=True) (project_root / "logs").mkdir(exist_ok=True)
if args.run_now: if args.run_now:
# 立即执行一次 # 立即执行一次并退出
daily_task(args.config) daily_task(args.config)
elif args.daemon: elif args.no_daemon:
# 后台运行模式 # 后台模式:执行一次后进入定时循环
setup_schedule(args.time, args.config)
run_scheduler_loop()
else:
# 默认:设置定时任务并执行一次(用于测试)
setup_schedule(args.time, args.config) setup_schedule(args.time, args.config)
logger.info("执行一次任务用于测试...") logger.info("执行一次任务用于测试...")
daily_task(args.config) daily_task(args.config)
logger.info("测试完成,启动定时任务循环(按 Ctrl+C 停止)...") logger.info("测试完成,启动定时任务循环(按 Ctrl+C 停止)...")
run_scheduler_loop() run_scheduler_loop()
else:
# 默认后台daemon模式
setup_schedule(args.time, args.config)
run_scheduler_loop()
if __name__ == "__main__": if __name__ == "__main__":

View File

@@ -292,7 +292,14 @@ def _plot_report_chart(
metrics: dict = None, 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 plt.rcParams["axes.unicode_minus"] = False
strategy_nav = backtest_result["轮动策略净值"] strategy_nav = backtest_result["轮动策略净值"]