feat: Introduce Polymarket API client, paper trading module, and essential utilities for market data and trading.
This commit is contained in:
@@ -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) 实验室**
|
||||
@@ -17,11 +17,12 @@ python run.py
|
||||
|
||||
## 🤖 电报机器人指令集
|
||||
|
||||
| 指令 | 描述 | 用法 |
|
||||
| :-------- | :--------------- | :---------------------------- |
|
||||
| `/signal` | **获取交易信号** | 返回当前最值得关注的 3 个档位 |
|
||||
| `/status` | **检查系统状态** | 确认监控引擎是否在线 |
|
||||
| `/help` | **指令帮助** | 显示所有可用指令 |
|
||||
| 指令 | 描述 | 用法 |
|
||||
| :----------- | :--------------- | :---------------------------- |
|
||||
| `/signal` | **获取交易信号** | 返回当前最值得关注的 3 个档位 |
|
||||
| `/portfolio` | **查看模拟仓位** | 获取实时模拟交易盈亏汇总报告 |
|
||||
| `/status` | **检查系统状态** | 确认监控引擎是否在线 |
|
||||
| `/help` | **指令帮助** | 显示所有可用指令 |
|
||||
|
||||
---
|
||||
|
||||
@@ -32,9 +33,10 @@ python run.py
|
||||
- **优化机制**: 同一轮扫描中,同一城市的所有异动将**合并为一条消息**发送,拒绝刷屏。
|
||||
- **触发内容**: 包含该城市下所有符合条件的“价格预警”与“市场异常”。
|
||||
|
||||
### 2. ⚡ 价格预警
|
||||
### 2. ⚡ 价格预警 (触发模拟买入)
|
||||
|
||||
- **触发条件**: Buy Yes 或 Buy No 价格在 **85¢-95¢** 区间。
|
||||
- **关联动作**: 系统会自动在该档位执行 **5 USD 的模拟开仓**,用于验证胜率。
|
||||
- **用途**: 高胜率/即将锁定区间提醒,适合平仓或收割。
|
||||
|
||||
### 3. 👀 市场异常
|
||||
@@ -42,7 +44,12 @@ python run.py
|
||||
- **大户入场**: 检测到单笔 >$5000 的大额交易且买卖比失衡。
|
||||
- **异常交易流**: 成交量突然放大 (>2倍历史标准差)。
|
||||
|
||||
### 4. 🎯 交易信号 (指令查询)
|
||||
### 4. 📅 每日盈亏总结
|
||||
|
||||
- **触发时间**: 北京时间 23:55 左右自动推送。
|
||||
- **内容**: 汇总当日所有模拟仓位的浮动盈亏、余额变动及胜率统计。
|
||||
|
||||
### 5. 🎯 交易信号 (指令查询)
|
||||
|
||||
- 对比气象预报与市场价格偏差。
|
||||
- 包含:城市、档位、当地时间、预期温度(含单位自适应)、偏差评分。
|
||||
@@ -69,10 +76,11 @@ HTTP_PROXY=http://127.0.0.1:7890
|
||||
## 📋 核心功能特性
|
||||
|
||||
- ✅ **智能合并推送**: 按城市汇总预警,界面整洁不刷屏。
|
||||
- ✅ **极速价格同步**: 采用批量 API 接口,一次请求同步全量城市盘口价,无延迟、无 404。
|
||||
- ✅ **北京时间适配**: 所有推送时间戳已自动转换为北京时间 (UTC+8)。
|
||||
- ✅ **全自动模拟交易**: 内置模拟仓位系统,支持 85-95¢ 区间自动跟单,记录实战胜率。
|
||||
- ✅ **极速价格同步**: 采用 Polymarket 批量 API 接口,一次同步全量城市,无延迟、无 404。
|
||||
- ✅ **北京时间适配**: 所有推送时间戳与每日总结均自动转换为北京时间 (UTC+8)。
|
||||
- ✅ **智能日期选择**: 自动定位最早的活跃市场日期,结算后自动顺延。
|
||||
- ✅ **温度单位自适应**: 美国市场自动切换华氏度 (°F),其他地区显示摄氏度 (°C)。
|
||||
- ✅ **全量数据持久化**: 信号记录与推送历史保存至本地 JSON,重启不丢失,不重复。
|
||||
- ✅ **温度单位自适应**: 美国市场切换华氏度 (°F),其他地区显示摄氏度 (°C)。
|
||||
- ✅ **全量数据持久化**: 信号记录、推送历史、交易仓位均保存至本地 JSON。
|
||||
|
||||
---
|
||||
|
||||
+16
-9
@@ -17,11 +17,12 @@ This command launches:
|
||||
|
||||
## 🤖 Telegram Bot Commands
|
||||
|
||||
| Command | Description | Usage |
|
||||
| :-------- | :---------------------- | :------------------------------------------- |
|
||||
| `/signal` | **Get Trading Signals** | Returns top 3 markets with highest deviation |
|
||||
| `/status` | **Check Status** | Confirm if the monitoring engine is online |
|
||||
| `/help` | **Help** | Display all available commands |
|
||||
| Command | Description | Usage |
|
||||
| :----------- | :---------------------- | :------------------------------------------- |
|
||||
| `/signal` | **Get Trading Signals** | Returns top 3 markets with highest deviation |
|
||||
| `/portfolio` | **View Portfolio** | Get real-time paper trading profit report |
|
||||
| `/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.
|
||||
- **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.
|
||||
- **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
|
||||
|
||||
- **Whale Inflow**: Detection of large single trades (>$5,000) with imbalanced buy/sell ratios.
|
||||
- **Volume Spikes**: Sudden increase in trading volume (>2x historical standard deviation).
|
||||
- **Whale Inflow**: Large single trades (>$5,000) with imbalanced ratios.
|
||||
- **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)
|
||||
|
||||
|
||||
+74
-9
@@ -28,9 +28,9 @@ def start_bot():
|
||||
"🌡️ <b>PolyWeather 监控机器人</b>\n\n"
|
||||
"可用指令:\n"
|
||||
"/signal - 获取当前高置信度交易信号\n"
|
||||
"/portfolio - 查看当前模拟交易报告\n"
|
||||
"/status - 检查监控系统状态\n"
|
||||
"/id - 获取当前聊天的 Chat ID\n\n"
|
||||
"💡 <b>直接输入城市名称</b> (如: <code>Seattle</code> 或 <code>London</code>) 即可查询该城市当天的最高温市场报价。"
|
||||
"/id - 获取当前聊天的 Chat ID"
|
||||
)
|
||||
bot.reply_to(message, welcome_text, parse_mode="HTML")
|
||||
|
||||
@@ -45,9 +45,6 @@ def start_bot():
|
||||
|
||||
@bot.message_handler(commands=["signal"])
|
||||
def get_signals(message):
|
||||
# 仅响应授权的 Chat ID (可选)
|
||||
# if str(message.chat.id) != str(chat_id): return
|
||||
|
||||
bot.send_message(message.chat.id, "🔍 正在检索当前最值得关注的天气信号...")
|
||||
|
||||
try:
|
||||
@@ -68,7 +65,7 @@ def start_bot():
|
||||
|
||||
# 按分数排序并取前 3 个
|
||||
sorted_signals = sorted(
|
||||
signals.values(), key=lambda x: x["score"], reverse=True
|
||||
signals.values(), key=lambda x: x.get("score", 0), reverse=True
|
||||
)[:3]
|
||||
|
||||
for s in sorted_signals:
|
||||
@@ -76,10 +73,10 @@ def start_bot():
|
||||
market_name=s["city"],
|
||||
full_title=s["full_title"],
|
||||
option=s["option"],
|
||||
score=round(s["score"] * 5, 1),
|
||||
score=round(s.get("score", 0) * 5, 1),
|
||||
prediction=s["prediction"],
|
||||
confidence=int(s["score"] * 100),
|
||||
analysis_list=[f"偏差解析: {s['rationale']}"],
|
||||
confidence=int(s.get("score", 0) * 100),
|
||||
analysis_list=[f"偏差解析: {s.get('rationale', 'N/A')}"],
|
||||
price=s["price"],
|
||||
market_url=s["url"],
|
||||
local_time=s["local_time"],
|
||||
@@ -90,6 +87,74 @@ def start_bot():
|
||||
except Exception as 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"])
|
||||
def get_status(message):
|
||||
bot.reply_to(
|
||||
|
||||
@@ -2,7 +2,7 @@ import sys
|
||||
import time
|
||||
import os
|
||||
import json
|
||||
from datetime import datetime
|
||||
from datetime import datetime, timedelta
|
||||
from loguru import logger
|
||||
|
||||
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.strategy.decision_engine import DecisionEngine
|
||||
from src.strategy.risk_manager import RiskManager
|
||||
from src.trading.paper_trader import PaperTrader
|
||||
from src.utils.notifier import TelegramNotifier
|
||||
|
||||
|
||||
def main():
|
||||
"""
|
||||
Polymarket 交易系统主循环 - 监控与推送模式
|
||||
"""
|
||||
# 1. 设置日志
|
||||
setup_logger()
|
||||
logger.info("正在启动 Polymarket 天气交易信号监控系统...")
|
||||
# 1. 初始化配置与日志
|
||||
config_data = load_config()
|
||||
setup_logger(config_data.get("app", {}).get("log_level", "INFO"))
|
||||
|
||||
# 2. 加载配置
|
||||
try:
|
||||
config_data = load_config()
|
||||
logger.info("配置加载成功。")
|
||||
except Exception as e:
|
||||
logger.error(f"配置加载失败: {e}")
|
||||
sys.exit(1)
|
||||
logger.info("🌟 PolyWeather 监控引擎启动中...")
|
||||
|
||||
# 3. 初始化组件
|
||||
# 2. 初始化核心组件
|
||||
polymarket = PolymarketClient(config_data["polymarket"])
|
||||
weather = WeatherDataCollector(config_data["weather"])
|
||||
onchain = OnchainTracker(config_data["polymarket"], polymarket)
|
||||
notifier = TelegramNotifier(config_data["telegram"])
|
||||
|
||||
# 3. 初始化分析与交易组件
|
||||
predictor = TemperaturePredictor()
|
||||
volume_analyzer = VolumeAnalyzer()
|
||||
orderbook_analyzer = OrderbookAnalyzer()
|
||||
tech_indicators = TechnicalIndicators()
|
||||
whale_tracker = WhaleTracker(config_data, onchain)
|
||||
|
||||
decision_engine = DecisionEngine(config_data)
|
||||
risk_manager = RiskManager(config_data)
|
||||
decision_engine = DecisionEngine(config_data.get("config", {}))
|
||||
whale_tracker = WhaleTracker(config_data.get("config", {}), onchain)
|
||||
paper_trader = PaperTrader()
|
||||
|
||||
# 发送启动通知
|
||||
notifier._send_message(
|
||||
@@ -226,7 +215,7 @@ def main():
|
||||
question = market.get("question", "未知市场")
|
||||
event_title = market.get("event_title", "")
|
||||
|
||||
# (日期处理逻辑保持不变...)
|
||||
# 识别该合约的目标日期
|
||||
target_date = weather.extract_date_from_title(
|
||||
event_title
|
||||
) or weather.extract_date_from_title(question)
|
||||
@@ -301,16 +290,13 @@ def main():
|
||||
"transactions": [],
|
||||
},
|
||||
weather_consensus={"average_temp": ref_temp},
|
||||
whale_activity=whale_tracker.analyze_market_whales(
|
||||
market_id
|
||||
),
|
||||
whale_activity=None,
|
||||
)
|
||||
cache_entry["score"] = signal["final_score"]
|
||||
cache_entry["rationale"] = signal.get("recommendation", "N/A")
|
||||
all_markets_cache[market_id] = cache_entry
|
||||
|
||||
# --- 预警收集 ---
|
||||
# 1. 价格预警
|
||||
# --- 预警收集 (仅监控价格) ---
|
||||
if (0.85 <= buy_yes_price <= 0.95) or (
|
||||
0.85 <= buy_no_price <= 0.95
|
||||
):
|
||||
@@ -324,40 +310,28 @@ def main():
|
||||
if trigger_side == "Buy Yes"
|
||||
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(
|
||||
{
|
||||
"type": "price",
|
||||
"market": f"{question} ({target_date or '今日'})",
|
||||
"msg": f"{trigger_side}进入锁定区间 {trigger_price}¢",
|
||||
"bought": success,
|
||||
}
|
||||
)
|
||||
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. 信号暂存
|
||||
cached_signals[market_id] = cache_entry
|
||||
|
||||
@@ -367,9 +341,6 @@ def main():
|
||||
city, city_alerts, local_time=city_local_time
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"分析城市 {city} 时出错: {e}")
|
||||
continue
|
||||
except Exception as e:
|
||||
logger.error(f"分析城市 {city} 时出错: {e}")
|
||||
continue
|
||||
@@ -416,11 +387,48 @@ def main():
|
||||
with open("data/pushed_signals.json", "w", encoding="utf-8") as f:
|
||||
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:
|
||||
logger.error(f"即时保存数据失败: {e}")
|
||||
|
||||
# 4. 每日概览已移除
|
||||
|
||||
logger.info("本轮扫描结束。等待 5 分钟...")
|
||||
time.sleep(300)
|
||||
|
||||
|
||||
@@ -183,38 +183,6 @@ class PolymarketClient:
|
||||
|
||||
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]:
|
||||
"""
|
||||
批量获取多个 token 的价格 (使用 Polymarket 批量接口)
|
||||
@@ -249,68 +217,6 @@ class PolymarketClient:
|
||||
logger.debug(f"批量获取盘口价格失败: {e}")
|
||||
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]:
|
||||
"""
|
||||
Get midpoint price for a token
|
||||
|
||||
@@ -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
@@ -1,7 +1,7 @@
|
||||
import sys
|
||||
from loguru import logger
|
||||
|
||||
def setup_logger():
|
||||
def setup_logger(level="DEBUG"):
|
||||
"""
|
||||
Configure loguru logger
|
||||
"""
|
||||
@@ -11,7 +11,7 @@ def setup_logger():
|
||||
logger.add(
|
||||
sys.stderr,
|
||||
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",
|
||||
rotation="10 MB",
|
||||
retention="10 days",
|
||||
level="DEBUG",
|
||||
level=level,
|
||||
encoding="utf-8",
|
||||
compression="zip"
|
||||
)
|
||||
|
||||
@@ -132,7 +132,8 @@ class TelegramNotifier:
|
||||
items_text = ""
|
||||
for a in alerts:
|
||||
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 = (
|
||||
f"🔔 <b>城市监控报告 #{self._escape_html(city)}</b>\n\n"
|
||||
|
||||
Reference in New Issue
Block a user