""" 预测服务 - 统一模型预测 (DLinear, N-BEATS, TTM-R2) """ import os import sys import threading from datetime import datetime, timedelta from typing import Optional, List, Dict, Tuple from zoneinfo import ZoneInfo import pandas as pd import numpy as np # 添加项目路径 sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) from data.fetcher import BinanceDataFetcher from api.models import PredictionModel from models.inference import get_inference_manager class PredictionService: """预测服务 - 管理多模型预测""" # 类级别的锁,确保多个请求不会同时生成预测 _prediction_lock = threading.Lock() def __init__(self): self.et_tz = ZoneInfo('America/New_York') self.fetcher = BinanceDataFetcher() self.inference_manager = get_inference_manager() # 记录正在处理的日期和月份,避免重复处理 self._processing_dates = set() self._processing_months = set() # 预加载模型 print("正在预加载预测模型...") self.inference_manager.load_all_models() def _get_btc_data(self, days: int = 30) -> pd.DataFrame: """获取BTC历史数据""" end_date = (datetime.now() + timedelta(days=1)).strftime('%Y-%m-%d') start_date = (datetime.now() - timedelta(days=days)).strftime('%Y-%m-%d') df = self.fetcher.fetch_ohlcv( symbol='BTC', timeframe='1h', start_date=start_date, end_date=end_date, use_cache=False ) return df def predict_dlinear(self, df: pd.DataFrame) -> Tuple[int, float]: """DLinear 预测代理""" pred, prob = self.inference_manager.predict_dlinear(df) if pred is None: return 0, 0.5 return pred, prob if prob is not None else 0.5 def predict_ttm(self, df: pd.DataFrame) -> Tuple[int, float]: """TTM-R2 预测代理""" pred, prob = self.inference_manager.predict_ttm(df) if pred is None: return 0, 0.5 return pred, prob if prob is not None else 0.5 def predict_current_hour(self) -> Dict: """ 预测当前小时的涨跌 (多模型集成) """ df = self._get_btc_data() # 获取当前时间 (美东) now_et = datetime.now(self.et_tz) current_date = now_et.strftime('%Y-%m-%d') current_hour = now_et.hour # 最后一个已完成的K线信息 (作为预测基准) last_timestamp = df['timestamp'].iloc[-1] last_close = float(df['close'].iloc[-1]) # 当前正在形成的K线信息 current_price = float(df['close'].iloc[-1]) # 当前价格 results = { "date": current_date, "hour": current_hour, "timestamp": str(last_timestamp), "base_price": last_close, # 预测基准价格 "current_price": current_price, "models": {} } # 使用 InferenceManager 获取所有预测 predictions = self.inference_manager.predict_all(df) # 填充结果 if 'dlinear' in predictions: results["models"]["DLinear"] = { "prediction": predictions['dlinear']['prediction'], "probability": round(predictions['dlinear']['probability'], 4), "direction": "UP" if predictions['dlinear']['prediction'] == 1 else "DOWN" } if 'nbeats' in predictions: results["models"]["N-BEATS"] = { "prediction": predictions['nbeats']['prediction'], "probability": round(predictions['nbeats']['probability'], 4), "direction": "UP" if predictions['nbeats']['prediction'] == 1 else "DOWN" } if 'ttm' in predictions: results["models"]["TTM-R2"] = { "prediction": predictions['ttm']['prediction'], "probability": round(predictions['ttm']['probability'], 4), "direction": "UP" if predictions['ttm']['prediction'] == 1 else "DOWN" } if 'ensemble' in predictions and predictions['ensemble']: ens_pred = predictions['ensemble']['prediction'] results["models"]["Ensemble"] = { "prediction": ens_pred, "probability": 0.0, # 集成暂无概率 "direction": "UP" if ens_pred == 1 else "DOWN", "votes": predictions['ensemble']['votes'] } return results def ensure_predictions_for_date(self, date: str) -> None: """ 确保指定日期有预测数据,如果没有则生成 (V2) Args: date: 日期 (YYYY-MM-DD) """ # 使用锁避免并发冲突 with PredictionService._prediction_lock: if date in self._processing_dates: return self._processing_dates.add(date) try: # 检查是否有缺失的小时 (只检查宽表是否有记录) missing_hours = PredictionModel.get_missing_hours(date) if not missing_hours: return print(f"为 {date} 生成预测 (缺失 {len(missing_hours)} 小时)...") # 获取足够的历史数据 target_date = datetime.strptime(date, '%Y-%m-%d') df = self._get_historical_data(target_date) if df is None or len(df) < 200: print(f" 警告: 数据不足,无法为 {date} 生成预测") return # 为每个缺失的小时生成预测 for hour in missing_hours: self._generate_prediction_for_hour(df, date, hour) finally: with PredictionService._prediction_lock: self._processing_dates.discard(date) def _get_historical_data(self, target_date: datetime) -> Optional[pd.DataFrame]: """获取指定日期的历史数据""" try: start_date = (target_date - timedelta(days=30)).strftime('%Y-%m-%d') end_date = (target_date + timedelta(days=2)).strftime('%Y-%m-%d') df = self.fetcher.fetch_ohlcv( symbol='BTC', timeframe='1h', start_date=start_date, end_date=end_date, use_cache=True ) return df except Exception as e: print(f"获取历史数据失败: {e}") return None def _generate_prediction_for_hour(self, df: pd.DataFrame, date: str, hour: int) -> None: """ 为指定小时生成预测 (V2) """ try: # 1. 构建目标时间 (美东时间 -> UTC) target_time_et = pd.Timestamp(f"{date} {hour:02d}:00:00", tz='America/New_York') target_time_utc = target_time_et.tz_convert('UTC').tz_localize(None) # 2. 确保 timestamp 列是 datetime 类型 if 'timestamp_dt' not in df.columns: df = df.copy() df['timestamp_dt'] = pd.to_datetime(df['timestamp']) # 3. 找到目标小时的K线 (用于验证) target_mask = df['timestamp_dt'] == target_time_utc if not target_mask.any(): return target_row = df[target_mask].iloc[0] target_open = float(target_row['open']) target_close = float(target_row['close']) # 4. 找到前一小时的K线 (用于预测输入和基准价格) prev_time_utc = target_time_utc - pd.Timedelta(hours=1) prev_mask = df['timestamp_dt'] == prev_time_utc if not prev_mask.any(): return prev_row = df[prev_mask].iloc[0] prev_close = float(prev_row['close']) # 5. 获取预测输入数据: 前一小时及以前的所有数据 input_mask = df['timestamp_dt'] <= prev_time_utc df_input = df[input_mask].copy() if len(df_input) < 200: return # 6. 调用多模型预测 preds = self.inference_manager.predict_all(df_input) # 7. 判断实际涨跌 # Check if the candle is closed (current time > candle open time + 1 hour) current_now_utc = datetime.now(ZoneInfo('UTC')).replace(tzinfo=None) candle_close_time = target_time_utc + timedelta(hours=1) is_closed = current_now_utc >= candle_close_time if not is_closed: # 仅保存已完成的K线预测,未完成的跳过 (等待完成后再生成) # 实时交易逻辑在 auto_trade_polymarket.py 中单独处理 return actual_result = 1 if target_close > prev_close else 0 # 8. 组装数据 (宽表格式) prediction_data = { "timestamp": target_time_utc.isoformat(), "timestamp_et": target_time_et.isoformat(), "date": date, "hour": hour, "open_price": target_open, "close_price": target_close, "actual_result": actual_result, # DLinear "dlinear_prediction": preds.get('dlinear', {}).get('prediction'), "dlinear_prob": preds.get('dlinear', {}).get('probability'), "dlinear_correct": 1 if preds.get('dlinear', {}).get('prediction') == actual_result else 0, # N-BEATS "nbeats_prediction": preds.get('nbeats', {}).get('prediction'), "nbeats_prob": preds.get('nbeats', {}).get('probability'), "nbeats_correct": 1 if preds.get('nbeats', {}).get('prediction') == actual_result else 0, # TTM-R2 "ttm_prediction": preds.get('ttm', {}).get('prediction'), "ttm_prob": preds.get('ttm', {}).get('probability'), "ttm_correct": 1 if preds.get('ttm', {}).get('prediction') == actual_result else 0, # Ensemble "ensemble_prediction": preds.get('ensemble', {}).get('prediction'), "ensemble_votes": preds.get('ensemble', {}).get('votes'), "ensemble_correct": 1 if preds.get('ensemble', {}).get('prediction') == actual_result else 0, } PredictionModel.save_hourly_prediction(prediction_data) except Exception as e: print(f"生成预测失败 ({date} {hour}:00): {e}") def ensure_predictions_for_month(self, month: str) -> None: """ 确保指定月份有预测数据 (CST) 需要检查: 1. 该月的所有日期 (ET) 2. 上个月的最后一天 (ET) -> 因为 ET 上个月最后一天 11:00-23:00 对应 CST 该月第一天 00:00-12:00 Args: month: 月份 (YYYY-MM) """ # 使用锁避免并发冲突 with PredictionService._prediction_lock: if month in self._processing_months: return self._processing_months.add(month) try: year, mon = map(int, month.split('-')) from calendar import monthrange _, last_day = monthrange(year, mon) # 1. 确保上个月最后一天有数据 (为了 CST 的开头) first_day_of_month = datetime(year, mon, 1).date() prev_day = first_day_of_month - timedelta(days=1) prev_day_str = prev_day.strftime('%Y-%m-%d') self.ensure_predictions_for_date(prev_day_str) today = datetime.now(self.et_tz).date() # 2. 确保本月所有日期 for day in range(1, last_day + 1): date = f"{year}-{mon:02d}-{day:02d}" date_obj = datetime.strptime(date, '%Y-%m-%d').date() if date_obj <= today: self.ensure_predictions_for_date(date) finally: with PredictionService._prediction_lock: self._processing_months.discard(month) # 全局单例 _prediction_service: Optional[PredictionService] = None def get_prediction_service() -> PredictionService: """获取预测服务单例""" global _prediction_service if _prediction_service is None: _prediction_service = PredictionService() return _prediction_service