chore: staged changes

This commit is contained in:
crypto_predictor committed 2026-02-27 13:36:37 +08:00
1 parent 1d732844e3
commit e7e01516ba
4 files changed
+431 -1

No files matched your search

+2
View File
@@ -12,6 +12,7 @@ import pytz
from dotenv import load_dotenv from dotenv import load_dotenv
from data.database import DatabaseManager from data.database import DatabaseManager
from utils.send_msg import lark_notifier
# 加载环境变量 # 加载环境变量
load_dotenv() load_dotenv()
@@ -156,6 +157,7 @@ class BinanceDataFetcher:
time.sleep(wait_time) time.sleep(wait_time)
if not klines: if not klines:
lark_notifier.send_alert(f"数据获取失败: {symbol} {timeframe}", "连续3次请求或解析发生异常,已放弃重试")
if not df.empty: if not df.empty:
return df return df
raise ValueError(f"未获取到 {symbol} {timeframe} 的数据") raise ValueError(f"未获取到 {symbol} {timeframe} 的数据")
+384
View File
@@ -0,0 +1,384 @@
"""2026年2月BTC数据 - DLinear, N-BEATS, TTM-R2, DeepSeek-R1(高并发版) 准确率测试
输入数据:预测K线的前一根(即用截止到t的数据预测t+1)
价格对比:预测结果与预测K线(t+1)的收盘价做对比
时间:币安数据UTC,输出转为北京时间(UTC+8),按北京时间分天
输出:每小时详细数据、每天准确率、总准确率、四模型集成、美股开盘标记
"""
import sys
import os
import json
import re
import time
import asyncio
import aiohttp
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
import torch
import numpy as np
import pandas as pd
import pickle
import pandas_ta as ta
from datetime import timedelta
from sklearn.preprocessing import StandardScaler
from data.fetcher import BinanceDataFetcher
from tsfm_public.models.tinytimemixer import TinyTimeMixerForPrediction
# ==================== 时区与美股判断 ====================
def utc_to_beijing(ts):
return ts + timedelta(hours=8)
def is_us_stock_open_by_utc(utc_ts):
et_ts = utc_ts - timedelta(hours=5)
et_date = pd.Timestamp(et_ts.date())
us_holidays_feb_2026 = {pd.Timestamp('2026-02-16')}
if et_date.weekday() >= 5 or et_date in us_holidays_feb_2026:
return False
return True
# ==================== 预处理器定义 ====================
class ClassificationPreprocessor:
def __init__(self, lookback=168, forecast_horizon=1):
self.lookback = lookback
self.forecast_horizon = forecast_horizon
self.scaler = StandardScaler()
self.is_fitted = False
class NBeatsPreprocessor:
def __init__(self, lookback=168, forecast_horizon=1):
self.lookback = lookback
self.forecast_horizon = forecast_horizon
self.price_scaler = StandardScaler()
self.is_fitted = False
def preprocess(self, df, fit=True):
df = df.copy()
prices = df['close'].values.reshape(-1, 1)
if fit:
scaled_prices = self.price_scaler.fit_transform(prices)
self.is_fitted = True
else:
scaled_prices = self.price_scaler.transform(prices)
X, valid_indices = [], []
for i in range(len(scaled_prices) - self.lookback):
X.append(scaled_prices[i:i + self.lookback, 0])
valid_indices.append(i + self.lookback - 1)
return np.array(X), df, valid_indices
def fmt_pred(val):
if val is None or (isinstance(val, float) and np.isnan(val)): return "N/A"
return "涨" if int(val) == 1 else "跌"
def fmt_result(pred, actual):
if pred is None or (isinstance(pred, float) and np.isnan(pred)): return " "
return "✓" if int(pred) == int(actual) else "✗"
from datetime import datetime
import json
import re
import time
import asyncio
from openai import AsyncOpenAI
import openai
# ==================== DeepSeek 异步并发核心逻辑 (双重锁绝对防 429) ====================
def get_log_time():
return datetime.now().strftime("%H:%M:%S")
async def fetch_deepseek_single(client, messages, item_id, rate_lock, global_state, sem):
"""
单次异步API调用:并发数控制 + 漏桶控速 + 指数退避重试
"""
attempt = 0
while True:
attempt += 1
# 0. 并发锁:保证同一时刻,最多只有 N 个请求在英伟达服务器上运行
async with sem:
# 1. 速率锁:保证就算允许并发,发包的瞬间也要间隔至少 2.1 秒 (约 28次/分钟,留足余量)
async with rate_lock:
now = time.time()
elapsed = now - global_state['last_call_time']
if elapsed < 2.1:
await asyncio.sleep(2.1 - elapsed)
global_state['last_call_time'] = time.time()
print(f"[{get_log_time()}] 🚀 [任务 {item_id}] 发起请求 (第 {attempt} 次尝试)...")
try:
# 2. 调用 API (注意:请确保 model 是你刚才探测到的最新可用模型名)
completion = await client.chat.completions.create(
model="deepseek-ai/deepseek-v3.2", # <--- 如果旧名字报错410,请务必替换为最新名字
messages=messages,
temperature=0.6,
top_p=0.7,
max_tokens=1024,
timeout=60.0
)
content = completion.choices[0].message.content
match = re.search(r'\{.*\}', content, re.DOTALL)
if match:
res = json.loads(match.group(0))
pred = res.get('prediction')
if pred in [0, 1]:
pred_str = "涨" if pred == 1 else "跌"
print(f"[{get_log_time()}] ✅ [任务 {item_id}] 成功获取预测: {pred_str}")
return item_id, int(pred)
else:
print(f"[{get_log_time()}] ⚠️ [任务 {item_id}] 异常预测值: {pred},重新请求...")
continue
else:
print(f"[{get_log_time()}] ⚠️ [任务 {item_id}] JSON 解析失败,重新请求...")
continue
except openai.RateLimitError:
# 指数退避:第一次等 5 秒,第二次等 10 秒,第三次等 20 秒...
wait_time = 5 * (2 ** (attempt - 1))
wait_time = min(wait_time, 60) # 最多等 60 秒
print(f"[{get_log_time()}] 🛑 [任务 {item_id}] 触发限流 (429),休眠 {wait_time} 秒后重试...")
await asyncio.sleep(wait_time)
except openai.APITimeoutError:
print(f"[{get_log_time()}] ⏳ [任务 {item_id}] 请求超时 (>60s),重新请求...")
await asyncio.sleep(2)
except openai.APIStatusError as e:
if e.status_code in [400, 401, 403, 404, 410]:
print(f"[{get_log_time()}] 🚨 [任务 {item_id}] 致命错误 (HTTP {e.status_code}): {e.message}。放弃。")
return item_id, None
print(f"[{get_log_time()}] ❌ [任务 {item_id}] API 报错 (HTTP {e.status_code}),休眠 3 秒后重试...")
await asyncio.sleep(3)
except Exception as e:
print(f"[{get_log_time()}] 💥 [任务 {item_id}] 未知错误: {e},休眠 3 秒后重新请求...")
await asyncio.sleep(3)
async def run_all_deepseek_async(tasks_data, api_key):
"""统筹所有的异步请求"""
total = len(tasks_data)
# === 核心参数配置 ===
MAX_CONCURRENT = 8 # 绝对并发上限:同一时刻最多 2 个请求在处理
print("\n" + "="*80)
print(f"[{get_log_time()}] 开始 DeepSeek 异步并发请求 (双重防 429 版)")
print(f"总任务数: {total} 个 | 严格并发数: {MAX_CONCURRENT} | 发包间隔: 2.1 秒")
print("="*80 + "\n")
client = AsyncOpenAI(
base_url="https://integrate.api.nvidia.com/v1",
api_key=api_key
)
sem = asyncio.Semaphore(MAX_CONCURRENT)
rate_lock = asyncio.Lock()
global_state = {'last_call_time': 0.0}
ds_results_dict = {}
tasks = []
for item in tasks_data:
task = asyncio.create_task(
fetch_deepseek_single(client, item['messages'], item['id'], rate_lock, global_state, sem)
)
tasks.append(task)
completed = 0
for coro in asyncio.as_completed(tasks):
item_id, pred = await coro
ds_results_dict[item_id] = pred
completed += 1
if completed % 10 == 0 or completed == total:
print(f"\n[{get_log_time()}] 📊 【全局进度】 DeepSeek 已完成: {completed}/{total} ({(completed/total):.1%})\n")
return ds_results_dict
# ==================== 主程序 ====================
def main():
print("=" * 115)
print("BTC 1h 涨跌预测准确率测试 (DLinear, N-BEATS, TTM-R2, DeepSeek-R1 异步版)")
print("=" * 115)
device = torch.device('mps' if torch.backends.mps.is_available() else 'cpu')
print(f"使用设备: {device}")
# ========== 步骤1: 加载模型 ==========
print("\n[1/4] 加载本地模型...")
from models.dlinear.model import DLinear
dlinear_checkpoint = torch.load('./models/checkpoints/dlinear_classification_2026/best_model.pt', map_location=device, weights_only=False)
dlinear_model = DLinear(dlinear_checkpoint['config']).to(device).eval()
dlinear_model.load_state_dict(dlinear_checkpoint['model_state_dict'])
with open('./models/checkpoints/dlinear_classification_2026/preprocessor.pkl', 'rb') as f:
dlinear_preprocessor = pickle.load(f)
from models.nbeats.model import NBeats
nbeats_checkpoint = torch.load('./models/checkpoints/nbeats_btc_1h/best_model.pt', map_location=device, weights_only=False)
nbeats_model = NBeats(nbeats_checkpoint['config']).to(device).eval()
with open('./models/checkpoints/nbeats_btc_1h/preprocessor.pkl', 'rb') as f:
nbeats_preprocessor = pickle.load(f)
ttm_model = TinyTimeMixerForPrediction.from_pretrained("ibm-granite/granite-timeseries-ttm-r2", trust_remote_code=True).to(device).eval()
ds_api_key = os.environ.get("NVIDIA_API_KEY", "")
if not ds_api_key:
print(" ⚠️ 未检测到 NVIDIA_API_KEY,跳过 DeepSeek 测试。")
else:
print(" ✓ DeepSeek-R1 (异步并发) 已就绪")
# ========== 步骤2: 获取历史数据 ==========
print("\n[2/4] 获取历史数据并计算指标...")
fetcher = BinanceDataFetcher()
df = fetcher.fetch_ohlcv('BTC', '1h', start_date='2025-10-01', end_date='2026-03-01', use_cache=False)
df['rsi'] = ta.rsi(df['close'], length=14)
macd = ta.macd(df['close'])
if macd is not None and not macd.empty:
df['macd_hist'] = macd.iloc[:, 1]
# ========== 步骤3: 本地预处理与预测 ==========
print("\n[3/4] 运行本地预测 & 构建 API 任务队列...")
# DLinear预处理
df_dl = df.copy()
df_dl['rsi'] = ta.rsi(df_dl['close'], length=14)
macd_dl = ta.macd(df_dl['close'])
if macd_dl is not None and not macd_dl.empty:
df_dl['macd'], df_dl['macd_hist'], df_dl['macd_signal'] = macd_dl.iloc[:, 0], macd_dl.iloc[:, 1], macd_dl.iloc[:, 2]
df_dl = df_dl.dropna().reset_index(drop=True)
scaled_dl = dlinear_preprocessor.scaler.transform(df_dl[['open', 'high', 'low', 'close', 'volume', 'rsi', 'macd', 'macd_hist', 'macd_signal']])
X_dl, dl_ts_map = [], {}
for i in range(len(scaled_dl) - dlinear_preprocessor.lookback):
X_dl.append(scaled_dl[i:i + dlinear_preprocessor.lookback])
dl_ts_map[df_dl.iloc[i + dlinear_preprocessor.lookback - 1]['timestamp']] = i
X_dl = np.array(X_dl)
# N-BEATS预处理
X_nb, df_nb, idx_nb = nbeats_preprocessor.preprocess(df, fit=False)
# TTM预处理
scaler_ttm = StandardScaler()
scaled_close_ttm = scaler_ttm.fit_transform(df[['close']])
X_ttm, idx_ttm_map = [], {}
for i in range(len(scaled_close_ttm) - 512):
X_ttm.append(scaled_close_ttm[i:i+512])
idx_ttm_map[i + 511] = i
X_ttm = np.array(X_ttm)
target_targets = []
for target_idx in range(1, len(df)):
target_time_bj = utc_to_beijing(df.iloc[target_idx]['timestamp'])
if target_time_bj.year == 2026 and target_time_bj.month == 2: # 锁定 2026年 2月 数据
target_targets.append(target_idx)
temp_results = []
deepseek_tasks_data = []
for target_idx in target_targets:
input_idx = target_idx - 1
current_close, target_close = df.iloc[input_idx]['close'], df.iloc[target_idx]['close']
actual_label = 1 if target_close > current_close else 0
# DL
dl_pred, input_ts = None, df.iloc[input_idx]['timestamp']
if (dl_i := dl_ts_map.get(input_ts)) is not None and dl_i < len(X_dl):
with torch.no_grad():
logits = dlinear_model(torch.FloatTensor(X_dl[dl_i:dl_i+1]).to(device)).cpu().numpy()
dl_pred = int((1 / (1 + np.exp(-logits))).item() > 0.5)
# NB
nb_pred = None
if input_idx in idx_nb:
nb_i = idx_nb.index(input_idx)
with torch.no_grad():
nb_out = nbeats_model(torch.FloatTensor(X_nb[nb_i:nb_i+1]).to(device)).cpu().numpy()
nb_pred = 1 if (nb_out[0] if nb_out.ndim == 1 else nb_out[0, 0]) > X_nb[nb_i, -1] else 0
# TTM
ttm_pred = None
if (ttm_i := idx_ttm_map.get(input_idx)) is not None:
with torch.no_grad():
ttm_pred_scaled = ttm_model(torch.FloatTensor(X_ttm[ttm_i:ttm_i+1]).to(device)).prediction_outputs[0, 0, 0].cpu().numpy()
ttm_pred = 1 if ttm_pred_scaled > X_ttm[ttm_i, -1, 0] else 0
# 构造 DeepSeek Payload
if ds_api_key:
recent_k = df.iloc[max(0, input_idx-14) : input_idx+1]
if len(recent_k) >= 5:
kline_text = "".join([f"时间: {row['timestamp']}, 开盘: {row['open']:.2f}, 收盘: {row['close']:.2f}, RSI: {row.get('rsi', 0):.2f}\n" for _, row in recent_k.iterrows()])
sys_prompt = '你是一位顶级量化分析师。请分析BTC 1小时K线,预测下一小时涨跌。必须输出合法的JSON格式:{"prediction": 1(看涨)或0(看跌), "reason": "理由"}'
messages = [
{"role": "system", "content": sys_prompt},
{"role": "user", "content": f"近15周期数据:\n{kline_text}\n输出JSON:"}
]
deepseek_tasks_data.append({'id': target_idx, 'messages': messages})
temp_results.append({
'id': target_idx, 'time_utc': df.iloc[target_idx]['timestamp'], 'time_bj': utc_to_beijing(df.iloc[target_idx]['timestamp']),
'current_close': current_close, 'target_close': target_close, 'actual': actual_label,
'dlinear': dl_pred, 'nbeats': nb_pred, 'ttm': ttm_pred, 'deepseek': None
})
# ========== 异步执行 DeepSeek 并合入数据 ==========
if deepseek_tasks_data:
# 并发数设为 5,对 NVIDIA 免费/基础层级比较安全
ds_predictions = asyncio.run(run_all_deepseek_async(deepseek_tasks_data, ds_api_key))
for res in temp_results:
if res['id'] in ds_predictions:
res['deepseek'] = ds_predictions[res['id']]
# 结算集成模型结果 (四投票)
final_results = []
for res in temp_results:
votes = [v for v in [res['dlinear'], res['nbeats'], res['ttm'], res['deepseek']] if v is not None]
if votes:
vote_sum = sum(votes)
if vote_sum > len(votes) / 2: res['ensemble'] = 1
elif vote_sum < len(votes) / 2: res['ensemble'] = 0
else: res['ensemble'] = res['dlinear'] if res['dlinear'] is not None else 0
else:
res['ensemble'] = None
final_results.append(res)
# ========== 步骤4: 输出结果 ==========
if not final_results:
print("没有找到符合条件的测试样本")
return
df_res = pd.DataFrame(final_results)
df_res['date_bj'] = df_res['time_bj'].dt.date
df_res['us_open'] = df_res['time_utc'].apply(is_us_stock_open_by_utc)
# === (下方打印逻辑与原版一致,缩略优化版) ===
print(f"\n[4/4] 准确率统计 (北京时间)\n总样本数: {len(df_res)}")
print("\n" + "=" * 115 + "\n每小时详细数据\n" + "=" * 115)
hourly_h = f"{'北京时间':<18} {'前收盘':>10} {'现收盘':>10} {'实际':>4} {'DL':>3} {'':>1} {'NB':>3} {'':>1} {'TTM':>3} {'':>1} {'DS':>3} {'':>1} {'集成':>4} {'':<1}"
for date_bj, day_group in df_res.groupby('date_bj'):
us_str = "美股开盘" if day_group['us_open'].iloc[0] else "美股休市"
def s_acc(c): return (day_group.dropna(subset=[c])[c] == day_group.dropna(subset=[c])['actual']).mean() if len(day_group.dropna(subset=[c])) > 0 else None
print(f"\n--- {date_bj} ({us_str}) --- DL:{s_acc('dlinear'):.1%} | NB:{s_acc('nbeats'):.1%} | TTM:{s_acc('ttm'):.1%} | DS:{s_acc('deepseek'):.1%} | 集成:{s_acc('ensemble'):.1%}")
print(hourly_h + "\n" + "-" * 115)
for _, r in day_group.iterrows():
print(f"{r['time_bj'].strftime('%m-%d %H:%M'):<18} {r['current_close']:>10.2f} {r['target_close']:>10.2f} {fmt_pred(r['actual']):>4} "
f"{fmt_pred(r['dlinear']):>3} {fmt_result(r['dlinear'],r['actual']):>1} {fmt_pred(r['nbeats']):>3} {fmt_result(r['nbeats'],r['actual']):>1} "
f"{fmt_pred(r['ttm']):>3} {fmt_result(r['ttm'],r['actual']):>1} {fmt_pred(r['deepseek']):>3} {fmt_result(r['deepseek'],r['actual']):>1} "
f"{fmt_pred(r['ensemble']):>4} {fmt_result(r['ensemble'],r['actual']):<1}")
print("\n\n" + "=" * 115 + "\n每日准确率汇总 (北京时间)\n" + "=" * 115)
print(f"{'日期':<14} {'样本':>4} {'美股':>6} {'DLinear':>8} {'N-BEATS':>8} {'TTM-R2':>8} {'DeepSeek':>8} {'集成':>10}\n" + "-" * 115)
for date_bj, g in df_res.groupby('date_bj'):
def g_acc(c): return (g.dropna(subset=[c])[c] == g.dropna(subset=[c])['actual']).mean() if len(g.dropna(subset=[c])) > 0 else None
print(f"{str(date_bj):<14} {len(g):>4} {'开盘' if g['us_open'].iloc[0] else '休市':>6} {g_acc('dlinear'):>8.1%} {g_acc('nbeats'):>8.1%} {g_acc('ttm'):>8.1%} {g_acc('deepseek'):>8.1%} {g_acc('ensemble'):>10.1%}".replace('nan%', 'N/A'))
def t_acc(c): return (df_res.dropna(subset=[c])[c] == df_res.dropna(subset=[c])['actual']).mean() if len(df_res.dropna(subset=[c])) > 0 else None
print("-" * 115)
print(f"{'总计':<14} {len(df_res):>4} {'':>6} {t_acc('dlinear'):>8.2%} {t_acc('nbeats'):>8.2%} {t_acc('ttm'):>8.2%} {t_acc('deepseek'):>8.2%} {t_acc('ensemble'):>10.2%}".replace('nan%', 'N/A'))
print("=" * 115)
if __name__ == "__main__":
main()
+7 -1
View File
@@ -34,6 +34,7 @@ from data.fetcher import BinanceDataFetcher
from data.trading_db import TradingDatabase from data.trading_db import TradingDatabase
from api.models import PredictionModel from api.models import PredictionModel
from models.inference import get_inference_manager from models.inference import get_inference_manager
from utils.send_msg import lark_notifier
load_dotenv() load_dotenv()
@@ -298,6 +299,7 @@ class PolymarketBTCTrader:
def get_market_by_slug(self, slug: str) -> Optional[Dict]: def get_market_by_slug(self, slug: str) -> Optional[Dict]:
"""通过slug获取市场信息""" """通过slug获取市场信息"""
for attempt in range(3):
try: try:
response = self.http.get( response = self.http.get(
f"{self.GAMMA_API}/events", f"{self.GAMMA_API}/events",
@@ -347,7 +349,11 @@ class PolymarketBTCTrader:
'closed': market.get('closed', False), 'closed': market.get('closed', False),
} }
except Exception as e: except Exception as e:
logger.error(f"获取市场失败: {e}") logger.warning(f"尝试 {attempt+1}/3 获取市场失败: {e}")
if attempt < 2:
time.sleep(5) # 等待5秒后重试
lark_notifier.send_alert(f"Slug: {slug}","连续3次请求或解析发生异常,已放弃重试")
return None return None
def get_current_hourly_market(self) -> Optional[Dict]: def get_current_hourly_market(self) -> Optional[Dict]:
+38
View File
@@ -0,0 +1,38 @@
import requests
class LarkNotifier:
"""飞书消息发送工具类"""
def __init__(self, bot_name="weibo", receive_id="on_f972f5f8c4de6ac967574b97dc8c1d82"):
# 将基础配置放在初始化中,方便后续修改或通过环境变量传入
self.url = "https://zhiyinzhe.xyz/sendMsg/api/send"
self.bot_name = bot_name
self.receive_id = receive_id
# 使用 Session 可以复用底层 TCP 连接,提升发送效率
self.session = requests.Session()
def send_alert(self, title: str, content: str):
"""
发送通用文本告警
:param title: 告警标题 (如 "⚠️ 市场获取失败")
:param content: 告警具体内容 (如 "Slug: xxx \n 原因: xxx")
"""
payload = {
"bot_name": self.bot_name,
"receive_id": self.receive_id,
"receive_id_type": "union_id",
"content": f"{title}\n{content}"
}
try:
# 使用 data=payload 发送 Form Data
response = self.session.post(self.url, data=payload, timeout=5)
response.raise_for_status()
print(f"告警发送成功: {title}")
except Exception as e:
# 告警系统自身的错误只记录日志,绝不抛出异常影响主程序
print(f"发送飞书告警失败: {e}")
# 实例化一个全局单例供其他模块直接导入使用
lark_notifier = LarkNotifier()