feat: Introduce Polymarket API client, paper trading module, and essential utilities for market data and trading.

This commit is contained in:
2569718930@qq.com
2026-02-05 22:29:52 +08:00
parent 2c76c835c0
commit 81b3736ea1
9 changed files with 374 additions and 185 deletions
+38
View File
@@ -0,0 +1,38 @@
# 📈 PolyWeather 模拟仓 (Paper Trading) 使用指南
本系统提供全自动的模拟交易功能,让您在不投入真实资金的情况下,验证天气预测逻辑的盈利能力。
## 🛠️ 运行机制
1. **自动开仓**:
- 监控引擎在扫描中,一旦发现任何档位的 **Buy Yes****Buy No** 价格处于 **85¢ - 95¢** 区间(与城市监控报告一致),即触发买入。
- 初始本金: **$1000.00**
- 单笔投入: **$5.00**
- 资金检查: 余额不足时将停止开仓。
2. **实时估值**:
- 每轮扫描结束后,系统会根据最新盘口中间价更新持仓价值。
3. **数据持久化**:
- 持仓与余额保存在 `data/paper_positions.json`
## 📊 盈亏计算公式
- **持仓份额** = $5 / (买入价格 / 100)
- **可用余额** = 初始本金 - 累计投入总额
- **浮动盈亏** = 当前总价值 - 投入本金 ($5)
## 🤖 电报指令
您可以直接在机器人中通过以下指令查看进度:
- **/portfolio**: 实时返回当前所有“浮动”持仓的盈亏状况、历史胜率以及账户余额。
## 📁 存储文件说明
如果您需要手动清理或修改仓位,可以编辑 `data/paper_positions.json`
- `status: "OPEN"` 表示正在持仓。
- `entry_price` 以美分为单位(如 91 表示 0.91$)。
---
**蚂蚁重力 (Antigravity) 实验室**
+19 -11
View File
@@ -17,11 +17,12 @@ python run.py
## 🤖 电报机器人指令集 ## 🤖 电报机器人指令集
| 指令 | 描述 | 用法 | | 指令 | 描述 | 用法 |
| :-------- | :--------------- | :---------------------------- | | :----------- | :--------------- | :---------------------------- |
| `/signal` | **获取交易信号** | 返回当前最值得关注的 3 个档位 | | `/signal` | **获取交易信号** | 返回当前最值得关注的 3 个档位 |
| `/status` | **检查系统状态** | 确认监控引擎是否在线 | | `/portfolio` | **查看模拟仓位** | 获取实时模拟交易盈亏汇总报告 |
| `/help` | **指令帮助** | 显示所有可用指令 | | `/status` | **检查系统状态** | 确认监控引擎是否在线 |
| `/help` | **指令帮助** | 显示所有可用指令 |
--- ---
@@ -32,9 +33,10 @@ python run.py
- **优化机制**: 同一轮扫描中,同一城市的所有异动将**合并为一条消息**发送,拒绝刷屏。 - **优化机制**: 同一轮扫描中,同一城市的所有异动将**合并为一条消息**发送,拒绝刷屏。
- **触发内容**: 包含该城市下所有符合条件的“价格预警”与“市场异常”。 - **触发内容**: 包含该城市下所有符合条件的“价格预警”与“市场异常”。
### 2. ⚡ 价格预警 ### 2. ⚡ 价格预警 (触发模拟买入)
- **触发条件**: Buy Yes 或 Buy No 价格在 **85¢-95¢** 区间。 - **触发条件**: Buy Yes 或 Buy No 价格在 **85¢-95¢** 区间。
- **关联动作**: 系统会自动在该档位执行 **5 USD 的模拟开仓**,用于验证胜率。
- **用途**: 高胜率/即将锁定区间提醒,适合平仓或收割。 - **用途**: 高胜率/即将锁定区间提醒,适合平仓或收割。
### 3. 👀 市场异常 ### 3. 👀 市场异常
@@ -42,7 +44,12 @@ python run.py
- **大户入场**: 检测到单笔 >$5000 的大额交易且买卖比失衡。 - **大户入场**: 检测到单笔 >$5000 的大额交易且买卖比失衡。
- **异常交易流**: 成交量突然放大 (>2倍历史标准差)。 - **异常交易流**: 成交量突然放大 (>2倍历史标准差)。
### 4. 🎯 交易信号 (指令查询) ### 4. 📅 每日盈亏总结
- **触发时间**: 北京时间 23:55 左右自动推送。
- **内容**: 汇总当日所有模拟仓位的浮动盈亏、余额变动及胜率统计。
### 5. 🎯 交易信号 (指令查询)
- 对比气象预报与市场价格偏差。 - 对比气象预报与市场价格偏差。
- 包含:城市、档位、当地时间、预期温度(含单位自适应)、偏差评分。 - 包含:城市、档位、当地时间、预期温度(含单位自适应)、偏差评分。
@@ -69,10 +76,11 @@ HTTP_PROXY=http://127.0.0.1:7890
## 📋 核心功能特性 ## 📋 核心功能特性
-**智能合并推送**: 按城市汇总预警,界面整洁不刷屏。 -**智能合并推送**: 按城市汇总预警,界面整洁不刷屏。
-**极速价格同步**: 采用批量 API 接口,一次请求同步全量城市盘口价,无延迟、无 404 -**全自动模拟交易**: 内置模拟仓位系统,支持 85-95¢ 区间自动跟单,记录实战胜率
-**北京时间适配**: 所有推送时间戳已自动转换为北京时间 (UTC+8) -**极速价格同步**: 采用 Polymarket 批量 API 接口,一次同步全量城市,无延迟、无 404
-**北京时间适配**: 所有推送时间戳与每日总结均自动转换为北京时间 (UTC+8)。
-**智能日期选择**: 自动定位最早的活跃市场日期,结算后自动顺延。 -**智能日期选择**: 自动定位最早的活跃市场日期,结算后自动顺延。
-**温度单位自适应**: 美国市场自动切换华氏度 (°F),其他地区显示摄氏度 (°C)。 -**温度单位自适应**: 美国市场切换华氏度 (°F),其他地区显示摄氏度 (°C)。
-**全量数据持久化**: 信号记录推送历史保存至本地 JSON,重启不丢失,不重复 -**全量数据持久化**: 信号记录推送历史、交易仓位均保存至本地 JSON。
--- ---
+16 -9
View File
@@ -17,11 +17,12 @@ This command launches:
## 🤖 Telegram Bot Commands ## 🤖 Telegram Bot Commands
| Command | Description | Usage | | Command | Description | Usage |
| :-------- | :---------------------- | :------------------------------------------- | | :----------- | :---------------------- | :------------------------------------------- |
| `/signal` | **Get Trading Signals** | Returns top 3 markets with highest deviation | | `/signal` | **Get Trading Signals** | Returns top 3 markets with highest deviation |
| `/status` | **Check Status** | Confirm if the monitoring engine is online | | `/portfolio` | **View Portfolio** | Get real-time paper trading profit report |
| `/help` | **Help** | Display all available commands | | `/status` | **Check Status** | Confirm if the monitoring engine is online |
| `/help` | **Help** | Display all available commands |
--- ---
@@ -32,15 +33,21 @@ This command launches:
- **Optimization**: All anomalies for the same city are merged into a **single report** per scan cycle to prevent spamming. - **Optimization**: All anomalies for the same city are merged into a **single report** per scan cycle to prevent spamming.
- **Content**: Includes Price Alerts and Market Anomalies (Whales/Volume). - **Content**: Includes Price Alerts and Market Anomalies (Whales/Volume).
### 2. ⚡ Price Alerts ### 2. ⚡ Price Alerts (Auto Paper Trade)
- **Trigger**: Buy Yes or Buy No price enters the **85¢-95¢** range. - **Trigger**: Buy Yes or Buy No price enters the **85¢-95¢** range.
- **Purpose**: High-probability / Near-settlement reminders, ideal for closing or reaping positions. - **Auto Action**: System automatically executes a **$5.00 Paper Trade** to track success rate.
- **Purpose**: High-probability / Near-settlement reminders.
### 3. 👀 Market Anomalies ### 3. 👀 Market Anomalies
- **Whale Inflow**: Detection of large single trades (>$5,000) with imbalanced buy/sell ratios. - **Whale Inflow**: Large single trades (>$5,000) with imbalanced ratios.
- **Volume Spikes**: Sudden increase in trading volume (>2x historical standard deviation). - **Volume Spikes**: Sudden increase in volume (>2x historical std dev).
### 4. 📅 Daily PnL Summary
- **Trigger**: Triggered automatically around 23:55 (Beijing Time).
- **Content**: Summarizes daily floating PnL, balance changes, and win rate.
### 4. 🎯 Trading Signals (Query) ### 4. 🎯 Trading Signals (Query)
+74 -9
View File
@@ -28,9 +28,9 @@ def start_bot():
"🌡️ <b>PolyWeather 监控机器人</b>\n\n" "🌡️ <b>PolyWeather 监控机器人</b>\n\n"
"可用指令:\n" "可用指令:\n"
"/signal - 获取当前高置信度交易信号\n" "/signal - 获取当前高置信度交易信号\n"
"/portfolio - 查看当前模拟交易报告\n"
"/status - 检查监控系统状态\n" "/status - 检查监控系统状态\n"
"/id - 获取当前聊天的 Chat ID\n\n" "/id - 获取当前聊天的 Chat ID"
"💡 <b>直接输入城市名称</b> (如: <code>Seattle</code> 或 <code>London</code>) 即可查询该城市当天的最高温市场报价。"
) )
bot.reply_to(message, welcome_text, parse_mode="HTML") bot.reply_to(message, welcome_text, parse_mode="HTML")
@@ -45,9 +45,6 @@ def start_bot():
@bot.message_handler(commands=["signal"]) @bot.message_handler(commands=["signal"])
def get_signals(message): def get_signals(message):
# 仅响应授权的 Chat ID (可选)
# if str(message.chat.id) != str(chat_id): return
bot.send_message(message.chat.id, "🔍 正在检索当前最值得关注的天气信号...") bot.send_message(message.chat.id, "🔍 正在检索当前最值得关注的天气信号...")
try: try:
@@ -68,7 +65,7 @@ def start_bot():
# 按分数排序并取前 3 个 # 按分数排序并取前 3 个
sorted_signals = sorted( sorted_signals = sorted(
signals.values(), key=lambda x: x["score"], reverse=True signals.values(), key=lambda x: x.get("score", 0), reverse=True
)[:3] )[:3]
for s in sorted_signals: for s in sorted_signals:
@@ -76,10 +73,10 @@ def start_bot():
market_name=s["city"], market_name=s["city"],
full_title=s["full_title"], full_title=s["full_title"],
option=s["option"], option=s["option"],
score=round(s["score"] * 5, 1), score=round(s.get("score", 0) * 5, 1),
prediction=s["prediction"], prediction=s["prediction"],
confidence=int(s["score"] * 100), confidence=int(s.get("score", 0) * 100),
analysis_list=[f"偏差解析: {s['rationale']}"], analysis_list=[f"偏差解析: {s.get('rationale', 'N/A')}"],
price=s["price"], price=s["price"],
market_url=s["url"], market_url=s["url"],
local_time=s["local_time"], local_time=s["local_time"],
@@ -90,6 +87,74 @@ def start_bot():
except Exception as e: except Exception as e:
bot.send_message(message.chat.id, f"❌ 获取信号时出错: {e}") bot.send_message(message.chat.id, f"❌ 获取信号时出错: {e}")
@bot.message_handler(commands=["portfolio"])
def get_portfolio(message):
"""查看模拟仓位"""
try:
if not os.path.exists("data/paper_positions.json"):
bot.reply_to(message, "📭 目前没有任何模拟记录。")
return
with open("data/paper_positions.json", "r", encoding="utf-8") as f:
data = json.load(f)
positions = data.get("positions", {})
history = data.get("history", [])
balance = data.get("balance", 1000.0)
if not positions and not history:
bot.reply_to(
message,
f"📭 目前没有任何模拟记录。\n可用余额: <b>${balance:.2f}</b>",
parse_mode="HTML",
)
return
msg_lines = ["📊 <b>模拟交易报告 (北京时间)</b>\n" + "" * 15]
# 1. 活跃持仓
if positions:
msg_lines.append("📌 <b>当前持仓:</b>")
total_pnl = 0
for pid, pos in positions.items():
pnl_usd = pos.get("pnl_usd", 0)
total_pnl += pnl_usd
icon = "🟢" if pnl_usd >= 0 else "🔴"
msg_lines.append(
f"{icon} {pos['city']} {pos['option']} ({pos['side']}): {pnl_usd:+.2f}$"
)
msg_lines.append(f"<b>持仓小计: {total_pnl:+.2f}$</b>\n")
# 2. 最近交易记录 (最新 5 笔)
trades = data.get("trades", [])
if trades:
msg_lines.append("\n📝 <b>最近操作:</b>")
# 取末尾 5 笔交易并展示
recent_trades = trades[-5:]
for t in reversed(recent_trades):
t_type = "🛒 买入" if t["type"] == "BUY" else "💰 卖出"
t_time = t.get("time", "").split(" ")[1] # 仅显示时间
msg_lines.append(
f"{t_time} {t_type} {t['city']} {t['option']} ({t['price']}¢)"
)
# 3. 历史汇总统计
if history:
total_trades = len(history)
wins = sum(1 for p in history if p.get("pnl_usd", 0) > 0)
win_rate = (wins / total_trades) * 100 if total_trades > 0 else 0
msg_lines.append("\n📈 <b>历史战绩:</b>")
msg_lines.append(f"累计成交: {total_trades}")
msg_lines.append(f"综合胜率: <b>{win_rate:.1f}%</b>")
footer = "\n" + "" * 15 + "\n" + f"💳 虚拟账户余额: <b>${balance:.2f}</b>"
msg_lines.append(footer)
bot.reply_to(message, "\n".join(msg_lines), parse_mode="HTML")
except Exception as e:
bot.reply_to(message, f"❌ 获取持仓失败: {e}")
@bot.message_handler(commands=["status"]) @bot.message_handler(commands=["status"])
def get_status(message): def get_status(message):
bot.reply_to( bot.reply_to(
+66 -58
View File
@@ -2,7 +2,7 @@ import sys
import time import time
import os import os
import json import json
from datetime import datetime from datetime import datetime, timedelta
from loguru import logger from loguru import logger
from src.utils.config_loader import load_config from src.utils.config_loader import load_config
@@ -17,39 +17,28 @@ from src.analysis.technical_indicators import TechnicalIndicators
from src.analysis.whale_tracker import WhaleTracker from src.analysis.whale_tracker import WhaleTracker
from src.strategy.decision_engine import DecisionEngine from src.strategy.decision_engine import DecisionEngine
from src.strategy.risk_manager import RiskManager from src.strategy.risk_manager import RiskManager
from src.trading.paper_trader import PaperTrader
from src.utils.notifier import TelegramNotifier from src.utils.notifier import TelegramNotifier
def main(): def main():
""" # 1. 初始化配置与日志
Polymarket 交易系统主循环 - 监控与推送模式 config_data = load_config()
""" setup_logger(config_data.get("app", {}).get("log_level", "INFO"))
# 1. 设置日志
setup_logger()
logger.info("正在启动 Polymarket 天气交易信号监控系统...")
# 2. 加载配置 logger.info("🌟 PolyWeather 监控引擎启动中...")
try:
config_data = load_config()
logger.info("配置加载成功。")
except Exception as e:
logger.error(f"配置加载失败: {e}")
sys.exit(1)
# 3. 初始化组件 # 2. 初始化核心组件
polymarket = PolymarketClient(config_data["polymarket"]) polymarket = PolymarketClient(config_data["polymarket"])
weather = WeatherDataCollector(config_data["weather"]) weather = WeatherDataCollector(config_data["weather"])
onchain = OnchainTracker(config_data["polymarket"], polymarket) onchain = OnchainTracker(config_data["polymarket"], polymarket)
notifier = TelegramNotifier(config_data["telegram"]) notifier = TelegramNotifier(config_data["telegram"])
# 3. 初始化分析与交易组件
predictor = TemperaturePredictor() predictor = TemperaturePredictor()
volume_analyzer = VolumeAnalyzer() decision_engine = DecisionEngine(config_data.get("config", {}))
orderbook_analyzer = OrderbookAnalyzer() whale_tracker = WhaleTracker(config_data.get("config", {}), onchain)
tech_indicators = TechnicalIndicators() paper_trader = PaperTrader()
whale_tracker = WhaleTracker(config_data, onchain)
decision_engine = DecisionEngine(config_data)
risk_manager = RiskManager(config_data)
# 发送启动通知 # 发送启动通知
notifier._send_message( notifier._send_message(
@@ -226,7 +215,7 @@ def main():
question = market.get("question", "未知市场") question = market.get("question", "未知市场")
event_title = market.get("event_title", "") event_title = market.get("event_title", "")
# (日期处理逻辑保持不变...) # 识别该合约的目标日期
target_date = weather.extract_date_from_title( target_date = weather.extract_date_from_title(
event_title event_title
) or weather.extract_date_from_title(question) ) or weather.extract_date_from_title(question)
@@ -301,16 +290,13 @@ def main():
"transactions": [], "transactions": [],
}, },
weather_consensus={"average_temp": ref_temp}, weather_consensus={"average_temp": ref_temp},
whale_activity=whale_tracker.analyze_market_whales( whale_activity=None,
market_id
),
) )
cache_entry["score"] = signal["final_score"] cache_entry["score"] = signal["final_score"]
cache_entry["rationale"] = signal.get("recommendation", "N/A") cache_entry["rationale"] = signal.get("recommendation", "N/A")
all_markets_cache[market_id] = cache_entry all_markets_cache[market_id] = cache_entry
# --- 预警收集 --- # --- 预警收集 (仅监控价格) ---
# 1. 价格预警
if (0.85 <= buy_yes_price <= 0.95) or ( if (0.85 <= buy_yes_price <= 0.95) or (
0.85 <= buy_no_price <= 0.95 0.85 <= buy_no_price <= 0.95
): ):
@@ -324,40 +310,28 @@ def main():
if trigger_side == "Buy Yes" if trigger_side == "Buy Yes"
else int(buy_no_price * 100) else int(buy_no_price * 100)
) )
# --- 模拟交易触发逻辑 ---
side = "YES" if trigger_side == "Buy Yes" else "NO"
success = paper_trader.open_position(
market_id=market_id,
city=city,
option=question,
price=trigger_price,
side=side,
amount_usd=5.0,
)
city_alerts.append( city_alerts.append(
{ {
"type": "price", "type": "price",
"market": f"{question} ({target_date or '今日'})", "market": f"{question} ({target_date or '今日'})",
"msg": f"{trigger_side}进入锁定区间 {trigger_price}¢", "msg": f"{trigger_side}进入锁定区间 {trigger_price}¢",
"bought": success,
} }
) )
pushed_signals[alert_key] = time.time() pushed_signals[alert_key] = time.time()
# 2. 市场异常
whale_sig = signal["factor_details"].get("whale", {})
volume_sig = signal["factor_details"].get("volume", {})
if (
whale_sig.get("signal")
in ["STRONG_ACCUMULATION", "STRONG_DISTRIBUTION"]
or volume_sig.get("volume_signal", {}).get("signal")
== "VOLUME_SPIKE"
):
anomaly_key = f"anomaly_{market_id}"
if anomaly_key not in pushed_signals:
msg = (
"检测到异常交易流"
if volume_sig.get("score", 0) > 0.7
else "大户入场"
)
city_alerts.append(
{
"type": "anomaly",
"market": f"{question} ({target_date or '今日'})",
"msg": f"{msg} (当前 {int(buy_yes_price * 100)}¢)",
}
)
pushed_signals[anomaly_key] = time.time()
# 3. 信号暂存 # 3. 信号暂存
cached_signals[market_id] = cache_entry cached_signals[market_id] = cache_entry
@@ -367,9 +341,6 @@ def main():
city, city_alerts, local_time=city_local_time city, city_alerts, local_time=city_local_time
) )
except Exception as e:
logger.error(f"分析城市 {city} 时出错: {e}")
continue
except Exception as e: except Exception as e:
logger.error(f"分析城市 {city} 时出错: {e}") logger.error(f"分析城市 {city} 时出错: {e}")
continue continue
@@ -416,11 +387,48 @@ def main():
with open("data/pushed_signals.json", "w", encoding="utf-8") as f: with open("data/pushed_signals.json", "w", encoding="utf-8") as f:
json.dump(pushed_signals, f, ensure_ascii=False) json.dump(pushed_signals, f, ensure_ascii=False)
# --- 4. 更新模拟仓位盈亏 ---
price_snapshot = {}
for mid, entry in all_markets_cache.items():
price_snapshot[mid] = {"price": entry["price"]}
paper_trader.update_pnl(price_snapshot)
# --- 5. 每日收益总结推送 (北京时间 23:55 - 00:05 之间发送) ---
now_bj = datetime.utcnow() + timedelta(hours=8)
if now_bj.hour == 23 and now_bj.minute >= 50:
summary_key = f"daily_pnl_{now_bj.strftime('%Y%m%d')}"
if summary_key not in pushed_signals:
# 构造总结消息
total_cost = 0
total_pnl = 0
data = paper_trader._load_data()
pos_list = data.get("positions", {})
if pos_list:
report = [
f"📊 <b>每日模拟仓结算总结 ({now_bj.strftime('%Y-%m-%d')})</b>\n"
+ "" * 15
]
for p in pos_list.values():
if p["status"] == "OPEN":
total_cost += p["cost_usd"]
total_pnl += p.get("pnl_usd", 0)
report.append(
f"💳 可用余额: <b>${data.get('balance', 0):.2f}</b>"
)
report.append(
f"💰 今日累计投入: <b>${total_cost:.2f}</b>"
)
report.append(
f"📈 累计浮动盈亏: <b>{total_pnl:+.2f}$</b>"
)
notifier._send_message("\n".join(report))
pushed_signals[summary_key] = time.time()
except Exception as e: except Exception as e:
logger.error(f"即时保存数据失败: {e}") logger.error(f"即时保存数据失败: {e}")
# 4. 每日概览已移除
logger.info("本轮扫描结束。等待 5 分钟...") logger.info("本轮扫描结束。等待 5 分钟...")
time.sleep(300) time.sleep(300)
-94
View File
@@ -183,38 +183,6 @@ class PolymarketClient:
return None return None
def get_buy_prices(self, yes_token_id: str, no_token_id: str) -> Optional[Dict]:
"""
获取买入价格 (Buy Yes 和 Buy No)
Args:
yes_token_id: Yes token ID
no_token_id: No token ID
Returns:
dict: {"buy_yes": float, "buy_no": float} 或 None
"""
try:
# Buy Yes = Yes token 的最佳卖单 (asks)
yes_book = self.get_orderbook(yes_token_id)
buy_yes = None
if yes_book and isinstance(yes_book, dict) and yes_book.get("asks"):
buy_yes = float(yes_book["asks"][0].get("price", 0))
# Buy No = No token 的最佳卖单 (asks)
no_book = self.get_orderbook(no_token_id)
buy_no = None
if no_book and isinstance(no_book, dict) and no_book.get("asks"):
buy_no = float(no_book["asks"][0].get("price", 0))
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"获取买入价格失败: {e}")
return None
def get_multiple_prices(self, token_requests: List[Dict]) -> Dict[str, float]: def get_multiple_prices(self, token_requests: List[Dict]) -> Dict[str, float]:
""" """
批量获取多个 token 的价格 (使用 Polymarket 批量接口) 批量获取多个 token 的价格 (使用 Polymarket 批量接口)
@@ -249,68 +217,6 @@ class PolymarketClient:
logger.debug(f"批量获取盘口价格失败: {e}") logger.debug(f"批量获取盘口价格失败: {e}")
return {} return {}
try:
url = f"{self.base_url}/prices"
# 这里的价格接口通常返回最佳买入/卖出价
# 构造请求体:Polymarket 期望的格式
payload = []
for req in token_requests:
payload.append(
{
"token_id": req["token_id"],
"side": "buy"
if req["side"] == "ask"
else "sell", # 映射:我们要买,所以查盘口的 sell side (ask)
}
)
# 分批处理,每批 50 个,避免请求过大
all_prices = {}
for i in range(0, len(payload), 50):
batch = payload[i : i + 50]
response = self.session.post(url, json=batch, timeout=20)
if response.status_code == 200:
results = response.json()
# 结果通常是一个字典 {token_id: price}
if isinstance(results, dict):
all_prices.update(results)
return all_prices
except Exception as e:
logger.debug(f"批量获取价格失败: {e}")
return {}
def get_trades(self, market_id: str = None, limit: int = 100) -> Optional[Dict]:
"""
获取成交历史 (使用 CLOB 专业接口 + Builder Key)
"""
try:
url = f"{self.base_url}/trades"
params = {"limit": limit}
if market_id:
params["market"] = market_id
# 关键:带上你的 Builder Key
headers = {}
if self.api_key:
headers["x-api-key"] = self.api_key
response = self.session.get(
url, params=params, headers=headers, timeout=self.timeout
)
if response.status_code == 200:
return response.json()
elif response.status_code == 401:
logger.debug(
f"CLOB Trades 依然返回 401 (权限受限): {market_id[:20]}..."
)
else:
logger.debug(f"CLOB Trades 接口返回状态码: {response.status_code}")
except Exception as e:
logger.debug(f"获取成交历史失败: {e}")
return None
def get_midpoint(self, token_id: str) -> Optional[float]: def get_midpoint(self, token_id: str) -> Optional[float]:
""" """
Get midpoint price for a token Get midpoint price for a token
+156
View File
@@ -0,0 +1,156 @@
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):
"""
开仓进入模拟仓位
"""
# 价格以美分计,转换为 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",
"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
+3 -3
View File
@@ -1,7 +1,7 @@
import sys import sys
from loguru import logger from loguru import logger
def setup_logger(): def setup_logger(level="DEBUG"):
""" """
Configure loguru logger Configure loguru logger
""" """
@@ -11,7 +11,7 @@ def setup_logger():
logger.add( logger.add(
sys.stderr, sys.stderr,
format="<green>{time:YYYY-MM-DD HH:mm:ss}</green> | <level>{level: <8}</level> | <level>{message}</level>", format="<green>{time:YYYY-MM-DD HH:mm:ss}</green> | <level>{level: <8}</level> | <level>{message}</level>",
level="DEBUG" level=level
) )
# 文件输出 # 文件输出
@@ -19,7 +19,7 @@ def setup_logger():
"data/logs/trading_system.log", "data/logs/trading_system.log",
rotation="10 MB", rotation="10 MB",
retention="10 days", retention="10 days",
level="DEBUG", level=level,
encoding="utf-8", encoding="utf-8",
compression="zip" compression="zip"
) )
+2 -1
View File
@@ -132,7 +132,8 @@ class TelegramNotifier:
items_text = "" items_text = ""
for a in alerts: for a in alerts:
type_icon = "" if a["type"] == "price" else "🐋" type_icon = "" if a["type"] == "price" else "🐋"
items_text += f"{type_icon} <b>{a['market']}</b>: {a['msg']}\n" buy_tag = " [🛒 模拟仓已买入]" if a.get("bought") else ""
items_text += f"{type_icon} <b>{a['market']}</b>: {a['msg']}{buy_tag}\n"
text = ( text = (
f"🔔 <b>城市监控报告 #{self._escape_html(city)}</b>\n\n" f"🔔 <b>城市监控报告 #{self._escape_html(city)}</b>\n\n"