接入数据库

This commit is contained in:
crypto_predictor committed 2026-02-04 18:35:10 +08:00
1 parent 1bd9d25f60
commit 59fcb11ffa
9 files changed
+1558 -101

No files matched your search

+1
View File
@@ -0,0 +1 @@
# api模块初始化
+390
View File
@@ -0,0 +1,390 @@
"""
Polymarket API 客户端
用于与Polymarket CLOB API交互,支持:
- 搜索BTC相关市场
- 获取订单簿和价格
- 下单交易
"""
import os
import httpx
from typing import Optional, Dict, List, Any
from dataclasses import dataclass
from dotenv import load_dotenv
# 加载环境变量
load_dotenv()
@dataclass
class MarketInfo:
"""市场信息"""
condition_id: str
question: str
outcomes: List[str]
outcome_prices: List[float]
tokens: List[Dict]
volume: float
liquidity: float
end_date: str
class PolymarketClient:
"""Polymarket API客户端"""
GAMMA_API = "https://gamma-api.polymarket.com"
CLOB_API = "https://clob.polymarket.com"
def __init__(self, private_key: Optional[str] = None):
"""
初始化客户端
Args:
private_key: 钱包私钥(用于交易),如不提供则只能读取数据
"""
self.private_key = private_key or os.getenv("POLYMARKET_PRIVATE_KEY")
# 配置代理
proxy = os.getenv("SOCKS5_PROXY") if os.getenv("USE_PROXY", "false").lower() == "true" else None
# HTTP客户端 - 禁用压缩避免brotli问题
self.http = httpx.Client(
timeout=30.0,
proxy=proxy,
headers={'Accept-Encoding': 'identity'} # 禁用压缩
)
# 如果有私钥,初始化CLOB客户端
self.clob_client = None
if self.private_key:
self._init_clob_client()
def _init_clob_client(self):
"""初始化CLOB交易客户端"""
try:
from py_clob_client.client import ClobClient
from py_clob_client.clob_types import ApiCreds
# Polygon链ID
chain_id = 137
# 初始化客户端
self.clob_client = ClobClient(
host=self.CLOB_API,
key=self.private_key,
chain_id=chain_id,
)
# 获取或创建API凭证
self.clob_client.set_api_creds(self.clob_client.create_or_derive_api_creds())
print("✓ CLOB客户端初始化成功")
except Exception as e:
print(f"⚠️ CLOB客户端初始化失败: {e}")
self.clob_client = None
def search_markets(self, query: str = "BTC", limit: int = 20) -> List[Dict]:
"""
搜索市场
Args:
query: 搜索关键词
limit: 返回数量限制
Returns:
市场列表
"""
try:
response = self.http.get(
f"{self.GAMMA_API}/markets",
params={
"active": "true",
"closed": "false",
"limit": limit,
}
)
response.raise_for_status()
markets = response.json()
# 过滤包含关键词的市场
filtered = [m for m in markets if query.lower() in m.get("question", "").lower()]
return filtered
except Exception as e:
print(f"搜索市场失败: {e}")
return []
def search_btc_1h_markets(self) -> List[Dict]:
"""
搜索BTC 1小时价格预测市场
Returns:
BTC 1H相关市场列表
"""
try:
response = self.http.get(
f"{self.GAMMA_API}/markets",
params={
"active": "true",
"closed": "false",
"limit": 100,
}
)
response.raise_for_status()
markets = response.json()
# 过滤BTC 1小时相关市场
btc_1h_markets = []
keywords = ["btc", "bitcoin"]
time_keywords = ["1h", "1 hour", "hourly", "hour"]
for m in markets:
question = m.get("question", "").lower()
# 检查是否包含BTC关键词
has_btc = any(kw in question for kw in keywords)
# 检查是否包含1小时关键词
has_1h = any(kw in question for kw in time_keywords)
if has_btc and has_1h:
btc_1h_markets.append(m)
return btc_1h_markets
except Exception as e:
print(f"搜索BTC 1H市场失败: {e}")
return []
def get_market_info(self, condition_id: str) -> Optional[MarketInfo]:
"""
获取市场详细信息
Args:
condition_id: 市场条件ID
Returns:
市场信息对象
"""
try:
response = self.http.get(f"{self.GAMMA_API}/markets/{condition_id}")
response.raise_for_status()
data = response.json()
return MarketInfo(
condition_id=data.get("conditionId", ""),
question=data.get("question", ""),
outcomes=data.get("outcomes", []),
outcome_prices=data.get("outcomePrices", []),
tokens=data.get("tokens", []),
volume=float(data.get("volume", 0)),
liquidity=float(data.get("liquidity", 0)),
end_date=data.get("endDate", ""),
)
except Exception as e:
print(f"获取市场信息失败: {e}")
return None
def get_orderbook(self, token_id: str) -> Dict:
"""
获取订单簿
Args:
token_id: 代币ID
Returns:
订单簿数据
"""
try:
response = self.http.get(
f"{self.CLOB_API}/book",
params={"token_id": token_id}
)
response.raise_for_status()
return response.json()
except Exception as e:
print(f"获取订单簿失败: {e}")
return {}
def get_price(self, token_id: str) -> Optional[float]:
"""
获取代币当前价格
Args:
token_id: 代币ID
Returns:
当前价格(0-1之间)
"""
try:
response = self.http.get(
f"{self.CLOB_API}/price",
params={"token_id": token_id, "side": "buy"}
)
response.raise_for_status()
data = response.json()
return float(data.get("price", 0))
except Exception as e:
print(f"获取价格失败: {e}")
return None
def place_market_order(
self,
token_id: str,
side: str, # "BUY" or "SELL"
amount: float, # USDC金额
) -> Optional[Dict]:
"""
下市价单
Args:
token_id: 代币ID
side: 买卖方向 "BUY" 或 "SELL"
amount: 下注金额(USDC)
Returns:
订单响应
"""
if not self.clob_client:
print("❌ CLOB客户端未初始化,无法下单")
return None
try:
from py_clob_client.order_builder.constants import BUY, SELL
order_side = BUY if side.upper() == "BUY" else SELL
# 创建市价单
order = self.clob_client.create_market_order(
token_id=token_id,
amount=amount,
side=order_side,
)
# 提交订单
response = self.clob_client.post_order(order)
print(f"✓ 订单已提交: {response}")
return response
except Exception as e:
print(f"❌ 下单失败: {e}")
return None
def place_limit_order(
self,
token_id: str,
side: str, # "BUY" or "SELL"
price: float, # 价格(0-1)
size: float, # 数量
) -> Optional[Dict]:
"""
下限价单
Args:
token_id: 代币ID
side: 买卖方向 "BUY" 或 "SELL"
price: 价格(0-1之间)
size: 购买数量
Returns:
订单响应
"""
if not self.clob_client:
print("❌ CLOB客户端未初始化,无法下单")
return None
try:
from py_clob_client.order_builder.constants import BUY, SELL
order_side = BUY if side.upper() == "BUY" else SELL
# 创建限价单
order = self.clob_client.create_limit_order(
token_id=token_id,
price=price,
size=size,
side=order_side,
)
# 提交订单
response = self.clob_client.post_order(order)
print(f"✓ 限价单已提交: {response}")
return response
except Exception as e:
print(f"❌ 下单失败: {e}")
return None
def get_positions(self) -> List[Dict]:
"""
获取当前仓位
Returns:
仓位列表
"""
if not self.clob_client:
print("❌ CLOB客户端未初始化")
return []
try:
positions = self.clob_client.get_positions()
return positions
except Exception as e:
print(f"获取仓位失败: {e}")
return []
def cancel_order(self, order_id: str) -> bool:
"""
取消订单
Args:
order_id: 订单ID
Returns:
是否成功
"""
if not self.clob_client:
print("❌ CLOB客户端未初始化")
return False
try:
self.clob_client.cancel(order_id)
print(f"✓ 订单已取消: {order_id}")
return True
except Exception as e:
print(f"❌ 取消订单失败: {e}")
return False
def close(self):
"""关闭客户端"""
self.http.close()
# 测试函数
def test_search_markets():
"""测试搜索市场功能"""
client = PolymarketClient()
print("搜索BTC相关市场...")
markets = client.search_markets("BTC", limit=10)
if markets:
print(f"\n找到 {len(markets)} 个BTC相关市场:\n")
for m in markets[:5]:
print(f" 问题: {m.get('question', 'N/A')}")
print(f" 条件ID: {m.get('conditionId', 'N/A')}")
print(f" 结果: {m.get('outcomes', [])}")
print(f" 价格: {m.get('outcomePrices', [])}")
print("-" * 50)
else:
print("未找到BTC相关市场")
print("\n搜索BTC 1H市场...")
btc_1h = client.search_btc_1h_markets()
if btc_1h:
print(f"找到 {len(btc_1h)} 个BTC 1H市场")
for m in btc_1h:
print(f" {m.get('question', 'N/A')}")
else:
print("未找到BTC 1H市场")
client.close()
if __name__ == "__main__":
test_search_markets()
+844
View File
@@ -0,0 +1,844 @@
"""
BTC Hourly 自动交易脚本
基于DLinear模型预测,在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
import pickle
from zoneinfo import ZoneInfo
import torch
import numpy as np
import pandas as pd
import pandas_ta as ta
from sklearn.preprocessing import StandardScaler
from dotenv import load_dotenv
import httpx
# 添加项目路径
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
from data.fetcher import BinanceDataFetcher
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)
# 避免重复添加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 ClassificationPreprocessor:
"""DLinear分类预处理器"""
def __init__(self, lookback: int = 168, forecast_horizon: int = 1):
self.lookback = lookback
self.forecast_horizon = forecast_horizon
self.scaler = StandardScaler()
self.is_fitted = False
def add_features(self, df: pd.DataFrame) -> pd.DataFrame:
df = df.copy()
df['rsi'] = ta.rsi(df['close'], length=14)
macd = ta.macd(df['close'])
if macd is not None and not macd.empty:
df['macd'] = macd.iloc[:, 0]
df['macd_hist'] = macd.iloc[:, 1]
df['macd_signal'] = macd.iloc[:, 2]
return df
def create_labels(self, df: pd.DataFrame) -> pd.DataFrame:
df = df.copy()
df['future_price'] = df['close'].shift(-self.forecast_horizon)
df['future_return'] = (df['future_price'] - df['close']) / df['close']
df['label'] = (df['future_return'] > 0).astype(int)
return df[:-self.forecast_horizon]
def preprocess(self, df: pd.DataFrame, fit: bool = True):
df = self.add_features(df)
df = self.create_labels(df)
df = df.dropna().reset_index(drop=True)
feature_cols = ['open', 'high', 'low', 'close', 'volume',
'rsi', 'macd', 'macd_hist', 'macd_signal']
if fit:
scaled_features = self.scaler.fit_transform(df[feature_cols])
self.is_fitted = True
self.feature_columns = feature_cols
else:
scaled_features = self.scaler.transform(df[feature_cols])
X, y = [], []
for i in range(len(scaled_features) - self.lookback):
X.append(scaled_features[i:i + self.lookback])
y.append(df['label'].iloc[i + self.lookback])
return np.array(X), np.array(y), df
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'}
)
# 设备
self.device = torch.device('mps' if torch.backends.mps.is_available() else
'cuda' if torch.cuda.is_available() else 'cpu')
# 加载模型
self.model = None
self.preprocessor = None
self._load_model()
# 数据获取器
self.fetcher = BinanceDataFetcher()
# 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 _load_model(self):
"""加载DLinear模型"""
logger.info("加载DLinear模型...")
from models.dlinear.model import DLinear
checkpoint_path = './models/checkpoints/dlinear_classification_2026/best_model.pt'
preprocessor_path = './models/checkpoints/dlinear_classification_2026/preprocessor.pkl'
checkpoint = torch.load(checkpoint_path, map_location=self.device, weights_only=False)
self.model = DLinear(checkpoint['config'])
self.model.load_state_dict(checkpoint['model_state_dict'])
self.model = self.model.to(self.device).eval()
with open(preprocessor_path, 'rb') as f:
self.preprocessor = pickle.load(f)
logger.info(f"✓ 模型已加载 (设备: {self.device})")
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 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
Args:
target_time: 目标时间(ET时区)
for_current_hour: 如果True,使用当前整点;否则使用下一个整点
Returns:
市场slug
"""
# 获取美东时间
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获取市场信息
Args:
slug: 市场slug
Returns:
市场信息
"""
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市场
Args:
for_current_hour: 如果True,获取当前整点市场;否则获取下一个整点市场
Returns:
市场信息
"""
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]:
"""
刷新当前市场价格
Returns:
更新后的市场信息
"""
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获取实时买入价格
使用/price?side=BUY端点获取真实买入价格 (与页面显示一致)
Args:
up_token_id: Up代币ID
down_token_id: Down代币ID
Returns:
[up_price, down_price] 或 None
"""
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:
"""
执行交易
Args:
prediction: 预测结果(0=跌, 1=涨)
market: 市场信息
Returns:
交易结果
"""
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线并预测当前小时的涨跌
注意: 使用上一小时收盘的K线(倒数第二根)来预测当前小时
例如: 22:00整点时,用21:00收盘的K线预测22:00这个小时的涨跌
Args:
delay_seconds: 延迟秒数(等待K线更新)
Returns:
(prediction, probability): 预测结果(0=跌, 1=涨)和概率
"""
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线(上一小时收盘)进行预测
# 例如22:00时,最新K线是14:00 UTC(22:00北京),我们用13:00 UTC(21:00北京)的数据
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} (实际结果)")
# 用除最后一根外的数据进行预测
df_for_prediction = df.iloc[:-1]
X, _, _ = self.preprocessor.preprocess(df_for_prediction, fit=False)
X_last = torch.FloatTensor(X[-1:]).to(self.device)
with torch.no_grad():
logits = self.model(X_last).cpu().numpy()
prob = 1 / (1 + np.exp(-logits))
prediction = int(prob.item() > 0.5)
probability = float(prob.item())
direction = "📈 UP (涨)" if prediction == 1 else "📉 DOWN (跌)"
confidence = probability if prediction == 1 else 1 - probability
logger.info(f"🤖 DLinear预测: {direction}")
logger.info(f" 置信度: {confidence:.2%}")
# 显示预测是否正确 (用于验证)
actual_up = current_close > prev_close
if prediction == 1:
correct = actual_up
else:
correct = not actual_up
logger.info(f" 实际涨跌: {'📈 涨' if actual_up else '📉 跌'} ({'✅ 正确' if correct else '❌ 错误'})")
return prediction, probability
def run_at_hour_start(self, delay_seconds: float = 2.0) -> Dict:
"""
整点时执行的交易流程
1. 延迟delay_seconds秒等待K线更新
2. 获取最新K线
3. DLinear预测当前整点的收盘涨跌
4. 获取对应的Polymarket市场
5. 检查价格是否≤max_price
6. 下单
Args:
delay_seconds: 整点后延迟秒数
Returns:
执行结果
"""
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}")
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']}")
return result
result['market'] = market['title']
if market['closed']:
result['error'] = '市场已关闭'
logger.error(f"❌ {result['error']}")
return result
# 3. 执行交易
trade_result = self.execute_trade(prediction, market)
result.update(trade_result)
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]
X, _, _ = self.preprocessor.preprocess(df_for_prediction, fit=False)
X_last = torch.FloatTensor(X[-1:]).to(self.device)
with torch.no_grad():
logits = self.model(X_last).cpu().numpy()
prob = 1 / (1 + np.exp(-logits))
prediction = int(prob.item() > 0.5)
probability = float(prob.item())
direction = "📈 UP (涨)" if prediction == 1 else "📉 DOWN (跌)"
confidence = probability if prediction == 1 else 1 - probability
logger.info(f" 🤖 预测: {direction} (置信度: {confidence:.2%})")
# 显示市场价格
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("")
def wait_for_next_hour(self) -> datetime:
"""
等待到下一个整点
Returns:
下一个整点时间
"""
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):
"""
定时运行交易器
工作流程:
1. 每个整点+delay_seconds秒触发交易
2. 期间每interval_minutes分钟刷新价格
Args:
interval_minutes: 价格刷新间隔(分钟)
delay_seconds: 整点后延迟秒数
"""
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()
# 定时刷新价格
schedule.every(interval_minutes).minutes.do(self.refresh_prices)
# 每小时整点后执行交易
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模型')
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:
logs = json.load(f)
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()
+139
View File
@@ -0,0 +1,139 @@
import sqlite3
import pandas as pd
from pathlib import Path
from typing import Optional, List, Tuple
from contextlib import contextmanager
import os
class DatabaseManager:
"""Manages SQLite database for storing OHLCV data."""
def __init__(self, db_path: str = "./data/cache/market_data.db"):
self.db_path = Path(db_path)
self.db_path.parent.mkdir(parents=True, exist_ok=True)
self.initialize_db()
@contextmanager
def get_connection(self):
"""Context manager for database connection."""
conn = sqlite3.connect(self.db_path)
try:
yield conn
finally:
conn.close()
def initialize_db(self):
"""Create tables if they don't exist."""
with self.get_connection() as conn:
cursor = conn.cursor()
# Create OHLCV table
# Composite primary key ensures uniqueness for a specific candle
cursor.execute("""
CREATE TABLE IF NOT EXISTS ohlcv (
timestamp DATETIME,
symbol TEXT,
timeframe TEXT,
open REAL,
high REAL,
low REAL,
close REAL,
volume REAL,
PRIMARY KEY (symbol, timeframe, timestamp)
)
""")
# Create index for faster querying by time range
cursor.execute("""
CREATE INDEX IF NOT EXISTS idx_ohlcv_time
ON ohlcv (symbol, timeframe, timestamp)
""")
conn.commit()
def save_ohlcv(self, df: pd.DataFrame, symbol: str, timeframe: str):
"""Save OHLCV data to database (Upsert)."""
if df.empty:
return
# Ensure timestamp is datetime
df = df.copy()
if not pd.api.types.is_datetime64_any_dtype(df['timestamp']):
df['timestamp'] = pd.to_datetime(df['timestamp'])
# Convert to string for SQLite compatibility (ISO 8601)
df['timestamp'] = df['timestamp'].dt.strftime('%Y-%m-%d %H:%M:%S')
# Add metadata columns if they don't exist
if 'symbol' not in df.columns:
df['symbol'] = symbol
if 'timeframe' not in df.columns:
df['timeframe'] = timeframe
# Prepare data for insertion (ensure order)
records = df[['timestamp', 'symbol', 'timeframe', 'open', 'high', 'low', 'close', 'volume']].to_records(index=False)
with self.get_connection() as conn:
cursor = conn.cursor()
# efficiently insert or replace
# sqlite3 adapter handles datetime objects correctly
cursor.executemany("""
INSERT OR REPLACE INTO ohlcv
(timestamp, symbol, timeframe, open, high, low, close, volume)
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
""", records)
conn.commit()
def get_ohlcv(self, symbol: str, timeframe: str, start_date: Optional[str] = None, end_date: Optional[str] = None) -> pd.DataFrame:
"""Retrieve OHLCV data from database."""
query = """
SELECT timestamp, open, high, low, close, volume
FROM ohlcv
WHERE symbol = ? AND timeframe = ?
"""
params = [symbol, timeframe]
if start_date:
query += " AND timestamp >= ?"
params.append(start_date)
if end_date:
# Handle 'YYYY-MM-DD' vs full timestamp
if len(str(end_date)) == 10: # YYYY-MM-DD
query += " AND timestamp < date(?, '+1 day')"
else:
query += " AND timestamp <= ?"
params.append(end_date)
query += " ORDER BY timestamp ASC"
with self.get_connection() as conn:
df = pd.read_sql_query(query, conn, params=params, parse_dates=['timestamp'])
return df
def get_latest_timestamp(self, symbol: str, timeframe: str) -> Optional[pd.Timestamp]:
"""Get the latest timestamp for a symbol/timeframe."""
with self.get_connection() as conn:
cursor = conn.cursor()
cursor.execute("""
SELECT MAX(timestamp)
FROM ohlcv
WHERE symbol = ? AND timeframe = ?
""", (symbol, timeframe))
result = cursor.fetchone()
return pd.to_datetime(result[0]) if result[0] else None
def get_earliest_timestamp(self, symbol: str, timeframe: str) -> Optional[pd.Timestamp]:
"""Get the earliest timestamp for a symbol/timeframe."""
with self.get_connection() as conn:
cursor = conn.cursor()
cursor.execute("""
SELECT MIN(timestamp)
FROM ohlcv
WHERE symbol = ? AND timeframe = ?
""", (symbol, timeframe))
result = cursor.fetchone()
return pd.to_datetime(result[0]) if result[0] else None
+64 -51
View File
@@ -10,6 +10,8 @@ from binance.client import Client
import pytz import pytz
from dotenv import load_dotenv from dotenv import load_dotenv
from data.database import DatabaseManager
# 加载环境变量 # 加载环境变量
load_dotenv() load_dotenv()
@@ -23,10 +25,10 @@ class BinanceDataFetcher:
} }
def __init__(self, cache_dir: str = "./data/cache"): def __init__(self, cache_dir: str = "./data/cache"):
"""初始化Binance客户端和缓存目录 """初始化Binance客户端和数据库管理器
Args: Args:
cache_dir: 缓存下载数据的目录 cache_dir: 缓存/数据库目录
""" """
# 配置代理 # 配置代理
proxies = None proxies = None
@@ -58,8 +60,8 @@ class BinanceDataFetcher:
print(" 3. 或者使用已缓存的数据") print(" 3. 或者使用已缓存的数据")
self.client = None self.client = None
self.cache_dir = Path(cache_dir) # 初始化数据库管理器
self.cache_dir.mkdir(parents=True, exist_ok=True) self.db_manager = DatabaseManager(os.path.join(cache_dir, "market_data.db"))
def fetch_ohlcv( def fetch_ohlcv(
self, self,
@@ -81,21 +83,7 @@ class BinanceDataFetcher:
Returns: Returns:
包含以下列的DataFrame: timestamp, open, high, low, close, volume 包含以下列的DataFrame: timestamp, open, high, low, close, volume
""" """
# 生成缓存文件名 # 设置日期范围
cache_file = self._get_cache_filename(symbol, timeframe, start_date, end_date)
# 检查缓存
if use_cache and cache_file.exists():
print(f"从缓存加载数据:{cache_file}")
return pd.read_csv(cache_file, parse_dates=['timestamp'])
# 检查客户端是否可用
if self.client is None:
raise ConnectionError(
"Binance客户端不可用。请使用VPN或使用已缓存的数据。"
)
# 设置默认日期
if end_date is None: if end_date is None:
end_dt = datetime.now(pytz.UTC) end_dt = datetime.now(pytz.UTC)
else: else:
@@ -106,6 +94,34 @@ class BinanceDataFetcher:
else: else:
start_dt = datetime.strptime(start_date, '%Y-%m-%d').replace(tzinfo=pytz.UTC) start_dt = datetime.strptime(start_date, '%Y-%m-%d').replace(tzinfo=pytz.UTC)
start_str = start_dt.strftime('%Y-%m-%d')
end_str = end_dt.strftime('%Y-%m-%d')
# 1. 尝试从数据库获取数据
df = pd.DataFrame()
if use_cache:
df = self.db_manager.get_ohlcv(symbol, timeframe, start_str, end_str)
# 检查数据是否足够
if not df.empty:
min_ts = df['timestamp'].min().replace(tzinfo=pytz.UTC)
max_ts = df['timestamp'].max().replace(tzinfo=pytz.UTC)
# 简单判断覆盖范围 (容忍少量缺失)
if min_ts <= start_dt + timedelta(days=2) and max_ts >= end_dt - timedelta(days=2):
print(f"从数据库加载数据:{symbol} {timeframe} ({len(df)} rows)")
return df
# 2. 从API获取数据
if self.client is None:
if not df.empty:
print(f"⚠ API不可用,返回数据库现有数据 ({len(df)} rows)")
return df
raise ConnectionError(
"Binance客户端不可用。请使用VPN或确保数据库有数据。"
)
# Binance使用USDT交易对 # Binance使用USDT交易对
pair = f"{symbol}USDT" pair = f"{symbol}USDT"
interval = self.INTERVALS[timeframe] interval = self.INTERVALS[timeframe]
@@ -113,53 +129,47 @@ class BinanceDataFetcher:
print(f"获取 {symbol} {timeframe} 数据,时间范围:{start_dt.date()} 至 {end_dt.date()}...") print(f"获取 {symbol} {timeframe} 数据,时间范围:{start_dt.date()} 至 {end_dt.date()}...")
# 获取K线数据 # 获取K线数据
klines = self.client.get_historical_klines( try:
symbol=pair, klines = self.client.get_historical_klines(
interval=interval, symbol=pair,
start_str=str(int(start_dt.timestamp() * 1000)), interval=interval,
end_str=str(int(end_dt.timestamp() * 1000)) start_str=str(int(start_dt.timestamp() * 1000)),
) end_str=str(int(end_dt.timestamp() * 1000))
)
except Exception as e:
if not df.empty:
print(f"⚠ API请求失败 ({e}),返回数据库现有数据")
return df
raise e
if not klines: if not klines:
raise ValueError(f"未获取到 {symbol} {timeframe} 的数据") if not df.empty:
return df
raise ValueError(f"未获取到 {symbol} {timeframe} 的数据")
# 转换为DataFrame # 转换为DataFrame
df = pd.DataFrame(klines, columns=[ new_df = pd.DataFrame(klines, columns=[
'timestamp', 'open', 'high', 'low', 'close', 'volume', 'timestamp', 'open', 'high', 'low', 'close', 'volume',
'close_time', 'quote_volume', 'trades', 'taker_buy_base', 'close_time', 'quote_volume', 'trades', 'taker_buy_base',
'taker_buy_quote', 'ignore' 'taker_buy_quote', 'ignore'
]) ])
# 选择并转换列 # 选择并转换列
df = df[['timestamp', 'open', 'high', 'low', 'close', 'volume']].copy() new_df = new_df[['timestamp', 'open', 'high', 'low', 'close', 'volume']].copy()
df['timestamp'] = pd.to_datetime(df['timestamp'], unit='ms') new_df['timestamp'] = pd.to_datetime(new_df['timestamp'], unit='ms')
for col in ['open', 'high', 'low', 'close', 'volume']: for col in ['open', 'high', 'low', 'close', 'volume']:
df[col] = df[col].astype(float) new_df[col] = new_df[col].astype(float)
# 按时间排序 # 按时间排序
df = df.sort_values('timestamp').reset_index(drop=True) new_df = new_df.sort_values('timestamp').reset_index(drop=True)
print(f"已获取 {len(df)} 行数据,时间范围:{df['timestamp'].min()} 至 {df['timestamp'].max()}") print(f"已获取 {len(new_df)} 行数据")
# 保存到缓存 # 保存到数据库
df.to_csv(cache_file, index=False) self.db_manager.save_ohlcv(new_df, symbol, timeframe)
print(f"已缓存数据至:{cache_file}")
return df return new_df
def _get_cache_filename(
self,
symbol: str,
timeframe: str,
start_date: Optional[str],
end_date: Optional[str]
) -> Path:
"""根据参数生成缓存文件名"""
start = start_date or "1y"
end = end_date or "now"
filename = f"{symbol}_{timeframe}_{start}_{end}.csv"
return self.cache_dir / filename
def get_latest_price(self, symbol: Literal['BTC', 'ETH', 'SOL']) -> float: def get_latest_price(self, symbol: Literal['BTC', 'ETH', 'SOL']) -> float:
"""获取符号的当前价格 """获取符号的当前价格
@@ -187,5 +197,8 @@ if __name__ == "__main__":
print(f"日期范围:{df['timestamp'].min()} 至 {df['timestamp'].max()}") print(f"日期范围:{df['timestamp'].min()} 至 {df['timestamp'].max()}")
# 获取当前价格 # 获取当前价格
current_price = fetcher.get_latest_price('BTC') try:
print(f"\n当前BTC价格:${current_price:,.2f}") current_price = fetcher.get_latest_price('BTC')
print(f"\n当前BTC价格:${current_price:,.2f}")
except:
print("\n无法获取当前价格")
+26 -42
View File
@@ -205,12 +205,13 @@ def main():
# 还原真实价格 # 还原真实价格
nbeats_pred_prices = nbeats_preprocessor.inverse_transform_price(nbeats_pred_scaled) nbeats_pred_prices = nbeats_preprocessor.inverse_transform_price(nbeats_pred_scaled)
# 【修改】获取当前K线的open价格(未来可交易价格)用于判断方向 # 【修复】使用当前K线的close价格判断涨跌(与标签定义一致)
# 第i个样本对应的是第168+i根K线,其open价格是真正可以交易的价格 # 第i个样本对应的是第168+i根K线,其close价格是当前收盘价
current_open_prices = df_nbeats['open'].iloc[168:168+len(nbeats_pred_prices)].values # 标签定义: label = (future_close > current_close)
nbeats_preds = (nbeats_pred_prices > current_open_prices).astype(int) # 1=涨, 0=跌 current_close_prices = df_nbeats['close'].iloc[168:168+len(nbeats_pred_prices)].values
nbeats_preds = (nbeats_pred_prices > current_close_prices).astype(int) # 1=涨, 0=跌
print(f"✓ N-BEATS预测完成 (使用当前K线open价格判断)") print(f"✓ N-BEATS预测完成 (使用当前K线close价格判断)")
print(f" 预测上涨: {(nbeats_preds==1).sum()} ({(nbeats_preds==1).sum()/len(nbeats_preds)*100:.1f}%)") print(f" 预测上涨: {(nbeats_preds==1).sum()} ({(nbeats_preds==1).sum()/len(nbeats_preds)*100:.1f}%)")
print(f" 预测下跌: {(nbeats_preds==0).sum()} ({(nbeats_preds==0).sum()/len(nbeats_preds)*100:.1f}%)") print(f" 预测下跌: {(nbeats_preds==0).sum()} ({(nbeats_preds==0).sum()/len(nbeats_preds)*100:.1f}%)")
@@ -223,7 +224,6 @@ def main():
# 从df_2026获取价格数据 # 从df_2026获取价格数据
all_prices = df_2026['close'].values all_prices = df_2026['close'].values
all_open_prices = df_2026['open'].values
# 为每个样本创建预测 # 为每个样本创建预测
for i in range(min_samples): for i in range(min_samples):
@@ -235,12 +235,12 @@ def main():
if end_idx > len(all_prices): if end_idx > len(all_prices):
# 如果超出范围,使用最后512个价格 # 如果超出范围,使用最后512个价格
context_data = all_prices[-context_length:].reshape(1, -1, 1) context_data = all_prices[-context_length:].reshape(1, -1, 1)
# 【修改】使用对应K线的open价格作为当前可交易价格 # 【修复】使用对应K线的close价格作为当前收盘价
current_open_price = all_open_prices[min(168 + i, len(all_open_prices) - 1)] current_close_price = all_prices[min(168 + i, len(all_prices) - 1)]
else: else:
context_data = all_prices[start_idx:end_idx].reshape(1, -1, 1) context_data = all_prices[start_idx:end_idx].reshape(1, -1, 1)
# 【修改】使用第end_idx根K线的open价格(即当前K线的开盘价) # 【修复】使用第168+i根K线的close价格(即当前K线的收盘价)
current_open_price = all_open_prices[end_idx] if end_idx < len(all_open_prices) else all_open_prices[-1] current_close_price = all_prices[168 + i] if 168 + i < len(all_prices) else all_prices[-1]
context_tensor = torch.FloatTensor(context_data).to(device) context_tensor = torch.FloatTensor(context_data).to(device)
@@ -249,12 +249,12 @@ def main():
# 预测第一个时间步的值 # 预测第一个时间步的值
pred_price = output.prediction_outputs.cpu().numpy()[0, 0, 0] pred_price = output.prediction_outputs.cpu().numpy()[0, 0, 0]
# 【修改】判断方向: 预测价格 vs 当前K线open价格 # 【修复】判断方向: 预测价格 vs 当前K线close价格(与标签定义一致)
ttm_preds_list.append(1 if pred_price > current_open_price else 0) ttm_preds_list.append(1 if pred_price > current_close_price else 0)
ttm_preds = np.array(ttm_preds_list) ttm_preds = np.array(ttm_preds_list)
print(f"✓ TTM-R2预测完成 (使用当前K线open价格判断)") print(f"✓ TTM-R2预测完成 (使用当前K线close价格判断)")
print(f" 预测上涨: {(ttm_preds==1).sum()} ({(ttm_preds==1).sum()/len(ttm_preds)*100:.1f}%)") print(f" 预测上涨: {(ttm_preds==1).sum()} ({(ttm_preds==1).sum()/len(ttm_preds)*100:.1f}%)")
print(f" 预测下跌: {(ttm_preds==0).sum()} ({(ttm_preds==0).sum()/len(ttm_preds)*100:.1f}%)") print(f" 预测下跌: {(ttm_preds==0).sum()} ({(ttm_preds==0).sum()/len(ttm_preds)*100:.1f}%)")
@@ -358,38 +358,22 @@ def main():
print(f"\n集成策略性能:") print(f"\n集成策略性能:")
print(f" 双模型交集 (DLinear+NBEATS): {two_model_acc*100:.2f}% (覆盖率: {two_model_valid.sum()/len(dlinear_preds)*100:.1f}%)") print(f" 双模型交集 (DLinear+NBEATS): {two_model_acc*100:.2f}% (覆盖率: {two_model_valid.sum()/len(dlinear_preds)*100:.1f}%)")
print(f" 三模型多数投票: {ensemble_accuracy*100:.2f}% (覆盖率: 100%)")
if len(valid_preds) > 0: # 性能评估
print(f" 三模型交集 (ALL): {ensemble_accuracy*100:.2f}% (覆盖率: {valid_mask.sum()/len(dlinear_preds)*100:.1f}%)") print("\n" + "="*80)
print("🎯 策略评估")
print("="*80)
# 性能评估 best_single = max(dlinear_acc, nbeats_acc, ttm_acc)
print("\n" + "="*80)
print("🎯 策略评估")
print("="*80)
best_single = max(dlinear_acc, nbeats_acc, ttm_acc) if ensemble_accuracy > best_single:
improvement = ensemble_accuracy - best_single
if ensemble_accuracy > best_single: print(f"\n ✅ 三模型多数投票优于所有单模型!")
improvement = ensemble_accuracy - best_single print(f" 准确率提升: {improvement*100:.2f}个百分点")
print(f"\n ✅ 三模型交集优于所有单模型!") else:
print(f" 准确率提升: {improvement*100:.2f}个百分点") print(f"\n ⚠️ 三模型多数投票未能超越最佳单模型")
print(f" 代价: 弃权{(~valid_mask).sum()}次预测 ({(~valid_mask).sum()/len(dlinear_preds)*100:.1f}%)") print(f" 可能原因: 某个模型表现太差,拖累整体性能")
elif ensemble_accuracy > two_model_acc:
improvement = ensemble_accuracy - two_model_acc
print(f"\n ✅ 三模型交集优于双模型交集!")
print(f" 准确率提升: {improvement*100:.2f}个百分点")
print(f" 但覆盖率下降: {(two_model_valid.sum() - valid_mask.sum())/len(dlinear_preds)*100:.1f}%")
else:
print(f"\n ⚠️ 三模型交集未能提升准确率")
print(f" 可能原因: TTM-R2表现太差({ttm_acc*100:.2f}%),拖累了整体性能")
print(f" 建议: 使用双模型交集(DLinear+NBEATS)")
# 实际收益分析
print(f"\n💰 实际收益分析:")
print(f" 如果只在三个模型都同意时交易:")
print(f" - 交易次数: {valid_mask.sum()} ({valid_mask.sum()/len(dlinear_preds)*100:.1f}%)")
print(f" - 准确率: {ensemble_accuracy*100:.2f}%")
print(f" - 避免交易: {(~valid_mask).sum()}次 (规避不确定性)")
print("\n" + "="*80) print("\n" + "="*80)
print("✓ 三模型集成预测分析完成!") print("✓ 三模型集成预测分析完成!")
+14 -8
View File
@@ -1,10 +1,17 @@
"""生成测试数据以便在无法连接Binance时使用""" """生成测试数据以便在无法连接Binance时使用"""
import sys
import os
from pathlib import Path
# 添加项目根目录 to path
sys.path.append(str(Path(__file__).resolve().parent.parent))
import pandas as pd import pandas as pd
import numpy as np import numpy as np
from datetime import datetime, timedelta from datetime import datetime, timedelta
from pathlib import Path
from data.database import DatabaseManager
def generate_sample_data( def generate_sample_data(
symbol: str = 'BTC', symbol: str = 'BTC',
@@ -76,14 +83,13 @@ def generate_sample_data(
df = pd.DataFrame(data) df = pd.DataFrame(data)
# 保存到缓存 # 保存到数据库
cache_dir = Path('./data/cache') # 注意:我们这里假设DatabaseManager的默认路径在脚本运行时是相对于CWD的
cache_dir.mkdir(parents=True, exist_ok=True) # 如果从根目录运行,./data/cache/market_data.db 是正确的
db_manager = DatabaseManager()
db_manager.save_ohlcv(df, symbol, timeframe)
cache_file = cache_dir / f"{symbol}_{timeframe}_1y_now.csv" print(f"✓ 已生成 {len(df)} 行模拟数据并保存到数据库")
df.to_csv(cache_file, index=False)
print(f"✓ 已生成 {len(df)} 行模拟数据并保存到:{cache_file}")
return df return df
+67
View File
@@ -0,0 +1,67 @@
"""Migrate existing CSV cache data to SQLite database."""
import sys
import os
from pathlib import Path
import re
# Add project root to path
sys.path.append(str(Path(__file__).resolve().parent.parent))
import pandas as pd
from data.database import DatabaseManager
def migrate():
cache_dir = Path("./data/cache")
db_path = cache_dir / "market_data.db"
print(f"Initializing database at {db_path}...")
db_manager = DatabaseManager(str(db_path))
# Find all CSV files
csv_files = list(cache_dir.glob("*.csv"))
print(f"Found {len(csv_files)} CSV files to migrate.")
success_count = 0
fail_count = 0
for csv_file in csv_files:
try:
filename = csv_file.name
print(f"Processing {filename}...")
# Parse filename
parts = filename.split('_')
if len(parts) < 2:
print(f" ⚠ Skipping {filename}: Invalid naming format")
continue
symbol = parts[0]
timeframe = parts[1]
# Validate symbol and timeframe (basic check)
if timeframe not in ['1h', '15m']:
# Relaxed check, just warning
print(f" ℹ Note: Unfamiliar timeframe {timeframe} in {filename}")
# Read CSV
df = pd.read_csv(csv_file)
if 'timestamp' not in df.columns:
print(f" ⚠ Skipping {filename}: No timestamp column")
continue
# Save to DB
db_manager.save_ohlcv(df, symbol, timeframe)
print(f" ✓ Imported {len(df)} records for {symbol} {timeframe}")
success_count += 1
except Exception as e:
print(f" ❌ Error processing {csv_file.name}: {e}")
fail_count += 1
print("\nMigration complete!")
print(f"Successfully migrated: {success_count} files")
print(f"Failed: {fail_count} files")
if __name__ == "__main__":
migrate()
+13
View File
@@ -0,0 +1,13 @@
[
{
"timestamp": "2026-02-03T21:18:33.773774",
"market": "Bitcoin Up or Down - February 3, 9AM ET",
"prediction": "DOWN",
"success": false,
"order": null,
"error": "价格$0.51超过限制$0.50",
"side": "DOWN",
"price": 0.505,
"probability": 0.5563886165618896
}
]