refactor: remove legacy trading engine, streamline project to weather-only bot

This commit is contained in:
AmandaloveYang
2026-02-18 09:56:50 +08:00
parent d533bd5b21
commit 30f86eab89
29 changed files with 25 additions and 3644 deletions
View File
-95
View File
@@ -1,95 +0,0 @@
from loguru import logger
class OrderbookAnalyzer:
"""
分析目标: 评估市场供需平衡和流动性
"""
def __init__(self, config=None):
self.config = config or {}
self.wall_threshold = self.config.get("wall_threshold", 500) # 单笔订单超过此值为墙
logger.info("Initializing Orderbook Analyzer...")
def assess_liquidity(self, orderbook, side="ask"):
"""
分析流动性深度 (基于前 3 档)
"""
orders = orderbook.get('asks' if side == "ask" else 'bids', [])
if not orders:
return "枯竭", 0
# 前 3 档总量 (Polymarket 通常返回价格字符串)
depth = sum(float(o.get("size", 0)) for o in orders[:3])
if depth < 50:
return "稀薄", depth
elif depth < 500:
return "正常", depth
else:
return "充裕", depth
def analyze(self, orderbook):
"""
增强版订单簿分析:集成深度与 Spread 评估
"""
bids = orderbook.get('bids', [])
asks = orderbook.get('asks', [])
if not bids or not asks:
return {
"signal": "NEUTRAL",
"confidence": 0.0,
"tradeable": False,
"reason": "缺乏双边报价",
"liquidity": "枯竭",
"spread": 1.0
}
# 1. 计算核心指标
best_bid = float(bids[0].get('price', 0))
best_ask = float(asks[0].get('price', 0))
spread = abs(best_ask - best_bid)
mid_price = (best_ask + best_bid) / 2
# 2. 评估流动性
ask_liq, ask_depth = self.assess_liquidity(orderbook, "ask")
bid_liq, bid_depth = self.assess_liquidity(orderbook, "bid")
# 3. 交易可行性判定 (Spread <= 10c 且 深度 >= $50)
is_tradeable = (spread <= 0.10) and (ask_depth >= 50 or bid_depth >= 50)
# 4. Imbalance 计算
bid_volume = sum([float(b.get('size', 0)) for b in bids])
ask_volume = sum([float(a.get('size', 0)) for a in asks])
imbalance = bid_volume / ask_volume if ask_volume > 0 else 0
result = {
"best_bid": best_bid,
"best_ask": best_ask,
"mid_price": mid_price,
"spread": round(spread, 4),
"ask_depth": round(ask_depth, 2),
"bid_depth": round(bid_depth, 2),
"liquidity": ask_liq if ask_depth < bid_depth else bid_liq,
"tradeable": is_tradeable,
"imbalance": imbalance,
"signal": "NEUTRAL",
"confidence": 0.5
}
# 5. 信号修正
if is_tradeable:
if imbalance > 2.5:
result["signal"] = "BULLISH"
result["confidence"] = 0.75
elif imbalance < 0.4:
result["signal"] = "BEARISH"
result["confidence"] = 0.75
else:
result["confidence"] = 0.1 # 不建议交易
return result
def analyze_orderbook(orderbook):
"""兼容旧接口的便捷函数"""
analyzer = OrderbookAnalyzer()
return analyzer.analyze(orderbook)
-146
View File
@@ -1,146 +0,0 @@
import numpy as np
from loguru import logger
class TechnicalIndicators:
"""
技术指标计算 - RSI, 布林带等
"""
def __init__(self):
logger.info("Initializing Technical Indicators...")
def calculate_rsi(self, prices: list, period: int = 14) -> float:
"""
计算相对强弱指标 (RSI)
Args:
prices: 价格历史列表
period: RSI周期,默认14
Returns:
float: RSI值 (0-100)
"""
if len(prices) < period + 1:
logger.debug("Insufficient data for RSI calculation")
return 50.0 # 返回中性值
prices = np.array(prices)
deltas = np.diff(prices)
gains = np.where(deltas > 0, deltas, 0)
losses = np.where(deltas < 0, -deltas, 0)
avg_gain = np.mean(gains[-period:])
avg_loss = np.mean(losses[-period:])
if avg_loss == 0:
return 100.0
rs = avg_gain / avg_loss
rsi = 100 - (100 / (1 + rs))
logger.debug(f"RSI({period}): {rsi:.2f}")
return rsi
def calculate_bollinger_bands(self, prices: list, period: int = 20, std_dev: float = 2.0) -> dict:
"""
计算布林带
Args:
prices: 价格历史列表
period: 移动平均周期
std_dev: 标准差倍数
Returns:
dict: 包含上轨、中轨、下轨
"""
if len(prices) < period:
logger.debug("Insufficient data for Bollinger Bands")
return {"upper": None, "middle": None, "lower": None}
prices = np.array(prices[-period:])
middle = np.mean(prices)
std = np.std(prices)
upper = middle + std_dev * std
lower = middle - std_dev * std
return {
"upper": upper,
"middle": middle,
"lower": lower,
"std": std
}
def calculate_momentum(self, prices: list, period: int = 10) -> float:
"""
计算价格动量
Args:
prices: 价格历史
period: 动量周期
Returns:
float: 动量值 (当前价格 / N周期前价格 - 1)
"""
if len(prices) < period + 1:
return 0.0
current = prices[-1]
past = prices[-period - 1]
if past == 0:
return 0.0
momentum = (current / past) - 1
return momentum
def get_signal(self, prices: list) -> dict:
"""
综合技术指标信号
Returns:
dict: 包含信号和分数
"""
rsi = self.calculate_rsi(prices)
bb = self.calculate_bollinger_bands(prices)
momentum = self.calculate_momentum(prices)
# RSI信号
if rsi > 70:
rsi_signal = "OVERBOUGHT"
rsi_score = 0.3 # 超买,看跌
elif rsi < 30:
rsi_signal = "OVERSOLD"
rsi_score = 0.8 # 超卖,看涨
else:
rsi_signal = "NEUTRAL"
rsi_score = 0.5
# 布林带信号
if bb["upper"] and len(prices) > 0:
current_price = prices[-1]
if current_price > bb["upper"]:
bb_signal = "ABOVE_UPPER"
bb_score = 0.7 # 突破上轨,强势
elif current_price < bb["lower"]:
bb_signal = "BELOW_LOWER"
bb_score = 0.3 # 跌破下轨,弱势
else:
bb_signal = "WITHIN_BANDS"
bb_score = 0.5
else:
bb_signal = "NO_DATA"
bb_score = 0.5
# 综合分数
combined_score = (rsi_score * 0.5 + bb_score * 0.3 +
(0.5 + momentum * 2) * 0.2) # momentum 转换为 0-1
combined_score = max(0, min(1, combined_score))
return {
"rsi": {"value": rsi, "signal": rsi_signal, "score": rsi_score},
"bollinger": {"bands": bb, "signal": bb_signal, "score": bb_score},
"momentum": momentum,
"combined_score": combined_score
}
-135
View File
@@ -1,135 +0,0 @@
import numpy as np
from loguru import logger
class VolumeAnalyzer:
"""
交易量异常检测 - 识别聪明钱和市场转折点
"""
def __init__(self, config=None):
self.config = config or {}
self.volume_threshold = self.config.get("volume_threshold", 2.0) # 2倍标准差
self.large_order_threshold = self.config.get("large_order_threshold", 1000) # $1000
logger.info("Initializing Volume Analyzer...")
def detect_volume_spike(self, volume_history: list) -> dict:
"""
检测成交量异常放大
Args:
volume_history: 历史成交量列表
Returns:
dict: 包含信号和置信度
"""
if len(volume_history) < 24:
return {"signal": "INSUFFICIENT_DATA", "score": 0.5}
recent_volume = np.array(volume_history[-24:]) # 最近24小时
historical_volume = np.array(volume_history[:-24])
if len(historical_volume) == 0:
return {"signal": "INSUFFICIENT_DATA", "score": 0.5}
avg_volume = np.mean(historical_volume)
std_volume = np.std(historical_volume)
recent_avg = np.mean(recent_volume)
# 计算Z-score
if std_volume > 0:
z_score = (recent_avg - avg_volume) / std_volume
else:
z_score = 0
logger.debug(f"Volume Z-score: {z_score:.2f}")
if z_score > self.volume_threshold:
return {
"signal": "VOLUME_SPIKE",
"score": min(0.9, 0.5 + z_score * 0.1),
"z_score": z_score,
"interpretation": "成交量异常放大,可能有新信息进入市场"
}
elif z_score < -self.volume_threshold:
return {
"signal": "VOLUME_DRY",
"score": 0.3,
"z_score": z_score,
"interpretation": "成交量萎缩,市场观望"
}
return {"signal": "NORMAL", "score": 0.5, "z_score": z_score}
def detect_large_orders(self, transactions: list) -> dict:
"""
检测大额订单 (聪明钱信号)
Args:
transactions: 交易列表,每个包含 size, side, price
Returns:
dict: 大额订单分析结果
"""
large_buys = []
large_sells = []
for tx in transactions:
size = tx.get("size", 0)
side = tx.get("side", "").upper()
if size >= self.large_order_threshold:
if side == "BUY":
large_buys.append(tx)
elif side == "SELL":
large_sells.append(tx)
total_large_buy = sum(t.get("size", 0) for t in large_buys)
total_large_sell = sum(t.get("size", 0) for t in large_sells)
logger.debug(f"Large buys: ${total_large_buy:.2f}, Large sells: ${total_large_sell:.2f}")
if total_large_buy > total_large_sell * 2:
return {
"signal": "SMART_MONEY_BUY",
"score": 0.8,
"large_buy_volume": total_large_buy,
"large_sell_volume": total_large_sell,
"interpretation": "大户在积极买入,跟随机会"
}
elif total_large_sell > total_large_buy * 2:
return {
"signal": "SMART_MONEY_SELL",
"score": 0.2,
"large_buy_volume": total_large_buy,
"large_sell_volume": total_large_sell,
"interpretation": "大户在抛售,风险警告"
}
return {
"signal": "NEUTRAL",
"score": 0.5,
"large_buy_volume": total_large_buy,
"large_sell_volume": total_large_sell
}
def analyze(self, volume_history: list, transactions: list = None) -> dict:
"""
综合分析交易量
"""
volume_signal = self.detect_volume_spike(volume_history)
if transactions:
order_signal = self.detect_large_orders(transactions)
else:
order_signal = {"signal": "NO_DATA", "score": 0.5}
# 综合评分
combined_score = (volume_signal.get("score", 0.5) * 0.6 +
order_signal.get("score", 0.5) * 0.4)
return {
"volume_signal": volume_signal,
"order_signal": order_signal,
"combined_score": combined_score
}
-59
View File
@@ -1,59 +0,0 @@
from loguru import logger
from typing import List, Dict
from src.data_collection.onchain_tracker import OnchainTracker
class WhaleTracker:
"""
大户行为分析模块
"""
def __init__(self, config: dict, tracker: OnchainTracker):
self.config = config
self.tracker = tracker
logger.info("Initializing Whale Tracker...")
def analyze_market_whales(self, market_id: str) -> Dict:
"""
分析特定市场的鲸鱼行为
"""
large_trades = self.tracker.get_large_transactions(market_id)
if not large_trades:
return {"bullish": False, "signal": "NEUTRAL", "reason": "No whale activity detected"}
buy_value = 0
sell_value = 0
for trade in large_trades:
side = trade.get("side", "").upper()
value = trade.get("value", 0)
if side == "BUY":
buy_value += value
else:
sell_value += value
# 判断情绪
if buy_value > sell_value * 2:
return {
"bullish": True,
"signal": "STRONG_ACCUMULATION",
"buy_value": buy_value,
"sell_value": sell_value,
"reason": "Whales are heavily buying"
}
elif sell_value > buy_value * 2:
return {
"bullish": False,
"signal": "STRONG_DISTRIBUTION",
"buy_value": buy_value,
"sell_value": sell_value,
"reason": "Whales are heavily selling"
}
return {
"bullish": buy_value > sell_value,
"signal": "MODERATE",
"buy_value": buy_value,
"sell_value": sell_value,
"reason": "Mixed whale activity"
}
-56
View File
@@ -1,56 +0,0 @@
from loguru import logger
from typing import List, Dict, Optional
from src.data_collection.polymarket_api import PolymarketClient
class OnchainTracker:
"""
追踪 Polymarket 上的大额交易和钱包动向
主要通过 Polymarket API 获取交易历史,并模拟链上分析逻辑
"""
def __init__(self, config: dict, client: PolymarketClient):
self.config = config
self.client = client
self.whale_threshold = self.config.get("whale_threshold", 5000) # $5000 以上视为鲸鱼
logger.info(f"Initializing Onchain Tracker (Whale Threshold: ${self.whale_threshold})")
def get_large_transactions(self, market_id: str, limit: int = 100) -> List[Dict]:
"""
获取特定市场的历史大额交易
"""
trades = self.client.get_trades(market_id=market_id, limit=limit)
if not trades:
return []
# 过滤大额交易 (Polymarket API 返回的格式可能需要根据实际调整)
# 假设格式: [{"price": 0.9, "size": 10000, "side": "BUY", "maker": "0x...", "taker": "0x..."}]
large_trades = []
for trade in trades:
size = float(trade.get("size", 0))
price = float(trade.get("price", 0))
value = size * price
if value >= self.whale_threshold:
trade["value"] = value
large_trades.append(trade)
return large_trades
def get_whale_positions(self, market_id: str) -> Dict[str, float]:
"""
估算大户在某个市场的持仓情况
注意:这只是基于最近交易的估算,真实持仓需要查询链上合约
"""
trades = self.get_large_transactions(market_id, limit=500)
whale_holdings = {}
for trade in trades:
wallet = trade.get("proxyWallet") or trade.get("maker") or "unknown"
side = trade.get("side", "").upper()
size = float(trade.get("size", 0))
if side == "BUY":
whale_holdings[wallet] = whale_holdings.get(wallet, 0) + size
else:
whale_holdings[wallet] = whale_holdings.get(wallet, 0) - size
return whale_holdings
-291
View File
@@ -1,291 +0,0 @@
import os
import requests
import time
import re
from typing import Dict, List, Optional
from loguru import logger
from datetime import datetime
from concurrent.futures import ThreadPoolExecutor
class PolymarketClient:
"""
Polymarket API Client (Pure REST API version)
Directly uses Gamma API and CLOB REST API without py-clob-client dependency.
"""
def __init__(self, config: Dict):
self.clob_url = config.get("base_url", "https://clob.polymarket.com")
self.gamma_url = "https://gamma-api.polymarket.com"
self.timeout = config.get("timeout", 20)
self.session = requests.Session()
# Cache mechanism
self._weather_markets_cache = []
self._last_discovery_time = 0
self._cache_ttl = 300 # 5 minutes cache
# Proxy settings (automatically read from environment)
proxy = os.getenv("HTTPS_PROXY") or os.getenv("HTTP_PROXY")
if proxy:
self.session.proxies = {"http": proxy, "https": proxy}
logger.info(f"Requests session using proxy: {proxy}")
# Set common User-Agent and headers
self.session.headers.update(
{
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36",
"Accept": "application/json",
"Content-Type": "application/json"
}
)
self.api_key = config.get("api_key")
if self.api_key:
self.session.headers.update({"POLY_API_KEY": self.api_key})
logger.info(f"Polymarket REST Client initialized. CLOB: {self.clob_url}, Gamma: {self.gamma_url}")
def get_markets(self, next_cursor: str = None) -> Optional[Dict]:
"""Fetch markets list via CLOB REST API"""
try:
params = {}
if next_cursor:
params["next_cursor"] = next_cursor
resp = self.session.get(f"{self.clob_url}/markets", params=params, timeout=self.timeout)
return resp.json() if resp.status_code == 200 else None
except Exception as e:
logger.debug(f"get_markets failed: {e}")
return None
def get_market(self, market_id: str) -> Optional[Dict]:
"""Fetch market details via CLOB REST API"""
try:
resp = self.session.get(f"{self.clob_url}/markets/{market_id}", timeout=self.timeout)
return resp.json() if resp.status_code == 200 else None
except Exception as e:
logger.debug(f"get_market failed: {e}")
return None
def get_price(self, token_id: str, side: str = "ask") -> Optional[float]:
"""Fetch real-time price for a token via CLOB REST API"""
try:
# Correct CLOB Mapping:
# 'sell' side price is the ASK (price you pay to BUY)
# 'buy' side price is the BID (price you get to SELL)
clob_side = "sell" if side.lower() in ["ask", "buy"] else "buy"
resp = self.session.get(
f"{self.clob_url}/price",
params={"token_id": token_id, "side": clob_side},
timeout=10
)
data = resp.json()
return float(data.get("price", 0)) if resp.status_code == 200 else None
except Exception as e:
logger.debug(f"get_price failed ({token_id}): {e}")
return None
def get_orderbook(self, token_id: str) -> Optional[Dict]:
"""Fetch orderbook for a token via CLOB REST API"""
try:
resp = self.session.get(
f"{self.clob_url}/book", params={"token_id": token_id}, timeout=10
)
return resp.json() if resp.status_code == 200 else None
except Exception as e:
logger.debug(f"get_orderbook failed ({token_id}): {e}")
return None
def get_buy_prices(self, yes_token_id: str, no_token_id: str) -> Optional[Dict]:
"""Fetch buy prices for both YES and NO tokens"""
try:
# Buy Yes = Ask price of YES token
buy_yes = self.get_price(yes_token_id, "BUY")
# Buy No = Ask price of NO token
buy_no = self.get_price(no_token_id, "BUY")
if buy_yes is not None and buy_no is not None:
return {"buy_yes": buy_yes, "buy_no": buy_no}
except Exception as e:
logger.debug(f"get_buy_prices failed: {e}")
return None
def get_multiple_prices(self, token_requests: List[Dict]) -> Dict[str, float]:
"""Batch fetch prices for multiple tokens using ThreadPoolExecutor"""
if not token_requests:
return {}
all_prices = {}
def robust_float(val):
try: return float(val)
except: return 0.0
def fetch_single(req):
tid = req["token_id"]
side = req.get("side", "ask").lower()
# To get ASK (price to buy), request 'sell' side
# To get BID (price to sell), request 'buy' side
api_side = "sell" if side == "ask" else "buy"
val = self.get_price(tid, api_side)
if val:
return f"{tid}:{side.lower()}", val
return None
with ThreadPoolExecutor(max_workers=5) as executor:
results = list(executor.map(fetch_single, token_requests))
for res in results:
if res:
key, val = res
all_prices[key] = val
return all_prices
def get_midpoint(self, token_id: str) -> Optional[float]:
"""Fetch midpoint price via CLOB REST API"""
try:
resp = self.session.get(f"{self.clob_url}/midpoint", params={"token_id": token_id}, timeout=10)
data = resp.json()
return float(data.get("mid", 0)) if resp.status_code == 200 else None
except:
return None
def discover_weather_markets(self) -> list:
"""Scan Gamma API for all weather-related markets with prioritized search and city targeting"""
# Cache check
current_time = time.time()
if self._weather_markets_cache and (current_time - self._last_discovery_time < self._cache_ttl):
logger.debug(f"Using cached market list ({len(self._weather_markets_cache)} items)")
return self._weather_markets_cache
logger.info("📡 Scanning Polymarket via Gamma API for weather markets...")
all_weather_markets = []
seen_keys = set()
# 1. Target newest markets by query and ID sorting
search_queries = ["highest temperature", "temperature in", "daily weather"]
try:
# Use multiple offsets to find more historical/diverse markets
for offset in [0, 500, 1000]:
for query in search_queries:
logger.debug(f"Searching with query: {query} (offset {offset})")
params = {
"query": query,
"active": "true",
"limit": 500,
"offset": offset,
"order": "id",
"ascending": "false"
}
resp = self.session.get(f"{self.gamma_url}/markets", params=params, timeout=self.timeout)
if resp.status_code == 200:
markets = resp.json()
logger.debug(f"Query '{query}' returned {len(markets)} markets")
for m in markets:
q = m.get("question", "").lower()
slug = m.get("slug", "").lower()
# Filter for weather markets (Broadened)
is_weather = any(k in q or k in slug for k in [
"highest temperature", "highest-temperature",
"temperature in", "temperature-in",
"daily weather", "daily-weather",
"weather", "气温", "温度"
])
if is_weather:
c_id = m.get("conditionId")
t_ids = m.get("clobTokenIds")
active_id = m.get("activeTokenId")
# Robust JSON parsing for clobTokenIds string
if isinstance(t_ids, str) and t_ids.startswith("["):
try:
import json
t_ids = json.loads(t_ids)
except:
pass
# For Neg Risk markets, activeTokenId might be missing in list view
# If we have clobTokenIds, we can work with it
if not t_ids:
continue
if not active_id and isinstance(t_ids, list) and len(t_ids) > 0:
active_id = t_ids[0] # Assume first is YES
if not active_id:
continue
unique_key = f"{c_id}_{active_id}"
if unique_key not in seen_keys:
logger.debug(f"Found weather segment: {q}")
all_weather_markets.append({
"condition_id": c_id,
"question": m.get("question"),
"active_token_id": active_id,
"outcome_index": t_ids.index(active_id) if isinstance(t_ids, list) and active_id in t_ids else 0,
"tokens": t_ids,
"prices": m.get("outcomePrices"),
"event_title": m.get("description", "")[:100],
"slug": m.get("slug"),
"group_id": m.get("negRiskMarketID")
})
seen_keys.add(unique_key)
else:
logger.debug(f"Query '{query}' failed with status {resp.status_code}")
if len(all_weather_markets) > 50:
break
logger.info(f"Discovery complete: Found {len(all_weather_markets)} weather segments.")
self._weather_markets_cache = all_weather_markets
self._last_discovery_time = current_time
return all_weather_markets
except Exception as e:
logger.error(f"Market discovery failed: {e}")
return []
except Exception as e:
logger.error(f"Market discovery failed: {e}")
return []
except Exception as e:
logger.error(f"Market discovery failed: {e}")
return []
def get_weather_markets(self) -> list:
return self.discover_weather_markets()
def find_weather_market(self, city: str, date_str: str = None) -> Optional[Dict]:
markets = self.get_weather_markets()
for m in markets:
# Match against question, title AND slug
content = (str(m.get("question", "")) + str(m.get("event_title", "")) + str(m.get("slug", ""))).lower()
if city.lower() in content:
if date_str:
if date_str.lower() in content: return m
else:
return m
return None
def get_weather_event_markets(self, city: str) -> list:
all_markets = self.get_weather_markets()
return [
m for m in all_markets
if city.lower() in (str(m.get("question", "")) + str(m.get("event_title", "")) + str(m.get("slug", ""))).lower()
]
# --- Trading Stubs (Real trading requires signing, which is disabled in pure REST mode) ---
def create_order(self, *args, **kwargs) -> Optional[Dict]:
logger.warning("create_order: Real trading is disabled in pure REST mode. Please use paper trading.")
return None
def cancel_order(self, *args, **kwargs) -> Optional[Dict]:
logger.warning("cancel_order: Real trading is disabled in pure REST mode.")
return None
def get_orders(self, *args, **kwargs) -> Optional[Dict]:
logger.warning("get_orders: Real trading is disabled in pure REST mode.")
return None
View File
-285
View File
@@ -1,285 +0,0 @@
import numpy as np
import pandas as pd
from typing import Dict, List, Optional, Tuple
from datetime import datetime
from loguru import logger
try:
from statsmodels.tsa.arima.model import ARIMA
HAS_STATSMODELS = True
except ImportError:
HAS_STATSMODELS = False
logger.debug("statsmodels not installed, ARIMA model unavailable")
try:
from sklearn.ensemble import RandomForestRegressor
from sklearn.model_selection import train_test_split
HAS_SKLEARN = True
except ImportError:
HAS_SKLEARN = False
logger.debug("scikit-learn not installed, ML models unavailable")
class TemperaturePredictor:
"""
Temperature prediction model using statistical and ML methods
Supports:
- ARIMA for time series prediction
- Random Forest for feature-based prediction
- Ensemble of both methods
"""
def __init__(self, config: dict = None):
self.config = config or {}
self.arima_order = self.config.get("arima_order", (5, 1, 2))
self.rf_estimators = self.config.get("rf_estimators", 100)
self.arima_model = None
self.rf_model = None
self.is_trained = False
logger.info("Temperature Predictor initialized")
def prepare_features(self, data: pd.DataFrame) -> pd.DataFrame:
"""
Prepare features for ML model
Args:
data: DataFrame with temperature history
Returns:
DataFrame: Feature-engineered data
"""
df = data.copy()
# Time-based features
if 'date' in df.columns:
df['date'] = pd.to_datetime(df['date'])
df['day_of_year'] = df['date'].dt.dayofyear
df['month'] = df['date'].dt.month
df['day_of_week'] = df['date'].dt.dayofweek
# Lag features
if 'temp' in df.columns:
for lag in [1, 2, 3, 7, 14]:
df[f'temp_lag_{lag}'] = df['temp'].shift(lag)
# Rolling statistics
df['temp_rolling_mean_7'] = df['temp'].rolling(window=7).mean()
df['temp_rolling_std_7'] = df['temp'].rolling(window=7).std()
df['temp_rolling_mean_14'] = df['temp'].rolling(window=14).mean()
# Drop NaN rows created by lag features
df = df.dropna()
return df
def train_arima(self, temperature_series: List[float]) -> bool:
"""
Train ARIMA model on temperature time series
Args:
temperature_series: List of historical temperatures
Returns:
bool: Success status
"""
if not HAS_STATSMODELS:
logger.error("statsmodels required for ARIMA training")
return False
if len(temperature_series) < 30:
logger.warning("Insufficient data for ARIMA training (need 30+ points)")
return False
try:
series = np.array(temperature_series)
model = ARIMA(series, order=self.arima_order)
self.arima_model = model.fit()
logger.info(f"ARIMA model trained. AIC: {self.arima_model.aic:.2f}")
return True
except Exception as e:
logger.error(f"ARIMA training failed: {e}")
return False
def train_random_forest(self,
features: pd.DataFrame,
target_col: str = 'temp') -> bool:
"""
Train Random Forest model
Args:
features: Feature DataFrame
target_col: Target column name
Returns:
bool: Success status
"""
if not HAS_SKLEARN:
logger.error("scikit-learn required for Random Forest training")
return False
if len(features) < 50:
logger.warning("Insufficient data for RF training (need 50+ rows)")
return False
try:
# Prepare data
feature_cols = [c for c in features.columns
if c not in [target_col, 'date', 'datetime']]
X = features[feature_cols].values
y = features[target_col].values
# Train-test split
X_train, X_test, y_train, y_test = train_test_split(
X, y, test_size=0.2, random_state=42
)
# Train model
self.rf_model = RandomForestRegressor(
n_estimators=self.rf_estimators,
random_state=42,
n_jobs=-1
)
self.rf_model.fit(X_train, y_train)
# Evaluate
train_score = self.rf_model.score(X_train, y_train)
test_score = self.rf_model.score(X_test, y_test)
logger.info(f"Random Forest trained. Train R²: {train_score:.4f}, Test R²: {test_score:.4f}")
# Store feature names
self.feature_names = feature_cols
return True
except Exception as e:
logger.error(f"Random Forest training failed: {e}")
return False
def predict_arima(self, steps: int = 1) -> Optional[Dict]:
"""
Make prediction using ARIMA model
Args:
steps: Number of steps to forecast
Returns:
dict: Prediction with confidence interval
"""
if self.arima_model is None:
logger.warning("ARIMA model not trained")
return None
try:
forecast = self.arima_model.forecast(steps=steps)
conf_int = self.arima_model.get_forecast(steps=steps).conf_int()
return {
"method": "ARIMA",
"predicted_temp": float(forecast[0]) if steps == 1 else [float(f) for f in forecast],
"confidence_interval": [float(conf_int.iloc[0, 0]), float(conf_int.iloc[0, 1])] if steps == 1 else conf_int.values.tolist()
}
except Exception as e:
logger.error(f"ARIMA prediction failed: {e}")
return None
def predict_rf(self, features: np.ndarray) -> Optional[Dict]:
"""
Make prediction using Random Forest model
Args:
features: Feature array for prediction
Returns:
dict: Prediction result
"""
if self.rf_model is None:
logger.warning("Random Forest model not trained")
return None
try:
prediction = self.rf_model.predict(features.reshape(1, -1))[0]
# Estimate confidence using tree variance
tree_predictions = [tree.predict(features.reshape(1, -1))[0]
for tree in self.rf_model.estimators_]
std = np.std(tree_predictions)
return {
"method": "RandomForest",
"predicted_temp": float(prediction),
"confidence_interval": [float(prediction - 1.96 * std),
float(prediction + 1.96 * std)],
"std": float(std)
}
except Exception as e:
logger.error(f"Random Forest prediction failed: {e}")
return None
def predict_ensemble(self,
temperature_history: List[float],
feature_data: pd.DataFrame = None,
arima_weight: float = 0.4,
rf_weight: float = 0.6) -> Dict:
"""
Make ensemble prediction combining ARIMA and Random Forest
Args:
temperature_history: Historical temperature series
feature_data: Feature data for RF prediction
arima_weight: Weight for ARIMA prediction
rf_weight: Weight for RF prediction
Returns:
dict: Ensemble prediction
"""
predictions = []
weights = []
# ARIMA prediction
if self.arima_model is not None:
arima_pred = self.predict_arima(steps=1)
if arima_pred:
predictions.append(arima_pred["predicted_temp"])
weights.append(arima_weight)
# Random Forest prediction
if self.rf_model is not None and feature_data is not None:
# Get latest features
prepared = self.prepare_features(feature_data)
if len(prepared) > 0 and hasattr(self, 'feature_names'):
latest_features = prepared[self.feature_names].iloc[-1].values
rf_pred = self.predict_rf(latest_features)
if rf_pred:
predictions.append(rf_pred["predicted_temp"])
weights.append(rf_weight)
if not predictions:
logger.debug("No predictions available (Model not trained)")
return {
"predicted_temp": None,
"confidence": 0.5,
"error": "No models available for prediction"
}
# Weighted average
weights = np.array(weights) / np.sum(weights) # Normalize weights
ensemble_pred = np.average(predictions, weights=weights)
# Estimate confidence based on model agreement
if len(predictions) > 1:
spread = abs(predictions[0] - predictions[1])
confidence = max(0.5, 1.0 - spread / 5.0) # Lower confidence if predictions differ
else:
confidence = 0.7
return {
"predicted_temp": float(ensemble_pred),
"confidence": confidence,
"confidence_interval": [ensemble_pred - 2.0, ensemble_pred + 2.0], # Approximate
"individual_predictions": predictions,
"weights": weights.tolist()
}
View File
-198
View File
@@ -1,198 +0,0 @@
from loguru import logger
from src.analysis.volume_analyzer import VolumeAnalyzer
from src.analysis.orderbook_analyzer import analyze_orderbook
from src.analysis.technical_indicators import TechnicalIndicators
class DecisionEngine:
"""
综合决策引擎 - 多因子加权评分系统
"""
def __init__(self, config: dict = None):
self.config = config or {}
# 因子权重
self.weights = self.config.get("weights", {
"statistical_prediction": 0.50,
"data_source_consensus": 0.15,
"market_volume_signal": 0.15,
"orderbook_analysis": 0.10,
"technical_indicators": 0.05,
"onchain_whale_signal": 0.05
})
# 初始化分析器
self.volume_analyzer = VolumeAnalyzer(config)
self.tech_indicators = TechnicalIndicators()
logger.info("决策引擎初始化完成。")
logger.debug(f"权重配置: {self.weights}")
def calculate_signal(self,
model_prediction: dict,
market_data: dict,
weather_consensus: dict = None,
whale_activity: dict = None) -> dict:
"""
综合多因子计算交易信号
Args:
model_prediction: 统计模型预测结果
market_data: 市场数据 (价格历史、订单簿、交易量等)
weather_consensus: 天气数据源一致性检查结果
whale_activity: 链上大户活动数据
Returns:
dict: 综合评分和交易建议
"""
scores = {}
details = {}
# 1. 统计模型预测得分 (权重: 50%)
stat_confidence = model_prediction.get("confidence", 0.5)
scores["statistical"] = stat_confidence
details["statistical"] = {
"score": stat_confidence,
"prediction": model_prediction.get("predicted_temp"),
"confidence_interval": model_prediction.get("confidence_interval")
}
# 2. 多源数据一致性 (权重: 15%)
if weather_consensus:
is_consensus = weather_consensus.get("consensus", False)
consensus_score = 1.0 if is_consensus else 0.3
else:
consensus_score = 0.5
scores["consensus"] = consensus_score
details["consensus"] = weather_consensus
# 3. 交易量信号 (权重: 15%)
volume_history = market_data.get("volume_history", [])
transactions = market_data.get("transactions", [])
volume_analysis = self.volume_analyzer.analyze(volume_history, transactions)
scores["volume"] = volume_analysis.get("combined_score", 0.5)
details["volume"] = volume_analysis
# 4. 订单簿分析 (权重: 10%)
orderbook = market_data.get("orderbook", {})
orderbook_signal = analyze_orderbook(orderbook)
scores["orderbook"] = orderbook_signal.get("confidence", 0.5)
details["orderbook"] = orderbook_signal
# 5. 技术指标 (权重: 5%)
price_history = market_data.get("price_history", [])
if price_history:
tech_signal = self.tech_indicators.get_signal(price_history)
scores["technical"] = tech_signal.get("combined_score", 0.5)
details["technical"] = tech_signal
else:
scores["technical"] = 0.5
details["technical"] = {"message": "No price history available"}
# 6. 链上鲸鱼信号 (权重: 5%)
if whale_activity:
is_bullish = whale_activity.get("bullish", False)
whale_score = 0.8 if is_bullish else 0.2
else:
whale_score = 0.5
scores["whale"] = whale_score
details["whale"] = whale_activity
# 加权计算最终分数
final_score = (
scores["statistical"] * self.weights["statistical_prediction"] +
scores["consensus"] * self.weights["data_source_consensus"] +
scores["volume"] * self.weights["market_volume_signal"] +
scores["orderbook"] * self.weights["orderbook_analysis"] +
scores["technical"] * self.weights["technical_indicators"] +
scores["whale"] * self.weights["onchain_whale_signal"]
)
# 生成建议
recommendation = self._get_recommendation(final_score)
result = {
"final_score": round(final_score, 4),
"recommendation": recommendation,
"factor_scores": scores,
"factor_details": details,
"weights": self.weights
}
logger.info(f"Decision: {recommendation} (score: {final_score:.4f})")
return result
def _get_recommendation(self, score: float) -> str:
"""
根据评分生成交易建议
Args:
score: 综合评分 (0-1)
Returns:
str: 交易建议
"""
if score > 0.80:
return "STRONG_BUY"
elif score > 0.65:
return "BUY"
elif score > 0.50:
return "WEAK_BUY"
elif score > 0.35:
return "HOLD"
elif score > 0.20:
return "WEAK_SELL"
else:
return "NO_ACTION"
def should_trade(self,
signal: dict,
current_price: float,
min_confidence: float = 0.65) -> dict:
"""
判断是否应该执行交易
Args:
signal: calculate_signal返回的信号
current_price: 当前市场价格
min_confidence: 最低置信度阈值
Returns:
dict: 交易决策
"""
final_score = signal.get("final_score", 0)
recommendation = signal.get("recommendation", "NO_ACTION")
# 检查是否满足交易条件
should_buy = (
final_score >= min_confidence and
recommendation in ["STRONG_BUY", "BUY"] and
current_price >= 0.85 # 价格阈值
)
should_sell = (
final_score < 0.35 or
recommendation in ["WEAK_SELL", "NO_ACTION"]
)
if should_buy:
return {
"action": "BUY",
"confidence": final_score,
"price": current_price,
"reason": f"Score {final_score:.2f} >= threshold {min_confidence}"
}
elif should_sell:
return {
"action": "SELL",
"confidence": final_score,
"price": current_price,
"reason": f"Score {final_score:.2f} below threshold or bearish signal"
}
else:
return {
"action": "HOLD",
"confidence": final_score,
"price": current_price,
"reason": "Conditions not met for trading"
}
-153
View File
@@ -1,153 +0,0 @@
from loguru import logger
class PositionManager:
"""
仓位管理 - Kelly公式动态仓位计算
"""
def __init__(self, config=None):
self.config = config or {}
self.max_position_ratio = self.config.get("max_position_ratio", 0.25) # 最大单笔25%
self.max_total_exposure = self.config.get("max_total_exposure", 0.80) # 最大总仓位80%
self.min_trade_size = self.config.get("min_trade_size", 10) # 最小交易额$10
logger.info("Initializing Position Manager...")
def kelly_criterion(self, win_prob: float, odds: float) -> float:
"""
Kelly公式计算最优投资比例
f = (bp - q) / b
f: 应投资的资金比例
b: 赔率 (盈利/亏损)
p: 胜率
q: 败率 (1-p)
Args:
win_prob: 预测胜率 (0-1)
odds: 赔率
Returns:
float: 建议投资比例 (0-1)
"""
if win_prob <= 0 or win_prob >= 1 or odds <= 0:
return 0.0
q = 1 - win_prob
f = (win_prob * odds - q) / odds
# 限制最大仓位
f = max(0, min(f, self.max_position_ratio))
logger.debug(f"Kelly ratio: {f:.4f} (win_prob={win_prob:.2f}, odds={odds:.2f})")
return f
def calculate_position_size(self,
total_capital: float,
win_prob: float,
market_price: float,
current_exposure: float = 0) -> dict:
"""
计算建议仓位大小
Args:
total_capital: 总资金
win_prob: 模型预测胜率
market_price: 当前市场价格 (0-1)
current_exposure: 当前已有仓位占比
Returns:
dict: 包含建议仓位大小和相关信息
"""
# 计算赔率
if market_price <= 0 or market_price >= 1:
return {"size": 0, "error": "Invalid market price"}
odds = (1 - market_price) / market_price
# Kelly计算
kelly_ratio = self.kelly_criterion(win_prob, odds)
# 检查总仓位限制
available_ratio = self.max_total_exposure - current_exposure
if available_ratio <= 0:
return {
"size": 0,
"kelly_ratio": kelly_ratio,
"reason": "Max exposure reached"
}
# 实际使用比例
actual_ratio = min(kelly_ratio, available_ratio)
# 计算金额
position_size = total_capital * actual_ratio
# 检查最小交易额
if position_size < self.min_trade_size:
return {
"size": 0,
"kelly_ratio": kelly_ratio,
"reason": f"Below minimum trade size (${self.min_trade_size})"
}
return {
"size": position_size,
"kelly_ratio": kelly_ratio,
"actual_ratio": actual_ratio,
"odds": odds,
"expected_return": (win_prob * odds - (1 - win_prob)) * position_size
}
def should_exit(self,
entry_price: float,
current_price: float,
current_prediction: float,
stop_loss: float = 0.15,
take_profit: float = 0.30) -> dict:
"""
判断是否应该平仓
Args:
entry_price: 入场价格
current_price: 当前价格
current_prediction: 当前模型预测
stop_loss: 止损比例
take_profit: 止盈比例
Returns:
dict: 退出建议
"""
if entry_price <= 0:
return {"should_exit": False}
pnl_ratio = (current_price - entry_price) / entry_price
# 止损
if pnl_ratio < -stop_loss:
return {
"should_exit": True,
"reason": "STOP_LOSS",
"pnl_ratio": pnl_ratio
}
# 止盈
if pnl_ratio > take_profit:
return {
"should_exit": True,
"reason": "TAKE_PROFIT",
"pnl_ratio": pnl_ratio
}
# 模型预测反转
if current_prediction < 0.4: # 预测胜率下降
return {
"should_exit": True,
"reason": "PREDICTION_REVERSAL",
"pnl_ratio": pnl_ratio,
"current_prediction": current_prediction
}
return {
"should_exit": False,
"pnl_ratio": pnl_ratio
}
-99
View File
@@ -1,99 +0,0 @@
from loguru import logger
class RiskManager:
"""
风险控制系统
"""
def __init__(self, config=None):
self.config = config or {}
# 基础风控参数
self.max_single_trade = self.config.get(
"max_single_trade", 50.0
) # 最大单笔调整为 $50
self.max_daily_exposure = 50.0 # 每日最高投入上限
self.daily_used_exposure = 0.0
self.last_reset_date = ""
self.min_confidence = 0.5
self.peak_capital = 0
self.is_trading_paused = False
logger.info("Initializing Pro Risk Manager...")
def _reset_daily_exposure(self):
"""每日重置额度"""
from datetime import datetime
today = datetime.now().strftime("%Y-%m-%d")
if self.last_reset_date != today:
self.daily_used_exposure = 0.0
self.last_reset_date = today
logger.info(f"Daily exposure reset for {today}")
def calculate_position_size(
self,
base_confidence_usd: float,
depth: float = 0,
hours_to_settle: float = 24,
is_high_relative_volume: bool = False,
) -> tuple[float, str]:
"""
仓位计算方法 (简化版,移除流动性过滤):
仓位 = base_position(置信度)
× time_decay(离结算衰减)
× budget_limit
"""
self._reset_daily_exposure()
final_pos = base_confidence_usd
reason = "Normal"
# 1. 时间衰减因子
# 离结算时间越近,预测越准但也存在剧烈博弈风险
time_factor = 1.0
if hours_to_settle <= 1.0:
time_factor = 0.0 # 最后 1 小时停止建仓
reason = "🚫临近结算"
elif hours_to_settle <= 4.0:
time_factor = 0.4 # 1-4小时:缩小 60%
reason = "⏱️结算冲刺 (40%)"
elif hours_to_settle <= 12.0:
time_factor = 0.7 # 4-12小时:缩小 30%
reason = "⏳接近结算 (70%)"
final_pos *= time_factor
if final_pos <= 0:
return 0.0, reason
# 2. 预算上限过滤
remaining_daily = self.max_daily_exposure - self.daily_used_exposure
if remaining_daily <= 0:
return 0.0, "🚫今日总额度已满 ($50)"
if final_pos > remaining_daily:
final_pos = remaining_daily
reason = "🛑触及日风控上限"
# 3. 高相对成交量加权 (如果是高成交量市场,且逻辑支持,可保持原状或微增)
# 这里逻辑设定为:如果不是高成交量,再次缩减 20% 防御
if not is_high_relative_volume:
final_pos *= 0.8
if reason == "Normal":
reason = "📉低活缩减"
return round(final_pos, 2), reason
def record_trade(self, amount: float):
"""记录成交额以扣除额度"""
self.daily_used_exposure += amount
logger.debug(
f"Applied exposure: ${amount}. Daily Total: ${self.daily_used_exposure}"
)
def check_trade_risk(
self, trade_size: float, market_data: dict, model_confidence: float
) -> dict:
"""保持基础接口兼容"""
return {"passed": True, "risks": []}
View File
-219
View File
@@ -1,219 +0,0 @@
from loguru import logger
from typing import Optional, Dict
from src.data_collection.polymarket_api import PolymarketClient
class OrderExecutor:
"""
交易执行器 - 负责订单生成、提交和管理
"""
def __init__(self, config: dict, client: PolymarketClient):
self.config = config
self.client = client
self.pending_orders = {}
self.executed_orders = []
logger.info("Order Executor initialized")
def execute_trade(self,
token_id: str,
side: str,
amount: float,
price: float,
order_type: str = "GTC") -> Dict:
"""
执行交易
Args:
token_id: Token ID
side: "BUY""SELL"
amount: 交易金额
price: 价格
order_type: 订单类型 (GTC, GTD, FOK)
Returns:
dict: 订单结果
"""
logger.info(f"Executing {side} order: ${amount:.2f} @ {price:.4f}")
# 计算数量
if price <= 0:
return {"status": "error", "message": "Invalid price"}
size = amount / price
# 提交订单
try:
result = self.client.create_order(
token_id=token_id,
side=side,
price=price,
size=size,
order_type=order_type
)
if result:
order_id = result.get("orderID", "unknown")
self.executed_orders.append({
"order_id": order_id,
"token_id": token_id,
"side": side,
"price": price,
"size": size,
"amount": amount,
"result": result
})
logger.info(f"Order executed successfully: {order_id}")
return {
"status": "success",
"order_id": order_id,
"side": side,
"price": price,
"size": size,
"amount": amount
}
else:
return {"status": "error", "message": "Order submission failed"}
except Exception as e:
logger.error(f"Order execution failed: {e}")
return {"status": "error", "message": str(e)}
def cancel_order(self, order_id: str) -> Dict:
"""
取消订单
Args:
order_id: 订单ID
Returns:
dict: 取消结果
"""
try:
result = self.client.cancel_order(order_id)
if result:
logger.info(f"Order {order_id} cancelled")
return {"status": "success", "order_id": order_id}
else:
return {"status": "error", "message": "Cancel failed"}
except Exception as e:
logger.error(f"Cancel order failed: {e}")
return {"status": "error", "message": str(e)}
def get_open_orders(self, market_id: str = None) -> Optional[Dict]:
"""
获取当前挂单
Args:
market_id: 可选的市场过滤
Returns:
dict: 挂单列表
"""
return self.client.get_orders(market_id)
def get_execution_history(self) -> list:
"""
获取执行历史
Returns:
list: 已执行订单列表
"""
return self.executed_orders
class PortfolioTracker:
"""
持仓追踪器
"""
def __init__(self):
self.positions = {}
self.total_invested = 0
self.total_pnl = 0
logger.info("Portfolio Tracker initialized")
def add_position(self,
token_id: str,
side: str,
size: float,
entry_price: float,
amount: float):
"""
添加持仓
"""
if token_id not in self.positions:
self.positions[token_id] = {
"side": side,
"size": size,
"entry_price": entry_price,
"amount": amount,
"current_price": entry_price,
"unrealized_pnl": 0
}
else:
# 加仓
existing = self.positions[token_id]
total_size = existing["size"] + size
avg_price = (existing["size"] * existing["entry_price"] + size * entry_price) / total_size
existing["size"] = total_size
existing["entry_price"] = avg_price
existing["amount"] += amount
self.total_invested += amount
logger.info(f"Position added: {token_id}, size={size}, price={entry_price}")
def update_price(self, token_id: str, current_price: float):
"""
更新持仓价格
"""
if token_id in self.positions:
pos = self.positions[token_id]
pos["current_price"] = current_price
# 计算未实现盈亏
if pos["side"] == "BUY":
pos["unrealized_pnl"] = (current_price - pos["entry_price"]) * pos["size"]
else:
pos["unrealized_pnl"] = (pos["entry_price"] - current_price) * pos["size"]
def close_position(self, token_id: str, exit_price: float) -> Dict:
"""
平仓
"""
if token_id not in self.positions:
return {"status": "error", "message": "Position not found"}
pos = self.positions[token_id]
if pos["side"] == "BUY":
realized_pnl = (exit_price - pos["entry_price"]) * pos["size"]
else:
realized_pnl = (pos["entry_price"] - exit_price) * pos["size"]
self.total_pnl += realized_pnl
self.total_invested -= pos["amount"]
del self.positions[token_id]
return {
"status": "success",
"realized_pnl": realized_pnl,
"exit_price": exit_price
}
def get_summary(self) -> Dict:
"""
获取持仓汇总
"""
total_unrealized = sum(p["unrealized_pnl"] for p in self.positions.values())
return {
"positions_count": len(self.positions),
"total_invested": self.total_invested,
"total_unrealized_pnl": total_unrealized,
"total_realized_pnl": self.total_pnl,
"positions": self.positions
}
-158
View File
@@ -1,158 +0,0 @@
import json
import os
import time
from datetime import datetime, timedelta
from loguru import logger
class PaperTrader:
"""
模拟交易系统 (Paper Trading System)
"""
def __init__(self, storage_path="data/paper_positions.json", total_capital=1000.0):
self.storage_path = storage_path
self.initial_capital = total_capital
data = self._load_data()
self.positions = data.get("positions", {})
self.history = data.get("history", []) # 历史结项记录
self.trades = data.get("trades", []) # 原始买入/卖出记录
self.balance = data.get("balance", total_capital)
logger.info(f"模拟交易系统初始化。累计成交: {len(self.history)} 笔, 买入记录: {len(self.trades)}")
def _load_data(self):
if os.path.exists(self.storage_path):
try:
with open(self.storage_path, "r", encoding="utf-8") as f:
return json.load(f)
except:
return {"positions": {}, "history": [], "trades": [], "balance": self.initial_capital}
return {"positions": {}, "history": [], "trades": [], "balance": self.initial_capital}
def _save_data(self):
with open(self.storage_path, "w", encoding="utf-8") as f:
json.dump(
{
"positions": self.positions,
"history": self.history,
"trades": self.trades,
"balance": round(self.balance, 2),
},
f,
ensure_ascii=False,
indent=2,
)
def open_position(self, market_id: str, city: str, option: str, price: int, side: str, amount_usd: float = 5.0, target_date: str = None, predicted_temp: float = None):
"""
开仓进入模拟仓位
"""
# 价格以美分计,转换为 0-1 比例
price_decimal = price / 100.0
# 检查余额
if self.balance < amount_usd:
logger.warning(f"余额不足,无法开仓 (余额: ${self.balance:.2f})")
return False
# 计算持仓份额
shares = amount_usd / price_decimal if price_decimal > 0 else 0
position_id = f"{market_id}_{side}"
# 如果已经有相同方向的仓位,可以选择加仓或忽略(这里简单起见,不重复开仓)
if position_id in self.positions:
return False
new_pos = {
"market_id": market_id,
"city": city,
"option": option,
"side": side,
"entry_price": price,
"shares": shares,
"cost_usd": amount_usd,
"current_price": price,
"pnl_usd": 0.0,
"pnl_pct": 0.0,
"status": "OPEN",
"target_date": target_date,
"predicted_temp": predicted_temp,
"opened_at": (datetime.utcnow() + timedelta(hours=8)).strftime("%Y-%m-%d %H:%M:%S")
}
self.positions[position_id] = new_pos
self.balance -= amount_usd
# 记录交易流水
self.trades.append({
"type": "BUY",
"city": city,
"option": option,
"side": side,
"price": price,
"amount": amount_usd,
"time": new_pos["opened_at"]
})
self._save_data()
logger.success(f"【模拟开仓】{city} | {option} | {side} | 价格: {price}¢ | 投入: ${amount_usd}")
return True
def update_pnl(self, current_prices: dict):
updated_report = []
finished_ids = []
for pid, pos in self.positions.items():
if pos["status"] != "OPEN":
continue
m_id = pos["market_id"]
if m_id in current_prices:
curr_price = current_prices[m_id].get("price", 50)
if pos["side"] == "NO":
curr_price = 100 - curr_price
# 更新当前价值
value = pos["shares"] * (curr_price / 100.0)
pnl = value - pos["cost_usd"]
pnl_pct = (pnl / pos["cost_usd"]) * 100 if pos["cost_usd"] > 0 else 0
pos["current_price"] = curr_price
pos["pnl_usd"] = round(pnl, 2)
pos["pnl_pct"] = round(pnl_pct, 2)
# --- 自动结项检测:如果价格变为 0 或 100 (Polymarket 已结算) ---
if curr_price >= 99.5 or curr_price <= 0.5:
pos["status"] = "CLOSED"
pos["closed_at"] = (
datetime.utcnow() + timedelta(hours=8)
).strftime("%Y-%m-%d %H:%M:%S")
self.balance += value # 资金回笼
self.history.append(pos)
finished_ids.append(pid)
logger.success(
f"【模拟结项】{pos['city']} | {pos['option']} | 最终价格: {curr_price}¢ | 获利: ${pnl:+.2f}"
)
else:
updated_report.append(pos)
# 从活跃仓位中移除已结项的
for pid in finished_ids:
# 在流水中添加卖出(结项)记录
pos = self.positions[pid]
self.trades.append({
"type": "SELL",
"city": pos["city"],
"option": pos["option"],
"side": pos["side"],
"price": pos["current_price"],
"amount": round(pos["shares"] * (pos["current_price"] / 100.0), 2),
"time": pos.get("closed_at")
})
del self.positions[pid]
self._save_data()
return updated_report
-285
View File
@@ -1,285 +0,0 @@
import requests
import html
from loguru import logger
from datetime import datetime
class TelegramNotifier:
"""
Telegram 消息推送模块
支持信号推送、预警推送和市场异常提醒
"""
def __init__(self, config: dict):
self.config = config
self.token = config.get("bot_token")
self.chat_id = config.get("chat_id")
self.proxy = config.get("proxy")
self.session = requests.Session()
if self.proxy:
if not self.proxy.startswith("http"):
self.proxy = f"http://{self.proxy}"
self.session.proxies = {"http": self.proxy, "https": self.proxy}
logger.info("Telegram 通知器初始化完成。")
@staticmethod
def _escape_html(text: str) -> str:
"""Escape HTML special characters"""
if not isinstance(text, str):
text = str(text)
return html.escape(text, quote=False)
def _send_message(self, text: str):
"""发送 Telegram 消息的主函数 (支持多个 ID)"""
if not self.token or not self.chat_id:
logger.warning("未配置 Telegram Token 或 Chat ID,无法发送消息。")
return False
# 支持逗号分隔的多个 ID
chat_ids = str(self.chat_id).replace(" ", "").split(",")
url = f"https://api.telegram.org/bot{self.token}/sendMessage"
all_successful = True
for cid in chat_ids:
if not cid:
continue
payload = {
"chat_id": cid,
"text": text,
"parse_mode": "HTML",
"disable_web_page_preview": True,
}
try:
response = self.session.post(url, json=payload, timeout=10)
if response.status_code != 200:
error_msg = response.text
if "chat not found" in error_msg.lower():
logger.error(
f"Telegram 消息发送给 {cid} 失败 (400): Chat ID {cid} 无效或机器人尚未被加入该聊天。请在 Telegram 中发送 /id 给机器人确认正确的 Chat ID。"
)
else:
logger.error(
f"Telegram 消息发送给 {cid} 失败 ({response.status_code}): {error_msg}"
)
all_successful = False
else:
logger.info(f"Telegram 消息发送给 {cid} 成功。")
except Exception as e:
logger.error(f"Telegram 消息发送给 {cid} 异常: {e}")
all_successful = False
return all_successful
def send_signal(
self,
market_name: str,
full_title: str,
option: str,
score: float,
prediction: str,
confidence: int,
analysis_list: list,
price: float,
market_url: str,
local_time: str = None,
target_date: str = None,
):
"""发送交易信号推送"""
stars = "" * int(score) + "" * (5 - int(score))
timestamp_utc = datetime.utcnow().strftime("%H:%M")
analysis_text = "\n".join(
[
f"{self._escape_html(item)}" if "" not in item else item
for item in analysis_list
]
)
local_time_text = (
f"🕒 当地时间: <b>{self._escape_html(local_time)}</b>\n"
if local_time
else ""
)
target_date_text = self._escape_html(target_date) if target_date else "待定"
text = (
f"🎯 <b>交易信号 #{self._escape_html(market_name.split(' ')[0])}</b>\n\n"
f"📍 城市: <b>{self._escape_html(market_name)}</b>\n"
f"🏆 市场: <i>{self._escape_html(full_title)}</i>\n"
f"📝 选项: <b>{self._escape_html(option)}</b>\n"
f"💰 当前价格: <b>{price}¢</b>\n"
f"═══════════════════\n"
f"📊 信号评分: {stars} ({score}/5)\n"
f"🤖 模型预测: {self._escape_html(prediction)}\n"
f"📈 置信度: {confidence}%\n\n"
f"分析汇总:\n"
f"{analysis_text}\n"
f"═══════════════════\n"
f"{local_time_text}"
f"📅 结算日期: <b>{target_date_text}</b>\n"
f"🔗 <a href='{market_url}'>点击进入市场</a>\n\n"
f"⏰ 信号时间: {timestamp_utc} UTC"
)
return self._send_message(text)
def send_combined_alert(
self,
city: str,
alerts: list,
local_time: str = None,
forecast_temp: str = None,
total_volume: float = 0,
brackets_count: int = 0,
strategy_tips: list = None,
metar_data: dict = None,
):
"""发送简约版合并预警 (含 METAR 航空气象数据)"""
if not alerts:
return
from datetime import datetime, timedelta
# UTC+8 北京时间
now_bj = datetime.utcnow() + timedelta(hours=8)
timestamp_bj = now_bj.strftime("%H:%M")
# 1. METAR 航空气象数据区块
metar_text = ""
if metar_data and metar_data.get("current", {}).get("temp") is not None:
icao = metar_data.get("icao", "N/A")
temp = metar_data["current"]["temp"]
unit = "°F" if metar_data.get("unit") == "fahrenheit" else "°C"
# 解析观测时间 (格式: 2026-02-07T11:00:00.000Z)
obs_time_raw = metar_data.get("observation_time", "")
if "T" in obs_time_raw:
obs_time = obs_time_raw.split("T")[1][:5] + " UTC"
else:
obs_time = obs_time_raw or "N/A"
# 可选:风速信息
wind_kt = metar_data["current"].get("wind_speed_kt")
wind_text = f" | 风速:{wind_kt}kt" if wind_kt else ""
metar_text = (
f"✈️ <b>机场实测 ({icao}):</b>\n"
f" 🌡️ {temp:.1f}{unit}{wind_text}\n"
f" 🕐 观测: {obs_time}\n\n"
)
# 2. 信号详情构建
items_text = ""
for a in alerts:
items_text += f"{a['msg']}\n\n"
# 3. 策略建议(如果有)
tips_text = ""
if strategy_tips:
tips_text = (
"💡 <b>策略建议:</b>\n"
+ "\n".join([f"{self._escape_html(tip)}" for tip in strategy_tips])
+ "\n\n"
)
# 4. 总体布局
text = (
f"🔔 <b>城市监控报告 #{self._escape_html(city)}</b>\n\n"
f"📍 城市: {self._escape_html(city)}\n"
f"{metar_text}"
f"📊 <b>实时异动:</b>\n"
f"{items_text}"
f"{tips_text}"
f"═══════════════════\n"
f"🕒 当地时间: {self._escape_html(local_time or 'N/A')}\n"
f"⏰ 预警时间: {timestamp_bj} (北京时间)"
)
return self._send_message(text)
def send_anomaly(
self,
city_tag: str,
market_name: str,
detected_anomaly: str,
stats: dict,
whales: list,
current_price: float,
local_time: str = None,
):
"""发送市场异常推送"""
from datetime import datetime, timedelta
# UTC+8 北京时间
timestamp_bj = (datetime.utcnow() + timedelta(hours=8)).strftime("%H:%M")
whale_text = "\n".join([f"- {self._escape_html(w)}" for w in whales])
stats_text = "\n".join(
[
f"{self._escape_html(k)}: {self._escape_html(v)}"
for k, v in stats.items()
]
)
local_time_text = (
f"🕒 当地时间: <b>{self._escape_html(local_time)}</b>\n"
if local_time
else ""
)
text = (
f"👀 <b>市场异常 #{self._escape_html(city_tag)}</b>\n\n"
f"📍 城市: {self._escape_html(city_tag)}\n"
f"🏆 市场: {self._escape_html(market_name)}\n\n"
f"🚨 <b>检测到异常:</b>\n"
f"{self._escape_html(detected_anomaly)}\n"
f"{stats_text}\n\n"
f"🐋 <b>大户动向:</b>\n"
f"{whale_text}\n\n"
f"💰 当前价格: <b>{current_price}¢</b>\n"
f"═══════════════════\n"
f"{local_time_text}"
f"⏰ 信号时间: {timestamp_bj} (北京时间)"
)
return self._send_message(text)
def send_alert(
self,
city_tag: str,
market_name: str,
price: float,
trigger: str,
prev_price: float,
change: str,
quick_analysis: list,
local_time: str = None,
):
"""发送价格预警推送"""
from datetime import datetime, timedelta
# UTC+8 北京时间
timestamp_bj = (datetime.utcnow() + timedelta(hours=8)).strftime("%H:%M")
analysis_text = "\n".join(
[f"- {self._escape_html(item)}" for item in quick_analysis]
)
local_time_text = (
f"🕒 当地时间: <b>{self._escape_html(local_time)}</b>\n"
if local_time
else ""
)
text = (
f"⚡ <b>价格预警 #{self._escape_html(city_tag)}</b>\n\n"
f"📍 城市: {self._escape_html(city_tag)}\n"
f"🏆 市场: {self._escape_html(market_name)}\n"
f"💰 报价: <b>{price}¢ ↗️</b>\n\n"
f"触发条件: {self._escape_html(trigger)}\n"
f"变动详情: {prev_price}¢ -> {price}¢ ({self._escape_html(change)})\n\n"
f"📊 <b>快速分析:</b>\n"
f"{analysis_text}\n\n"
f"═══════════════════\n"
f"{local_time_text}"
f"⏰ 预警时间: {timestamp_bj} (北京时间)"
)
return self._send_message(text)