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 个档位 |
|
| `/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
@@ -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
@@ -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(
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
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"
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
Reference in New Issue
Block a user