feat: refactor model inference architecture
- Unified inference logic into models/inference.py - Added InferenceManager for handling DLinear, N-BEATS, and TTM-R2 models - Updated prediction_service.py and auto_trade_polymarket.py to use the new manager - Fixed dependency issues (torch, transformers) - Resolved logger duplication issue - Verified with test_models_feb_2026.py
This commit is contained in:
1 parent
d34b67f522
commit
c0a02868ac
14 files changed
+791
-4284
No files matched your search
@@ -0,0 +1,971 @@
|
||||
"""
|
||||
BTC Hourly 自动交易脚本
|
||||
|
||||
基于DLinear、N-BEATS、TTM-R2多模型集成预测,在Polymarket自动下单BTC小时涨跌
|
||||
支持:
|
||||
- 每小时自动寻找对应市场
|
||||
- 实时价格刷新
|
||||
- 定时下单
|
||||
- 详细日志记录
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
import argparse
|
||||
import json
|
||||
import time
|
||||
import logging
|
||||
from logging.handlers import RotatingFileHandler
|
||||
import schedule
|
||||
from datetime import datetime, timedelta
|
||||
from typing import Optional, Tuple, Dict, List
|
||||
from zoneinfo import ZoneInfo
|
||||
|
||||
import torch
|
||||
import numpy as np
|
||||
import pandas as pd
|
||||
import httpx
|
||||
from dotenv import load_dotenv
|
||||
|
||||
# 添加项目路径
|
||||
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||
|
||||
from data.fetcher import BinanceDataFetcher
|
||||
from data.trading_db import TradingDatabase
|
||||
from api.models import PredictionModel
|
||||
from models.inference import get_inference_manager
|
||||
|
||||
load_dotenv()
|
||||
|
||||
|
||||
def setup_logging(log_dir: str = "logs", log_level: int = logging.INFO) -> logging.Logger:
|
||||
"""
|
||||
配置日志系统
|
||||
|
||||
Args:
|
||||
log_dir: 日志目录
|
||||
log_level: 日志级别
|
||||
|
||||
Returns:
|
||||
配置好的logger
|
||||
"""
|
||||
# 创建日志目录
|
||||
os.makedirs(log_dir, exist_ok=True)
|
||||
|
||||
# 创建logger
|
||||
logger = logging.getLogger("polymarket_trader")
|
||||
logger.setLevel(log_level)
|
||||
logger.propagate = False
|
||||
|
||||
# 避免重复添加handler
|
||||
if logger.handlers:
|
||||
return logger
|
||||
|
||||
# 日志格式
|
||||
formatter = logging.Formatter(
|
||||
"%(asctime)s | %(levelname)-8s | %(message)s",
|
||||
datefmt="%Y-%m-%d %H:%M:%S"
|
||||
)
|
||||
|
||||
# 文件handler (按大小轮转, 最大10MB, 保留5个备份)
|
||||
log_file = os.path.join(log_dir, "polymarket_trading.log")
|
||||
file_handler = RotatingFileHandler(
|
||||
log_file,
|
||||
maxBytes=10*1024*1024, # 10MB
|
||||
backupCount=5,
|
||||
encoding='utf-8'
|
||||
)
|
||||
file_handler.setLevel(log_level)
|
||||
file_handler.setFormatter(formatter)
|
||||
logger.addHandler(file_handler)
|
||||
|
||||
# 控制台handler
|
||||
console_handler = logging.StreamHandler(sys.stdout)
|
||||
console_handler.setLevel(log_level)
|
||||
console_handler.setFormatter(formatter)
|
||||
logger.addHandler(console_handler)
|
||||
|
||||
return logger
|
||||
|
||||
|
||||
# 初始化全局logger
|
||||
logger = setup_logging()
|
||||
|
||||
|
||||
class PolymarketBTCTrader:
|
||||
"""Polymarket BTC Hourly交易器 (多模型集成版)"""
|
||||
|
||||
GAMMA_API = "https://gamma-api.polymarket.com"
|
||||
CLOB_API = "https://clob.polymarket.com"
|
||||
|
||||
# 月份名称映射
|
||||
MONTH_NAMES = {
|
||||
1: 'january', 2: 'february', 3: 'march', 4: 'april',
|
||||
5: 'may', 6: 'june', 7: 'july', 8: 'august',
|
||||
9: 'september', 10: 'october', 11: 'november', 12: 'december'
|
||||
}
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
private_key: Optional[str] = None,
|
||||
max_bet: float = 1.0,
|
||||
max_price: float = 0.50,
|
||||
dry_run: bool = True,
|
||||
):
|
||||
self.private_key = private_key or os.getenv("POLYMARKET_PRIVATE_KEY")
|
||||
self.max_bet = max_bet
|
||||
self.max_price = max_price
|
||||
self.dry_run = dry_run
|
||||
|
||||
# HTTP客户端
|
||||
proxy = os.getenv("SOCKS5_PROXY") if os.getenv("USE_PROXY", "false").lower() == "true" else None
|
||||
self.http = httpx.Client(
|
||||
timeout=30.0,
|
||||
proxy=proxy,
|
||||
headers={'Accept-Encoding': 'identity'}
|
||||
)
|
||||
|
||||
# 初始化推理管理器
|
||||
logger.info("初始化推理管理器...")
|
||||
self.inference_manager = get_inference_manager()
|
||||
self.inference_manager.load_all_models()
|
||||
self.device = self.inference_manager.device
|
||||
|
||||
# 数据获取器
|
||||
self.fetcher = BinanceDataFetcher()
|
||||
|
||||
# 数据库
|
||||
self.db = TradingDatabase()
|
||||
|
||||
# CLOB客户端(用于下单)
|
||||
self.clob_client = None
|
||||
if self.private_key and not self.dry_run:
|
||||
self._init_clob_client()
|
||||
|
||||
# 当前市场缓存
|
||||
self.current_market = None
|
||||
self.last_price_update = None
|
||||
|
||||
def _init_clob_client(self):
|
||||
"""初始化CLOB交易客户端"""
|
||||
try:
|
||||
from py_clob_client.client import ClobClient
|
||||
|
||||
self.clob_client = ClobClient(
|
||||
host=self.CLOB_API,
|
||||
key=self.private_key,
|
||||
chain_id=137, # Polygon
|
||||
)
|
||||
self.clob_client.set_api_creds(self.clob_client.create_or_derive_api_creds())
|
||||
logger.info("✓ CLOB客户端初始化成功")
|
||||
except Exception as e:
|
||||
logger.warning(f"⚠️ CLOB客户端初始化失败: {e}")
|
||||
self.clob_client = None
|
||||
|
||||
def _log_to_db(self, result: Dict, market: Optional[Dict]):
|
||||
"""记录交易结果到数据库"""
|
||||
try:
|
||||
# 确定状态
|
||||
status = 'failure'
|
||||
if self.dry_run:
|
||||
status = 'test'
|
||||
elif result.get('success'):
|
||||
status = 'success'
|
||||
elif result.get('error'):
|
||||
status = 'failure'
|
||||
elif result.get('prediction'): # 有预测但未交易(如市场未找到)
|
||||
status = 'in_progress'
|
||||
if result.get('error'):
|
||||
status = 'failure'
|
||||
|
||||
log_data = {
|
||||
'timestamp': result.get('timestamp'),
|
||||
'market_title': result.get('market'),
|
||||
'market_slug': market.get('slug') if market else result.get('market_slug'),
|
||||
'model_name': 'Ensemble', # 记录为集成策略
|
||||
'prediction': result.get('prediction'),
|
||||
'probability': result.get('probability'),
|
||||
'action': result.get('side'),
|
||||
'price': result.get('price'),
|
||||
'amount': result.get('amount'),
|
||||
'size': result.get('size'),
|
||||
'status': status,
|
||||
'error_message': result.get('error'),
|
||||
'raw_data': result
|
||||
}
|
||||
self.db.log_trade(log_data)
|
||||
logger.info(f"💾 交易记录已保存到数据库")
|
||||
except Exception as e:
|
||||
logger.error(f"❌ 保存数据库失败: {e}")
|
||||
|
||||
def generate_market_slug(self, target_time: Optional[datetime] = None) -> str:
|
||||
"""
|
||||
生成市场slug
|
||||
|
||||
格式: bitcoin-up-or-down-{month}-{day}-{hour}am/pm-et
|
||||
例如: bitcoin-up-or-down-february-3-8am-et
|
||||
"""
|
||||
# 获取美东时间
|
||||
et_tz = ZoneInfo('America/New_York')
|
||||
|
||||
if target_time is None:
|
||||
now_et = datetime.now(et_tz)
|
||||
target_time = now_et.replace(minute=0, second=0, microsecond=0)
|
||||
|
||||
month_name = self.MONTH_NAMES[target_time.month]
|
||||
day = target_time.day
|
||||
|
||||
# 转换为12小时制
|
||||
hour_24 = target_time.hour
|
||||
if hour_24 == 0:
|
||||
hour_12 = 12
|
||||
am_pm = 'am'
|
||||
elif hour_24 < 12:
|
||||
hour_12 = hour_24
|
||||
am_pm = 'am'
|
||||
elif hour_24 == 12:
|
||||
hour_12 = 12
|
||||
am_pm = 'pm'
|
||||
else:
|
||||
hour_12 = hour_24 - 12
|
||||
am_pm = 'pm'
|
||||
|
||||
slug = f"bitcoin-up-or-down-{month_name}-{day}-{hour_12}{am_pm}-et"
|
||||
return slug
|
||||
|
||||
def get_market_by_slug(self, slug: str) -> Optional[Dict]:
|
||||
"""通过slug获取市场信息"""
|
||||
try:
|
||||
response = self.http.get(
|
||||
f"{self.GAMMA_API}/events",
|
||||
params={'slug': slug}
|
||||
)
|
||||
response.raise_for_status()
|
||||
events = response.json()
|
||||
|
||||
if not events:
|
||||
return None
|
||||
|
||||
event = events[0]
|
||||
markets = event.get('markets', [])
|
||||
|
||||
if not markets:
|
||||
return None
|
||||
|
||||
market = markets[0]
|
||||
|
||||
# outcomePrices可能是JSON字符串或列表
|
||||
outcome_prices_raw = market.get('outcomePrices', ['0', '0'])
|
||||
if isinstance(outcome_prices_raw, str):
|
||||
import json as _json
|
||||
try:
|
||||
outcome_prices_raw = _json.loads(outcome_prices_raw)
|
||||
except:
|
||||
outcome_prices_raw = ['0', '0']
|
||||
|
||||
# clobTokenIds也可能是JSON字符串
|
||||
clob_tokens_raw = market.get('clobTokenIds', [])
|
||||
if isinstance(clob_tokens_raw, str):
|
||||
import json as _json
|
||||
try:
|
||||
clob_tokens_raw = _json.loads(clob_tokens_raw)
|
||||
except:
|
||||
clob_tokens_raw = []
|
||||
|
||||
return {
|
||||
'title': event.get('title', ''),
|
||||
'slug': slug,
|
||||
'question': market.get('question', ''),
|
||||
'condition_id': market.get('conditionId', ''),
|
||||
'outcomes': market.get('outcomes', []),
|
||||
'outcome_prices': [float(p) for p in outcome_prices_raw],
|
||||
'tokens': clob_tokens_raw,
|
||||
'end_date': event.get('endDate', ''),
|
||||
'closed': market.get('closed', False),
|
||||
}
|
||||
except Exception as e:
|
||||
logger.error(f"获取市场失败: {e}")
|
||||
return None
|
||||
|
||||
def get_current_hourly_market(self) -> Optional[Dict]:
|
||||
"""获取BTC市场"""
|
||||
slug = self.generate_market_slug()
|
||||
logger.info(f"搜索市场: {slug}")
|
||||
|
||||
market = self.get_market_by_slug(slug)
|
||||
|
||||
if market:
|
||||
self.current_market = market
|
||||
self.last_price_update = datetime.now()
|
||||
logger.info(f"✓ 找到市场: {market['title']}")
|
||||
logger.info(f" Up价格: ${market['outcome_prices'][0]:.3f}")
|
||||
logger.info(f" Down价格: ${market['outcome_prices'][1]:.3f}")
|
||||
else:
|
||||
logger.warning(f"❌ 未找到市场: {slug}")
|
||||
|
||||
return market
|
||||
|
||||
def refresh_prices(self) -> Optional[Dict]:
|
||||
"""刷新当前市场价格"""
|
||||
if not self.current_market:
|
||||
self.get_current_hourly_market()
|
||||
|
||||
# 使用CLOB API获取实时价格
|
||||
tokens = self.current_market.get('tokens', [])
|
||||
if len(tokens) >= 2:
|
||||
realtime_prices = self.get_realtime_prices(tokens[0], tokens[1])
|
||||
if realtime_prices:
|
||||
old_prices = self.current_market['outcome_prices']
|
||||
new_prices = realtime_prices
|
||||
|
||||
self.current_market['outcome_prices'] = new_prices
|
||||
self.last_price_update = datetime.now()
|
||||
|
||||
# 显示价格变化
|
||||
up_change = new_prices[0] - old_prices[0]
|
||||
down_change = new_prices[1] - old_prices[1]
|
||||
|
||||
logger.info(f"📊 实时价格 [{datetime.now().strftime('%H:%M:%S')}]")
|
||||
logger.info(f" Up: ${new_prices[0]:.3f} ({up_change:+.3f})")
|
||||
logger.info(f" Down: ${new_prices[1]:.3f} ({down_change:+.3f})")
|
||||
|
||||
return self.current_market
|
||||
|
||||
# 备用: 使用Gamma API
|
||||
slug = self.current_market['slug']
|
||||
market = self.get_market_by_slug(slug)
|
||||
|
||||
if market:
|
||||
old_prices = self.current_market['outcome_prices']
|
||||
new_prices = market['outcome_prices']
|
||||
|
||||
self.current_market = market
|
||||
self.last_price_update = datetime.now()
|
||||
|
||||
# 显示价格变化
|
||||
up_change = new_prices[0] - old_prices[0]
|
||||
down_change = new_prices[1] - old_prices[1]
|
||||
|
||||
logger.info(f"📊 价格更新(Gamma) [{datetime.now().strftime('%H:%M:%S')}]")
|
||||
logger.info(f" Up: ${new_prices[0]:.3f} ({up_change:+.3f})")
|
||||
logger.info(f" Down: ${new_prices[1]:.3f} ({down_change:+.3f})")
|
||||
|
||||
return market
|
||||
|
||||
def get_realtime_prices(self, up_token_id: str, down_token_id: str) -> Optional[list]:
|
||||
"""从CLOB API获取实时买入价格"""
|
||||
try:
|
||||
# 使用/price端点获取买入价格
|
||||
up_response = self.http.get(
|
||||
f"{self.CLOB_API}/price",
|
||||
params={'token_id': up_token_id, 'side': 'BUY'}
|
||||
)
|
||||
up_response.raise_for_status()
|
||||
up_data = up_response.json()
|
||||
|
||||
down_response = self.http.get(
|
||||
f"{self.CLOB_API}/price",
|
||||
params={'token_id': down_token_id, 'side': 'BUY'}
|
||||
)
|
||||
down_response.raise_for_status()
|
||||
down_data = down_response.json()
|
||||
|
||||
# 解析买入价格
|
||||
up_price = float(up_data.get('price', 0.5))
|
||||
down_price = float(down_data.get('price', 0.5))
|
||||
|
||||
return [up_price, down_price]
|
||||
except Exception as e:
|
||||
logger.debug(f"获取实时价格失败: {e}")
|
||||
return None
|
||||
|
||||
def execute_trade(self, prediction: int, market: Dict) -> Dict:
|
||||
"""执行交易"""
|
||||
result = {
|
||||
'timestamp': datetime.now().isoformat(),
|
||||
'market': market['title'],
|
||||
'prediction': 'UP' if prediction == 1 else 'DOWN',
|
||||
'success': False,
|
||||
'order': None,
|
||||
'error': None,
|
||||
}
|
||||
|
||||
if prediction == 1: # 预测涨 -> 买UP
|
||||
token_id = market['tokens'][0]
|
||||
price = market['outcome_prices'][0]
|
||||
side = "UP"
|
||||
else: # 预测跌 -> 买DOWN
|
||||
token_id = market['tokens'][1]
|
||||
price = market['outcome_prices'][1]
|
||||
side = "DOWN"
|
||||
|
||||
result['side'] = side
|
||||
result['price'] = price
|
||||
|
||||
# 检查价格限制
|
||||
if price > self.max_price:
|
||||
result['error'] = f"价格${price:.2f}超过限制${self.max_price:.2f}"
|
||||
logger.error(f"❌ {result['error']}")
|
||||
return result
|
||||
|
||||
# 计算数量
|
||||
size = self.max_bet / price
|
||||
result['size'] = size
|
||||
result['amount'] = self.max_bet
|
||||
|
||||
logger.info("📋 交易详情:")
|
||||
logger.info(f" 方向: {side}")
|
||||
logger.info(f" 价格: ${price:.3f}")
|
||||
logger.info(f" 金额: ${self.max_bet:.2f} USDC")
|
||||
logger.info(f" 数量: {size:.2f} 股")
|
||||
|
||||
if self.dry_run:
|
||||
logger.info("🔸 [模拟模式] 未实际下单")
|
||||
result['success'] = True
|
||||
result['order'] = 'DRY_RUN'
|
||||
return result
|
||||
|
||||
# 实际下单
|
||||
if not self.clob_client:
|
||||
result['error'] = "CLOB客户端未初始化"
|
||||
logger.error(f"❌ {result['error']}")
|
||||
return result
|
||||
|
||||
try:
|
||||
from py_clob_client.order_builder.constants import BUY
|
||||
|
||||
order = self.clob_client.create_limit_order(
|
||||
token_id=token_id,
|
||||
price=price,
|
||||
size=size,
|
||||
side=BUY,
|
||||
)
|
||||
|
||||
response = self.clob_client.post_order(order)
|
||||
logger.info(f"✓ 订单已提交: {response}")
|
||||
result['success'] = True
|
||||
result['order'] = response
|
||||
except Exception as e:
|
||||
result['error'] = str(e)
|
||||
logger.error(f"❌ 下单失败: {e}")
|
||||
|
||||
return result
|
||||
|
||||
def get_prediction_with_delay(self, delay_seconds: float = 2.0) -> Tuple[int, float]:
|
||||
"""
|
||||
延迟后获取K线并预测当前小时的涨跌 (多模型)
|
||||
"""
|
||||
logger.info(f"⏳ 等待{delay_seconds}秒获取最新K线...")
|
||||
time.sleep(delay_seconds)
|
||||
|
||||
logger.info("获取最新BTC数据...")
|
||||
|
||||
end_date = (datetime.now() + timedelta(days=1)).strftime('%Y-%m-%d')
|
||||
start_date = (datetime.now() - timedelta(days=30)).strftime('%Y-%m-%d')
|
||||
|
||||
df = self.fetcher.fetch_ohlcv(
|
||||
symbol='BTC',
|
||||
timeframe='1h',
|
||||
start_date=start_date,
|
||||
end_date=end_date,
|
||||
use_cache=False # 不使用缓存,获取最新数据
|
||||
)
|
||||
|
||||
# 使用倒数第二根K线(上一小时收盘)进行预测
|
||||
prev_time = df['timestamp'].iloc[-2]
|
||||
prev_close = df['close'].iloc[-2]
|
||||
current_time = df['timestamp'].iloc[-1]
|
||||
current_close = df['close'].iloc[-1]
|
||||
|
||||
logger.info(f" 上一K线时间: {prev_time} (用于预测)")
|
||||
logger.info(f" 上一收盘价: ${prev_close:,.2f}")
|
||||
logger.info(f" 当前K线时间: {current_time} (预测目标)")
|
||||
logger.info(f" 当前收盘价: ${current_close:,.2f} (未收盘)")
|
||||
|
||||
# 回填上一小时的实际结果 (多模型)
|
||||
self._update_previous_prediction(df)
|
||||
|
||||
# 用除最后一根外的数据进行预测
|
||||
df_for_prediction = df.iloc[:-1]
|
||||
|
||||
# 打印模型输入的最后一根K线数据
|
||||
last_input_row = df_for_prediction.iloc[-1]
|
||||
logger.info(f" 📌 模型输入末根K线: {last_input_row['timestamp']} "
|
||||
f"O:{last_input_row['open']:.2f} H:{last_input_row['high']:.2f} "
|
||||
f"L:{last_input_row['low']:.2f} C:{last_input_row['close']:.2f} "
|
||||
f"V:{last_input_row['volume']:.0f}")
|
||||
|
||||
# === 多模型预测 ===
|
||||
all_preds = self.inference_manager.predict_all(df_for_prediction)
|
||||
|
||||
# 解析预测结果
|
||||
final_prediction = 0
|
||||
final_probability = 0.5
|
||||
|
||||
# 1. DLinear
|
||||
if 'dlinear' in all_preds:
|
||||
dl = all_preds['dlinear']
|
||||
dl_dir = "📈 UP" if dl['prediction'] == 1 else "📉 DOWN"
|
||||
logger.info(f" [DLinear] {dl_dir} (conf: {dl['probability']:.2%})")
|
||||
|
||||
# 2. N-BEATS
|
||||
if 'nbeats' in all_preds:
|
||||
nb = all_preds['nbeats']
|
||||
nb_dir = "📈 UP" if nb['prediction'] == 1 else "📉 DOWN"
|
||||
logger.info(f" [N-BEATS] {nb_dir}")
|
||||
|
||||
# 3. TTM-R2
|
||||
if 'ttm' in all_preds:
|
||||
ttm = all_preds['ttm']
|
||||
ttm_dir = "📈 UP" if ttm['prediction'] == 1 else "📉 DOWN"
|
||||
logger.info(f" [TTM-R2] {ttm_dir} (conf: {ttm['probability']:.2%})")
|
||||
|
||||
# 4. 集成结果
|
||||
if 'ensemble' in all_preds and all_preds['ensemble']:
|
||||
ens = all_preds['ensemble']
|
||||
ens_pred = ens['prediction']
|
||||
votes = ens['votes']
|
||||
ens_dir = "📈 UP" if ens_pred == 1 else "📉 DOWN"
|
||||
vote_str = f"{sum(votes)}/{len(votes)}"
|
||||
logger.info(f" [Ensemble] {ens_dir} (Votes: {vote_str})")
|
||||
|
||||
# 使用集成结果作为最终决策
|
||||
final_prediction = ens_pred
|
||||
final_probability = 0.6 # 集成策略暂时给一个固定置信度或基于投票比例
|
||||
if len(votes) > 0:
|
||||
final_probability = sum(votes) / len(votes) if ens_pred == 1 else 1 - (sum(votes) / len(votes))
|
||||
else:
|
||||
# 降级到 DLinear
|
||||
if 'dlinear' in all_preds:
|
||||
final_prediction = all_preds['dlinear']['prediction']
|
||||
final_probability = all_preds['dlinear']['probability']
|
||||
logger.warning("⚠️ 集成预测不可用,降级为 DLinear")
|
||||
|
||||
# 保存所有预测结果到 model_predictions 表
|
||||
try:
|
||||
et_tz = ZoneInfo('America/New_York')
|
||||
current_time_utc = pd.Timestamp(current_time)
|
||||
current_time_et = current_time_utc.tz_localize('UTC').tz_convert(et_tz)
|
||||
|
||||
common_data = {
|
||||
"timestamp": current_time_utc.isoformat(),
|
||||
"timestamp_et": current_time_et.isoformat(),
|
||||
"date": current_time_et.strftime('%Y-%m-%d'),
|
||||
"hour": current_time_et.hour,
|
||||
"actual_result": None,
|
||||
"is_correct": None,
|
||||
"open_price": float(df['open'].iloc[-1]),
|
||||
"close_price": None,
|
||||
}
|
||||
|
||||
# 辅助函数
|
||||
def save_single_pred(name, pred_res):
|
||||
data = common_data.copy()
|
||||
data['model_name'] = name
|
||||
data['prediction'] = pred_res['prediction']
|
||||
data['probability'] = pred_res.get('probability', 0.5)
|
||||
PredictionModel.save_prediction(data)
|
||||
|
||||
if 'dlinear' in all_preds:
|
||||
save_single_pred("DLinear", all_preds['dlinear'])
|
||||
if 'nbeats' in all_preds:
|
||||
save_single_pred("N-BEATS", all_preds['nbeats'])
|
||||
if 'ttm' in all_preds:
|
||||
save_single_pred("TTM-R2", all_preds['ttm'])
|
||||
if 'ensemble' in all_preds and all_preds['ensemble']:
|
||||
# 集成也保存
|
||||
save_single_pred("Ensemble", all_preds['ensemble'])
|
||||
|
||||
logger.info(f"💾 所有模型预测记录已保存 (ET {current_time_et.strftime('%Y-%m-%d %H:%M')})")
|
||||
except Exception as e:
|
||||
logger.error(f"保存预测记录失败: {e}")
|
||||
|
||||
return final_prediction, final_probability
|
||||
|
||||
def _update_previous_prediction(self, df: pd.DataFrame):
|
||||
"""回填上一小时预测的实际结果 (所有模型)"""
|
||||
if len(df) < 2:
|
||||
return
|
||||
|
||||
try:
|
||||
et_tz = ZoneInfo('America/New_York')
|
||||
prev_row = df.iloc[-2]
|
||||
prev_utc = pd.Timestamp(prev_row['timestamp'])
|
||||
prev_et = prev_utc.tz_localize('UTC').tz_convert(et_tz)
|
||||
prev_date = prev_et.strftime('%Y-%m-%d')
|
||||
prev_hour = prev_et.hour
|
||||
|
||||
prev_open = float(prev_row['open'])
|
||||
prev_close = float(prev_row['close'])
|
||||
actual_result = 1 if prev_close > prev_open else 0
|
||||
|
||||
models_to_update = ["DLinear", "N-BEATS", "TTM-R2", "Ensemble"]
|
||||
|
||||
for model_name in models_to_update:
|
||||
updated = PredictionModel.update_actual_result(
|
||||
date=prev_date,
|
||||
hour=prev_hour,
|
||||
model_name=model_name,
|
||||
actual_result=actual_result,
|
||||
close_price=prev_close
|
||||
)
|
||||
if updated:
|
||||
# 仅打印一条日志代表更新成功,避免刷屏
|
||||
pass
|
||||
|
||||
logger.info(f"📝 回填上一小时结果: ET {prev_date} {prev_hour}:00, "
|
||||
f"实际{'涨' if actual_result == 1 else '跌'} (O:{prev_open:.0f} C:{prev_close:.0f})")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"回填上一小时结果失败: {e}")
|
||||
|
||||
def run_once(self) -> Dict:
|
||||
"""运行一次交易流程(不等待整点)"""
|
||||
logger.info("运行单次交易...")
|
||||
return self.run_at_hour_start(delay_seconds=0)
|
||||
|
||||
def run_at_hour_start(self, delay_seconds: float = 2.0, timeout_minutes: int = 50) -> Dict:
|
||||
"""
|
||||
整点时执行的交易流程
|
||||
"""
|
||||
logger.info("=" * 60)
|
||||
logger.info(f"⏰ 整点交易触发 - {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}")
|
||||
logger.info("=" * 60)
|
||||
logger.info(f" 模式: {'🔸 模拟' if self.dry_run else '💰 真实交易'}")
|
||||
logger.info(f" 最大下注: ${self.max_bet:.2f} USDC")
|
||||
logger.info(f" 最高价格: ${self.max_price:.2f}")
|
||||
logger.info(f" 等待超时: {timeout_minutes}分钟")
|
||||
|
||||
result = {
|
||||
'timestamp': datetime.now().isoformat(),
|
||||
'success': False,
|
||||
'error': None,
|
||||
}
|
||||
|
||||
# 1. 延迟后获取预测
|
||||
prediction, probability = self.get_prediction_with_delay(delay_seconds)
|
||||
result['prediction'] = 'UP' if prediction == 1 else 'DOWN'
|
||||
result['probability'] = probability if prediction == 1 else 1 - probability
|
||||
|
||||
# 2. 获取初始市场
|
||||
market = self.get_current_hourly_market()
|
||||
if not market:
|
||||
result['error'] = '未找到市场'
|
||||
logger.error(f"❌ {result['error']}")
|
||||
self._log_to_db(result, None)
|
||||
return result
|
||||
|
||||
result['market'] = market['title']
|
||||
result['market_slug'] = market['slug']
|
||||
|
||||
if market['closed']:
|
||||
result['error'] = '市场已关闭'
|
||||
logger.error(f"❌ {result['error']}")
|
||||
self._log_to_db(result, market)
|
||||
return result
|
||||
|
||||
# 检查是否已交易过该市场
|
||||
check_status = 'test' if self.dry_run else 'success'
|
||||
if self.db.check_existing_trade(market['slug'], status=check_status):
|
||||
result['error'] = '该市场已存在成功交易记录'
|
||||
logger.warning(f"⚠️ {result['error']}, 跳过交易")
|
||||
return result
|
||||
|
||||
# 3. 循环检查价格并交易
|
||||
start_time = datetime.now()
|
||||
end_time = start_time + timedelta(minutes=timeout_minutes)
|
||||
|
||||
logger.info(f"🔄 开始价格监控 (预测: {'UP' if prediction == 1 else 'DOWN'})...")
|
||||
|
||||
while datetime.now() < end_time:
|
||||
# 刷新价格
|
||||
market = self.refresh_prices()
|
||||
if not market:
|
||||
logger.warning("无法获取市场价格,重试中...")
|
||||
time.sleep(10)
|
||||
continue
|
||||
|
||||
# 检查价格
|
||||
price_idx = 0 if prediction == 1 else 1
|
||||
current_price = market['outcome_prices'][price_idx]
|
||||
|
||||
logger.info(f" 当前价格: ${current_price:.3f} (目标: <= ${self.max_price:.2f})")
|
||||
|
||||
if current_price <= self.max_price:
|
||||
# 价格符合,执行交易
|
||||
logger.info("✅ 价格满足条件,正在下单...")
|
||||
trade_result = self.execute_trade(prediction, market)
|
||||
result.update(trade_result)
|
||||
|
||||
# 记录数据库
|
||||
self._log_to_db(result, market)
|
||||
|
||||
logger.info("=" * 60)
|
||||
return result
|
||||
else:
|
||||
# 价格过高,等待
|
||||
wait_time = 30 # 等待30秒
|
||||
elapsed = int((datetime.now() - start_time).total_seconds())
|
||||
logger.info(f"⏳ 价格过高,继续等待... (已耗时: {elapsed}s)")
|
||||
time.sleep(wait_time)
|
||||
|
||||
# 超时
|
||||
result['error'] = f'等待价格超时 ({timeout_minutes}分钟)'
|
||||
result['price'] = market['outcome_prices'][0] if prediction == 1 else market['outcome_prices'][1]
|
||||
result['side'] = 'UP' if prediction == 1 else 'DOWN'
|
||||
logger.error(f"❌ {result['error']}")
|
||||
|
||||
self._log_to_db(result, market)
|
||||
logger.info("=" * 60)
|
||||
|
||||
return result
|
||||
|
||||
def _show_initial_prediction(self):
|
||||
"""
|
||||
启动时显示当前预测(不下单)
|
||||
"""
|
||||
logger.info("\n📊 当前预测状态:")
|
||||
|
||||
end_date = (datetime.now() + timedelta(days=1)).strftime('%Y-%m-%d')
|
||||
start_date = (datetime.now() - timedelta(days=30)).strftime('%Y-%m-%d')
|
||||
|
||||
df = self.fetcher.fetch_ohlcv(
|
||||
symbol='BTC',
|
||||
timeframe='1h',
|
||||
start_date=start_date,
|
||||
end_date=end_date,
|
||||
use_cache=False
|
||||
)
|
||||
|
||||
# 显示K线信息
|
||||
prev_time = df['timestamp'].iloc[-2]
|
||||
prev_close = df['close'].iloc[-2]
|
||||
current_time = df['timestamp'].iloc[-1]
|
||||
current_close = df['close'].iloc[-1]
|
||||
|
||||
logger.info(f" 上一K线: {prev_time} 收盘价: ${prev_close:,.2f}")
|
||||
logger.info(f" 当前K线: {current_time} 收盘价: ${current_close:,.2f}")
|
||||
|
||||
# 进行多模型预测
|
||||
df_for_prediction = df.iloc[:-1]
|
||||
all_preds = self.inference_manager.predict_all(df_for_prediction)
|
||||
|
||||
# DLinear
|
||||
if 'dlinear' in all_preds:
|
||||
dl = all_preds['dlinear']
|
||||
logger.info(f" 🤖 DLinear: {'📈 UP' if dl['prediction']==1 else '📉 DOWN'} ({dl['probability']:.1%})")
|
||||
|
||||
# N-BEATS
|
||||
if 'nbeats' in all_preds:
|
||||
nb = all_preds['nbeats']
|
||||
logger.info(f" 🤖 N-BEATS: {'📈 UP' if nb['prediction']==1 else '📉 DOWN'}")
|
||||
|
||||
# TTM-R2
|
||||
if 'ttm' in all_preds:
|
||||
ttm = all_preds['ttm']
|
||||
logger.info(f" 🤖 TTM-R2: {'📈 UP' if ttm['prediction']==1 else '📉 DOWN'} ({ttm['probability']:.1%})")
|
||||
|
||||
prediction = 0
|
||||
if 'ensemble' in all_preds and all_preds['ensemble']:
|
||||
ens = all_preds['ensemble']
|
||||
prediction = ens['prediction']
|
||||
logger.info(f" 🤖 集成结果: {'📈 UP' if prediction==1 else '📉 DOWN'}")
|
||||
|
||||
# 显示市场价格
|
||||
if self.current_market:
|
||||
up_price = self.current_market['outcome_prices'][0]
|
||||
down_price = self.current_market['outcome_prices'][1]
|
||||
if prediction == 1:
|
||||
logger.info(f" 💰 买入Up价格: ${up_price:.3f} {'✅ 可下单' if up_price <= self.max_price else '❌ 超过限价'}")
|
||||
else:
|
||||
logger.info(f" 💰 买入Down价格: ${down_price:.3f} {'✅ 可下单' if down_price <= self.max_price else '❌ 超过限价'}")
|
||||
|
||||
# 计算距下一整点时间
|
||||
now = datetime.now()
|
||||
next_hour = now.replace(minute=0, second=0, microsecond=0) + timedelta(hours=1)
|
||||
wait_minutes = int((next_hour - now).total_seconds() / 60)
|
||||
logger.info(f" ⏰ 距离下一整点: {wait_minutes}分钟")
|
||||
logger.info("")
|
||||
|
||||
|
||||
# === 新增: 保存初始预测和状态 ===
|
||||
try:
|
||||
# 确定最终预测 (用于日志)
|
||||
final_pred = 0
|
||||
if 'ensemble' in all_preds and all_preds['ensemble']:
|
||||
final_pred = all_preds['ensemble']['prediction']
|
||||
elif 'dlinear' in all_preds:
|
||||
final_pred = all_preds['dlinear']['prediction']
|
||||
|
||||
et_tz = ZoneInfo('America/New_York')
|
||||
current_time_utc = pd.Timestamp(current_time)
|
||||
current_time_et = current_time_utc.tz_localize('UTC').tz_convert(et_tz)
|
||||
|
||||
# 1. 保存预测到 model_predictions
|
||||
common_data = {
|
||||
"timestamp": current_time_utc.isoformat(),
|
||||
"timestamp_et": current_time_et.isoformat(),
|
||||
"date": current_time_et.strftime('%Y-%m-%d'),
|
||||
"hour": current_time_et.hour,
|
||||
"actual_result": None,
|
||||
"is_correct": None,
|
||||
"open_price": float(df['open'].iloc[-1]),
|
||||
"close_price": None,
|
||||
}
|
||||
|
||||
def save_single_pred(name, pred_res):
|
||||
data = common_data.copy()
|
||||
data['model_name'] = name
|
||||
data['prediction'] = pred_res['prediction']
|
||||
data['probability'] = pred_res.get('probability', 0.5)
|
||||
PredictionModel.save_prediction(data)
|
||||
|
||||
if 'dlinear' in all_preds: save_single_pred("DLinear", all_preds['dlinear'])
|
||||
if 'nbeats' in all_preds: save_single_pred("N-BEATS", all_preds['nbeats'])
|
||||
if 'ttm' in all_preds: save_single_pred("TTM-R2", all_preds['ttm'])
|
||||
if 'ensemble' in all_preds and all_preds['ensemble']: save_single_pred("Ensemble", all_preds['ensemble'])
|
||||
|
||||
logger.info(f"💾 [Init] 初始预测已保存到数据库")
|
||||
|
||||
# 2. 记录当前监控状态到 trades (作为 'in_progress')
|
||||
status = 'in_progress'
|
||||
market_title = self.current_market['title'] if self.current_market else "Waiting for Market..."
|
||||
market_slug = self.current_market['slug'] if self.current_market else None
|
||||
|
||||
log_data = {
|
||||
'timestamp': datetime.now().isoformat(),
|
||||
'market_title': market_title,
|
||||
'market_slug': market_slug,
|
||||
'model_name': 'Ensemble',
|
||||
'prediction': 'UP' if final_pred == 1 else 'DOWN',
|
||||
'probability': 0.0,
|
||||
'action': 'MONITOR',
|
||||
'price': 0,
|
||||
'amount': 0,
|
||||
'size': 0,
|
||||
'status': status,
|
||||
'error_message': 'System Started - Monitoring',
|
||||
'raw_data': {}
|
||||
}
|
||||
self.db.log_trade(log_data)
|
||||
logger.info(f"💾 [Init] 初始监控状态已记录")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"保存初始状态失败: {e}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
|
||||
def wait_for_next_hour(self) -> datetime:
|
||||
"""等待到下一个整点"""
|
||||
now = datetime.now()
|
||||
next_hour = now.replace(minute=0, second=0, microsecond=0) + timedelta(hours=1)
|
||||
wait_seconds = (next_hour - now).total_seconds()
|
||||
|
||||
logger.info(f"⏳ 等待下一个整点: {next_hour.strftime('%H:%M:%S')}")
|
||||
logger.info(f" 剩余等待: {int(wait_seconds)}秒")
|
||||
|
||||
return next_hour
|
||||
|
||||
def run_scheduler(self, interval_minutes: int = 5, delay_seconds: float = 2.0):
|
||||
"""定时运行交易器"""
|
||||
logger.info("=" * 60)
|
||||
logger.info("🤖 BTC Hourly自动交易 (集成版) 已启动")
|
||||
logger.info("=" * 60)
|
||||
logger.info(f" 模式: {'🔸 模拟' if self.dry_run else '💰 真实交易'}")
|
||||
logger.info(f" 下单时机: 每个整点后延迟{delay_seconds}秒")
|
||||
logger.info(f" 价格刷新: 每{interval_minutes}分钟")
|
||||
logger.info(f" 最大下注: ${self.max_bet:.2f} USDC")
|
||||
logger.info(f" 最高价格: ${self.max_price:.2f}")
|
||||
logger.info(" 按 Ctrl+C 停止")
|
||||
logger.info("=" * 60)
|
||||
|
||||
# 立即获取整点的市场
|
||||
self.get_current_hourly_market()
|
||||
|
||||
# 启动时进行一次预测(不下单,仅显示)
|
||||
self._show_initial_prediction()
|
||||
|
||||
# 每小时整点后执行交易
|
||||
def hourly_trade():
|
||||
self.run_at_hour_start(delay_seconds=delay_seconds)
|
||||
|
||||
schedule.every().hour.at(":00").do(lambda: time.sleep(delay_seconds) or hourly_trade())
|
||||
|
||||
try:
|
||||
while True:
|
||||
schedule.run_pending()
|
||||
time.sleep(1)
|
||||
except KeyboardInterrupt:
|
||||
logger.info("✓ 交易器已停止")
|
||||
|
||||
def close(self):
|
||||
"""关闭资源"""
|
||||
self.http.close()
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description='BTC Hourly自动交易 - 基于DLinear、N-BEATS、TTM-R2集成模型')
|
||||
parser.add_argument('--dry-run', action='store_true', default=True,
|
||||
help='模拟模式(不实际下单)')
|
||||
parser.add_argument('--real', action='store_true',
|
||||
help='真实交易模式')
|
||||
parser.add_argument('--max-bet', type=float, default=1.0,
|
||||
help='最大下注金额(USDC)')
|
||||
parser.add_argument('--max-price', type=float, default=0.50,
|
||||
help='最高可接受价格')
|
||||
parser.add_argument('--once', action='store_true',
|
||||
help='只执行一次(不启动定时器)')
|
||||
parser.add_argument('--interval', type=int, default=1,
|
||||
help='价格刷新间隔(分钟)')
|
||||
parser.add_argument('--check-market', action='store_true',
|
||||
help='只检查当前市场信息')
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
dry_run = not args.real
|
||||
|
||||
# 真实交易确认
|
||||
if not dry_run:
|
||||
logger.warning("⚠️ 警告: 即将进行真实交易!")
|
||||
logger.warning(f" 最大下注: ${args.max_bet:.2f} USDC")
|
||||
confirm = input("确认继续? (yes/no): ")
|
||||
if confirm.lower() != 'yes':
|
||||
logger.info("已取消")
|
||||
return
|
||||
|
||||
trader = PolymarketBTCTrader(
|
||||
max_bet=args.max_bet,
|
||||
max_price=args.max_price,
|
||||
dry_run=dry_run,
|
||||
)
|
||||
|
||||
try:
|
||||
if args.check_market:
|
||||
# 只检查市场
|
||||
trader.get_current_hourly_market()
|
||||
elif args.once:
|
||||
# 执行一次
|
||||
result = trader.run_once()
|
||||
|
||||
# 保存日志
|
||||
log_file = 'trading_log.json'
|
||||
logs = []
|
||||
if os.path.exists(log_file):
|
||||
with open(log_file, 'r') as f:
|
||||
try:
|
||||
logs = json.load(f)
|
||||
except:
|
||||
pass
|
||||
logs.append(result)
|
||||
with open(log_file, 'w') as f:
|
||||
json.dump(logs, f, indent=2, ensure_ascii=False)
|
||||
logger.info(f"结果已保存到 {log_file}")
|
||||
else:
|
||||
# 定时运行
|
||||
trader.run_scheduler(interval_minutes=args.interval)
|
||||
finally:
|
||||
trader.close()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in new issue
Block a user