feat: 添加每日数据报告定时推送功能

- 新增 daily_report_sender.py 定时任务程序
- 北京时间每天 8 点推送昨天 24 小时预测统计
- 包含各模型准确率、逐小时预测详情
- 失败交易订单汇总
- start_system.py 集成启动报告推送服务
This commit is contained in:
crypto_predictor committed 2026-02-28 10:55:34 +08:00
1 parent e7e01516ba
commit 7af21bed6a
2 files changed
+399

No files matched your search

+379
View File
@@ -0,0 +1,379 @@
#!/usr/bin/env python3
"""
每日数据报告定时推送程序
- 每天北京时间 8 点推送昨天的 24 小时预测统计
- 包含各模型准确率、逐小时预测结果详情
"""
import schedule
import time
import logging
from datetime import datetime, timedelta
from zoneinfo import ZoneInfo
from typing import Dict, List
from utils.send_msg import lark_notifier
from api.models import PredictionModel, convert_to_cst, get_db_connection, TZ_CST, TZ_UTC
# 配置日志
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
handlers=[
logging.FileHandler('logs/daily_report.log'),
logging.StreamHandler()
]
)
logger = logging.getLogger(__name__)
class DailyReportSender:
"""每日数据报告发送器"""
def __init__(self):
self.model = PredictionModel()
self.lark = lark_notifier
def get_yesterday_failed_trades(self) -> List[Dict]:
"""
获取昨天的失败交易订单
Returns:
[
{
'timestamp': '2026-02-27 10:30:00',
'market_title': 'BTC 价格',
'action': 'BUY',
'status': 'failed',
'error_message': '余额不足'
},
...
]
"""
try:
# 获取昨天的日期(CST)
now_cst = datetime.now(TZ_CST)
yesterday_cst = now_cst - timedelta(days=1)
yesterday_date = yesterday_cst.strftime('%Y-%m-%d')
# 计算 CST 时间范围
start_cst = datetime.strptime(f"{yesterday_date} 00:00:00", '%Y-%m-%d %H:%M:%S').replace(tzinfo=TZ_CST)
end_cst = start_cst + timedelta(days=1)
start_str = start_cst.strftime('%Y-%m-%d %H:%M:%S')
end_str = end_cst.strftime('%Y-%m-%d %H:%M:%S')
# 从 trades 表查询失败订单
with get_db_connection() as conn:
cursor = conn.cursor()
sql = """
SELECT
timestamp,
market_title,
action,
status,
error_message
FROM trades
WHERE timestamp >= %s AND timestamp < %s
AND status != 'MATCHED'
ORDER BY timestamp DESC
"""
cursor.execute(sql, (start_str, end_str))
rows = cursor.fetchall()
return rows if rows else []
except Exception as e:
logger.error(f"获取昨天失败交易失败: {e}", exc_info=True)
return []
def get_yesterday_statistics(self) -> Dict:
"""
获取昨天的 24 小时预测统计和详细数据
Returns:
{
'date': 'YYYY-MM-DD',
'models': {
'DLinear': {'total': 24, 'correct': 14, 'accuracy': 58.3},
'N-BEATS': {...},
'TTM-R2': {...},
'Ensemble': {...}
},
'hourly_details': [
{
'hour': '00:00-01:00',
'actual': '涨',
'dlinear': {'pred': '涨', 'correct': True},
'nbeats': {...},
'ttm': {...},
'ensemble': {...}
},
...
]
}
"""
try:
# 获取昨天的日期(CST)
now_cst = datetime.now(TZ_CST)
yesterday_cst = now_cst - timedelta(days=1)
yesterday_date = yesterday_cst.strftime('%Y-%m-%d')
# 计算 UTC 时间范围
start_cst = datetime.strptime(f"{yesterday_date} 00:00:00", '%Y-%m-%d %H:%M:%S').replace(tzinfo=TZ_CST)
end_cst = start_cst + timedelta(days=1)
start_utc = start_cst.astimezone(TZ_UTC)
end_utc = end_cst.astimezone(TZ_UTC)
start_str = start_utc.strftime('%Y-%m-%d %H:%M:%S')
end_str = end_utc.strftime('%Y-%m-%d %H:%M:%S')
# 查询数据库
with get_db_connection() as conn:
cursor = conn.cursor()
sql = """
SELECT
timestamp,
actual_result,
dlinear_prediction, dlinear_correct,
nbeats_prediction, nbeats_correct,
ttm_prediction, ttm_correct,
ensemble_prediction, ensemble_correct
FROM hourly_model_predictions
WHERE timestamp >= %s AND timestamp < %s
ORDER BY timestamp ASC
"""
cursor.execute(sql, (start_str, end_str))
rows = cursor.fetchall()
# 整理统计数据
models = ['dlinear', 'nbeats', 'ttm', 'ensemble']
model_names = {
'dlinear': 'DLinear',
'nbeats': 'N-BEATS',
'ttm': 'TTM-R2',
'ensemble': 'Ensemble'
}
# 初始化统计
stats = {
'date': yesterday_date,
'models': {},
'hourly_details': []
}
for model in models:
stats['models'][model_names[model]] = {
'total': 0,
'correct': 0,
'accuracy': 0.0
}
# 处理每行数据
for idx, row in enumerate(rows):
if row.get('actual_result') is None:
continue
# 获取时间戳(转为 CST 显示)
ts_utc = row['timestamp']
if isinstance(ts_utc, str):
ts_utc = datetime.fromisoformat(str(ts_utc).replace('Z', '+00:00'))
if ts_utc.tzinfo is None:
ts_utc = ts_utc.replace(tzinfo=TZ_UTC)
ts_cst = ts_utc.astimezone(TZ_CST)
hour_start = ts_cst.strftime('%H:%M')
hour_end = (ts_cst + timedelta(hours=1)).strftime('%H:%M')
hour_label = f"{hour_start}-{hour_end}"
# 实际结果
actual_result = row['actual_result']
actual_label = '涨' if actual_result == 1 else '跌'
# 每小时预测详情
hourly_item = {
'hour': hour_label,
'actual': actual_label
}
# 逐模型处理预测结果
for model in models:
pred = row[f"{model}_prediction"]
correct = row[f"{model}_correct"]
# 更新统计
if pred is not None:
stats['models'][model_names[model]]['total'] += 1
if correct == 1:
stats['models'][model_names[model]]['correct'] += 1
# 添加到小时详情
pred_label = '涨' if pred == 1 else ('跌' if pred == 0 else '无')
correct_flag = correct == 1 if correct is not None else None
hourly_item[model] = {
'pred': pred_label,
'correct': '✓' if correct_flag else ('✗' if correct_flag is False else '-')
}
stats['hourly_details'].append(hourly_item)
# 计算准确率百分比
for model_name in stats['models']:
total = stats['models'][model_name]['total']
correct = stats['models'][model_name]['correct']
if total > 0:
stats['models'][model_name]['accuracy'] = round(correct / total * 100, 2)
return stats
except Exception as e:
logger.error(f"获取昨天统计数据失败: {e}", exc_info=True)
return None
def format_report(self, stats: Dict, failed_trades: List[Dict] = None) -> str:
"""
格式化报告为飞书 Markdown 格式
Args:
stats: get_yesterday_statistics 返回的数据字典
failed_trades: 失败订单列表
Returns:
Markdown 格式的报告文本
"""
if not stats:
return "❌ 无法获取昨天的统计数据,请检查数据库连接"
date = stats['date']
models = stats['models']
# 构建报告头
report = f"**【BTC 日报 - {date}】**\n\n"
# 模型准确率统计部分
report += "**📊 模型准确率统计(24小时)**\n"
for model_name in ['DLinear', 'N-BEATS', 'TTM-R2', 'Ensemble']:
data = models.get(model_name, {})
total = data.get('total', 0)
correct = data.get('correct', 0)
accuracy = data.get('accuracy', 0)
# 进度条可视化
bar_length = 10
filled = int(accuracy / 100 * bar_length)
bar = '█' * filled + '░' * (bar_length - filled)
report += f"• **{model_name}**: {correct}/{total} {bar} {accuracy}%\n"
report += "\n"
# 交易失败订单详情
report += "**⚠️ 交易失败订单**\n"
if failed_trades and len(failed_trades) > 0:
report += f"昨天共有 {len(failed_trades)} 个失败订单\n"
for idx, trade in enumerate(failed_trades[:10], 1): # 仅显示前10个
timestamp = trade.get('timestamp', '未知')
market_title = trade.get('market_title', '未知市场')
action = trade.get('action', '未知')
status = trade.get('status', '未知')
error_msg = trade.get('error_message', '无详细信息')
# 简化错误信息 - 仅取第一行
if error_msg:
error_msg = error_msg.split('\n')[0][:50] # 仅取前50字
else:
error_msg = '无错误信息'
report += f"{idx}. [{timestamp}] {market_title} ({action}) → {error_msg}\n"
if len(failed_trades) > 10:
report += f"... 还有 {len(failed_trades) - 10} 个失败订单\n"
else:
report += "昨日无失败订单,运营正常 ✓\n"
report += "\n生成时间: " + datetime.now(TZ_CST).strftime('%Y-%m-%d %H:%M:%S CST')
return report
def send_daily_report(self):
"""获取昨天统计并推送"""
try:
logger.info("开始推送每日报告...")
stats = self.get_yesterday_statistics()
if not stats:
logger.warning("无法获取昨天统计数据")
self.lark.send_alert(
title="⚠️ 每日报告生成失败",
content="无法获取昨天的统计数据,请检查数据库连接"
)
return
# 获取失败订单
failed_trades = self.get_yesterday_failed_trades()
# 格式化报告
report = self.format_report(stats, failed_trades)
# 推送到飞书
self.lark.send_alert(
title="📊 BTC 每日数据报告",
content=report
)
logger.info(f"每日报告已送出: {stats['date']}")
except Exception as e:
logger.error(f"推送每日报告异常: {e}", exc_info=True)
self.lark.send_alert(
title="❌ 每日报告推送异常",
content=f"错误详情: {str(e)}"
)
def schedule_daily_report():
"""
启动每日报告定时任务
北京时间每天 8 点执行
"""
sender = DailyReportSender()
sender.send_daily_report() # 启动时立即发送一次(可选)
# 使用 schedule 库在北京时间 8 点触发
# 由于 schedule 库基于本地时间,当前环境如果是 macOS 默认时间,
# 我们使用一个包装器来处理 CST 时间转换
def cst_8am_job():
"""北京时间 8 点执行的任务"""
now_cst = datetime.now(TZ_CST)
if now_cst.hour == 8 and now_cst.minute < 1:
# 确保只在 08:00-08:01 间隔执行一次(避免重复)
sender.send_daily_report()
# 每分钟检查一次是否需要执行 (确保能在 8 点执行)
schedule.every(1).minutes.do(cst_8am_job)
logger.info("每日报告定时任务已启动(北京时间每天 8 点执行)")
# 启动调度循环
while True:
schedule.run_pending()
time.sleep(1)
def run_scheduler_background():
"""
在后台线程中运行调度器(供 start_system.py 调用)
"""
import threading
scheduler_thread = threading.Thread(target=schedule_daily_report, daemon=True)
scheduler_thread.start()
logger.info("每日报告调度器已在后台启动")
return scheduler_thread
if __name__ == '__main__':
# 支持单独运行以及从其他模块导入
logger.info("启动每日报告系统...")
schedule_daily_report()
+20
View File
@@ -4,6 +4,7 @@
同时启动:
1. FastAPI 后端服务 (提供前端看板 API)
2. Polymarket 自动交易程序
3. 每日数据报告推送程序 (北京时间每天 8 点)
支持平台:macOS, Linux, Windows
"""
@@ -15,6 +16,13 @@ import os
import platform
from datetime import datetime
try:
from daily_report_sender import run_scheduler_background
except ImportError:
# 如果模块导入失败,定义一个空函数避免启动失败
def run_scheduler_background():
return None
# 检测操作系统
IS_WINDOWS = platform.system() == 'Windows'
@@ -126,6 +134,18 @@ def main():
trade_cmd = f"{python_exec} -u trader/auto_trade_polymarket.py"
trade_process = start_process(trade_cmd, "Polymarket 自动交易程序")
# 3. 启动每日数据报告推送程序(后台线程)
# 北京时间每天 8 点推送昨天的数据统计
try:
report_thread = run_scheduler_background()
if report_thread:
log("每日数据报告推送程序已启动", GREEN)
else:
log("⚠️ 每日数据报告推送程序启动失败", YELLOW)
except Exception as e:
log(f"⚠️ 启动每日报告程序异常: {e}", YELLOW)
report_thread = None
log("系统启动完成!", GREEN)
log("前端访问地址: http://localhost:8000", GREEN)
log("按 Ctrl+C 停止所有服务...", YELLOW)