chore: staged changes
This commit is contained in:
1 parent
1d732844e3
commit
e7e01516ba
4 files changed
+473
-43
No files matched your search
@@ -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} 的数据")
|
||||||
|
|||||||
@@ -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()
|
||||||
@@ -34,10 +34,11 @@ 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()
|
||||||
|
|
||||||
|
|
||||||
def setup_logging(log_dir: str = "logs", log_level: int = logging.INFO) -> logging.Logger:
|
def setup_logging(log_dir: str = "logs", log_level: int = logging.INFO) -> logging.Logger:
|
||||||
"""
|
"""
|
||||||
配置日志系统
|
配置日志系统
|
||||||
@@ -298,57 +299,62 @@ class PolymarketBTCTrader:
|
|||||||
|
|
||||||
def get_market_by_slug(self, slug: str) -> Optional[Dict]:
|
def get_market_by_slug(self, slug: str) -> Optional[Dict]:
|
||||||
"""通过slug获取市场信息"""
|
"""通过slug获取市场信息"""
|
||||||
try:
|
for attempt in range(3):
|
||||||
response = self.http.get(
|
try:
|
||||||
f"{self.GAMMA_API}/events",
|
response = self.http.get(
|
||||||
params={'slug': slug}
|
f"{self.GAMMA_API}/events",
|
||||||
)
|
params={'slug': slug}
|
||||||
response.raise_for_status()
|
)
|
||||||
events = response.json()
|
response.raise_for_status()
|
||||||
|
events = response.json()
|
||||||
|
|
||||||
if not events:
|
if not events:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
event = events[0]
|
event = events[0]
|
||||||
markets = event.get('markets', [])
|
markets = event.get('markets', [])
|
||||||
|
|
||||||
if not markets:
|
if not markets:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
market = markets[0]
|
market = markets[0]
|
||||||
|
|
||||||
# outcomePrices可能是JSON字符串或列表
|
# outcomePrices可能是JSON字符串或列表
|
||||||
outcome_prices_raw = market.get('outcomePrices', ['0', '0'])
|
outcome_prices_raw = market.get('outcomePrices', ['0', '0'])
|
||||||
if isinstance(outcome_prices_raw, str):
|
if isinstance(outcome_prices_raw, str):
|
||||||
import json as _json
|
import json as _json
|
||||||
try:
|
try:
|
||||||
outcome_prices_raw = _json.loads(outcome_prices_raw)
|
outcome_prices_raw = _json.loads(outcome_prices_raw)
|
||||||
except:
|
except:
|
||||||
outcome_prices_raw = ['0', '0']
|
outcome_prices_raw = ['0', '0']
|
||||||
|
|
||||||
# clobTokenIds也可能是JSON字符串
|
# clobTokenIds也可能是JSON字符串
|
||||||
clob_tokens_raw = market.get('clobTokenIds', [])
|
clob_tokens_raw = market.get('clobTokenIds', [])
|
||||||
if isinstance(clob_tokens_raw, str):
|
if isinstance(clob_tokens_raw, str):
|
||||||
import json as _json
|
import json as _json
|
||||||
try:
|
try:
|
||||||
clob_tokens_raw = _json.loads(clob_tokens_raw)
|
clob_tokens_raw = _json.loads(clob_tokens_raw)
|
||||||
except:
|
except:
|
||||||
clob_tokens_raw = []
|
clob_tokens_raw = []
|
||||||
|
|
||||||
return {
|
return {
|
||||||
'title': event.get('title', ''),
|
'title': event.get('title', ''),
|
||||||
'slug': slug,
|
'slug': slug,
|
||||||
'question': market.get('question', ''),
|
'question': market.get('question', ''),
|
||||||
'condition_id': market.get('conditionId', ''),
|
'condition_id': market.get('conditionId', ''),
|
||||||
'outcomes': market.get('outcomes', []),
|
'outcomes': market.get('outcomes', []),
|
||||||
'outcome_prices': [float(p) for p in outcome_prices_raw],
|
'outcome_prices': [float(p) for p in outcome_prices_raw],
|
||||||
'tokens': clob_tokens_raw,
|
'tokens': clob_tokens_raw,
|
||||||
'end_date': event.get('endDate', ''),
|
'end_date': event.get('endDate', ''),
|
||||||
'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}")
|
||||||
return None
|
if attempt < 2:
|
||||||
|
time.sleep(5) # 等待5秒后重试
|
||||||
|
|
||||||
|
lark_notifier.send_alert(f"Slug: {slug}","连续3次请求或解析发生异常,已放弃重试")
|
||||||
|
return None
|
||||||
|
|
||||||
def get_current_hourly_market(self) -> Optional[Dict]:
|
def get_current_hourly_market(self) -> Optional[Dict]:
|
||||||
"""获取BTC市场"""
|
"""获取BTC市场"""
|
||||||
|
|||||||
@@ -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()
|
||||||
Reference in new issue
Block a user