Refactor market analysis and price fetching logic, remove orderbook analysis from the main loop, add new data collection and strategy modules, and update documentation.
This commit is contained in:
@@ -0,0 +1,71 @@
|
||||
# Polymarket Weather Market Discovery Technical Documentation
|
||||
|
||||
This document explains the technical implementation of how PolyWeather identifies and tracks weather markets on Polymarket.
|
||||
|
||||
## 1. Data Sources
|
||||
|
||||
We bypass high-level SDKs and interact directly with the **Polymarket Gamma API**, which is the primary metadata layer for Discovery.
|
||||
|
||||
- **Base URL:** `https://gamma-api.polymarket.com`
|
||||
- **Endpoint:** `/markets`
|
||||
|
||||
## 2. Discovery Strategy
|
||||
|
||||
The system uses a multi-layered search approach to ensure no city segments are missed.
|
||||
|
||||
### 2.1 Keyword Triple-Search
|
||||
|
||||
Instead of one query, we execute three concurrent search patterns:
|
||||
|
||||
1. `"highest temperature"`: Targets the primary question text.
|
||||
2. `"temperature in"`: Broad search for regional markets.
|
||||
3. `"daily weather"`: Fallback for markets with different naming conventions.
|
||||
|
||||
### 2.2 Prioritization
|
||||
|
||||
We apply specific sorting to find the **latest** available contracts (e.g., February 9th, 2026):
|
||||
|
||||
- `order=id` & `ascending=false`: Scans the newest created markets first.
|
||||
- `active=true` & `closed=false`: Filters out resolved or expired contracts.
|
||||
|
||||
## 3. Filtering & Parsing Logic
|
||||
|
||||
Since Polymarket hosts thousands of events, we apply a strict "Weather Filter" in the code:
|
||||
|
||||
### 3.1 Text Validation
|
||||
|
||||
We inspect both the `question` and the `slug`:
|
||||
|
||||
- **Pattern Match:** Must contain `"highest temperature in"` or `"highest-temperature-in"`.
|
||||
- **Exclusion:** (Implicitly handled by keyword search) filtered from sports or politics.
|
||||
|
||||
### 3.2 Negative Risk Market Handling
|
||||
|
||||
Weather markets on Polymarket are often structured as **Negative Risk** groups (where multiple outcomes like "70°F or higher" and "68-69°F" belong to one event).
|
||||
|
||||
**Technical Challenge:** In the API's list view, the `activeTokenId` field is often `null` for these complex markets.
|
||||
**Our Solution:**
|
||||
|
||||
1. Check `clobTokenIds`.
|
||||
2. If it's a JSON string (common in Gamma), parse it into a Python list.
|
||||
3. If `activeTokenId` is missing, we treat the first token ID in the list as the **"YES" Token**.
|
||||
4. This allows us to fetch the real-time orderbook/price even for markets that haven't fully "activated" in the front-end metadata.
|
||||
|
||||
## 4. Market Data Structure
|
||||
|
||||
Every market found is normalized into this structure for the Decision Engine:
|
||||
|
||||
- `condition_id`: The UMA condition ID for resolution.
|
||||
- `active_token_id`: The specific ERC1155 token ID we want to buy/monitor.
|
||||
- `group_id`: The `negRiskMarketID`, which allows the bot to understand that specific temperature ranges (e.g., 70°F vs 72°F) are related to the same city.
|
||||
- `slug`: Used for generating direct dashboard links.
|
||||
|
||||
## 5. Frequency & Caching
|
||||
|
||||
- **Discovery Frequency:** The system rescans for new cities/dates every **5 minutes**.
|
||||
- **Caching:** Found markets are stored in an internal memory cache (`_weather_markets_cache`) to reduce API pressure and avoid rate limits.
|
||||
|
||||
---
|
||||
|
||||
_Created on: 2026-02-07_
|
||||
_PolyWeather System Documentation_
|
||||
@@ -0,0 +1,71 @@
|
||||
# Polymarket 天气市场搜寻技术文档
|
||||
|
||||
本文档详细说明了 PolyWeather 如何在 Polymarket 上自动识别、筛选并跟踪天气相关市场的技术实现逻辑。
|
||||
|
||||
## 1. 数据来源
|
||||
|
||||
我们跳过了复杂的官方 SDK,直接与 **Polymarket Gamma API** 交互。这是 Polymarket 的官方元数据层,负责所有市场的发现与展示。
|
||||
|
||||
- **Base URL:** `https://gamma-api.polymarket.com`
|
||||
- **Endpoint:** `/markets`
|
||||
|
||||
## 2. 搜寻策略
|
||||
|
||||
由于 Polymarket 同时挂载数千个预测市场,系统采用多层搜索方案以确保不会遗漏任何城市的分段合约。
|
||||
|
||||
### 2.1 关键词三重搜索
|
||||
|
||||
程序并非只搜索一个词,而是并发执行三个搜索模式:
|
||||
|
||||
1. `"highest temperature"`: 匹配大多数天气问题的核心描述。
|
||||
2. `"temperature in"`: 针对特定地区市场的宽泛搜索。
|
||||
3. `"daily weather"`: 针对某些命名不规范市场的兜底搜索。
|
||||
|
||||
### 2.3 优先级与排序
|
||||
|
||||
为了确保能搜到**最新**发布的合约(例如 2026年2月9日 的市场),我们应用了特定的 API 排序参数:
|
||||
|
||||
- `order=id` & `ascending=false`: 优先扫描最新创建的市场 ID。
|
||||
- `active=true` & `closed=false`: 过滤掉已结算或已关闭的无效合约。
|
||||
|
||||
## 3. 过滤与解析逻辑
|
||||
|
||||
系统在获取 API 返回的列表后,会进行二次深度筛选:
|
||||
|
||||
### 3.1 文本校验
|
||||
|
||||
检查市场的 `question`(问题描述)和 `slug`(URL 路径):
|
||||
|
||||
- **模式匹配:** 必须包含 `"highest temperature in"` 或 `"highest-temperature-in"`。
|
||||
- **城市提取:** 逻辑会自动识别问题中的城市名(如 芝加哥、伦敦 等)。
|
||||
|
||||
### 3.2 负风险(Negative Risk)市场处理
|
||||
|
||||
Polymarket 的天气市场通常以 **Negative Risk** 分组形式存在(一个事件下包含多个互斥的区间,如“70°F以上”和“68-69°F”)。
|
||||
|
||||
**技术挑战:** 在 API 的列表视图中,这类市场的 `activeTokenId` 字段经常返回 `null`。
|
||||
**我们的解决方案:**
|
||||
|
||||
1. 检查 `clobTokenIds` 字段。
|
||||
2. 如果该字段是 JSON 字符串(Gamma API 的常见返回格式),则将其解析为 Python 列表。
|
||||
3. 如果 `activeTokenId` 缺失,我们将列表中的第一个 Token ID 视为 **"YES" Token**。
|
||||
4. 这使系统能够绕过元数据同步延迟,直接在 CLOB 层面抓取实时买入/卖出价格。
|
||||
|
||||
## 4. 市场规范化结构
|
||||
|
||||
每个搜寻到的分段都会被规范化为以下结构,供决策引擎(Decision Engine)使用:
|
||||
|
||||
- `condition_id`: 用于结果判定的 UMA 条件 ID。
|
||||
- `active_token_id`: 我们需要监控并买入的特定 ERC1155 Token ID。
|
||||
- `group_id`: 即 `negRiskMarketID`。这让机器人知道哪些不同的温度区间是属于同一个城市的,从而进行跨区间套利或对冲分析。
|
||||
- `slug`: 市场的唯一路径名,用于在 Telegram 预警中生成直接跳转链接。
|
||||
|
||||
## 5. 频率与缓存机制
|
||||
|
||||
- **搜寻频率:** 系统每 **5 分钟** 重新扫描一次新城市和新日期。
|
||||
- **缓存策略:** 搜寻到的市场会存入内存缓存(`_weather_markets_cache`),以减轻 API 压力并避免触发现速限制。
|
||||
|
||||
---
|
||||
|
||||
_创建日期: 2026-02-07_
|
||||
_PolyWeather 系统技术文档_
|
||||
+13
-5
@@ -19,7 +19,7 @@
|
||||
|
||||
## 🎯 四层风控仓位策略
|
||||
|
||||
系统结合 **Open-Meteo 天气预测**、**市场深度**、**结算时间** 和 **成交量** 自动决定仓位大小:
|
||||
系统结合 **Open-Meteo 天气预测**、**结算时间** 和 **成交量** 自动决定仓位大小:
|
||||
|
||||
| 条件组合 | 基础仓位 | 标签 | 说明 |
|
||||
|----------|----------|------|------|
|
||||
@@ -30,14 +30,13 @@
|
||||
|
||||
### 风控过滤规则
|
||||
|
||||
1. **流动性过滤**: 市场深度 < $50 跳过;深度 < 5×仓位则按比例缩减
|
||||
2. **时间衰减**:
|
||||
1. **时间衰减**:
|
||||
- ≤1小时: 停止建仓 (0%)
|
||||
- 1-4小时: 缩小至 40%
|
||||
- 4-12小时: 缩小至 70%
|
||||
- >12小时: 100%
|
||||
3. **预算上限**: 每日最高投入 $50
|
||||
4. **成交量加权**: 低活跃市场额外缩减 20%
|
||||
2. **预算上限**: 每日最高投入 $50
|
||||
3. **成交量加权**: 低活跃市场额外缩减 20%
|
||||
|
||||
### 天气支持判断逻辑
|
||||
|
||||
@@ -52,6 +51,15 @@
|
||||
• 预测温度19.0°C落在21°C区间,市场与模型一致
|
||||
```
|
||||
|
||||
### METAR 实测数据
|
||||
|
||||
当天结算的市场会额外显示机场实测数据,帮助验证预测准确性:
|
||||
```
|
||||
✈️ 机场实测 (KORD):
|
||||
🌡️ 32.0°F | 风速:12kt
|
||||
🕐 观测: 14:00 UTC
|
||||
```
|
||||
|
||||
## 📊 盈亏计算公式
|
||||
|
||||
- **持仓份额** = $5 / (买入价格 / 100)
|
||||
|
||||
@@ -40,12 +40,45 @@ This command launches:
|
||||
|
||||
## 🤖 Telegram Bot Commands
|
||||
|
||||
| Command | Description | Usage |
|
||||
| :----------- | :---------------------- | :--------------------------------------------- |
|
||||
| `/signal` | **Get Trading Signals** | Returns Top 5 markets with earliest settlement |
|
||||
| `/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 |
|
||||
| Command | Description | Usage |
|
||||
| :---------------- | :---------------------- | :--------------------------------------------- |
|
||||
| `/signal` | **Get Trading Signals** | Returns Top 5 markets with earliest settlement |
|
||||
| `/city [name]` | **Query City Details** | Get market info, forecast & live temperature |
|
||||
| `/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 |
|
||||
|
||||
### /city Command Example
|
||||
|
||||
```
|
||||
/city chicago
|
||||
```
|
||||
|
||||
Output:
|
||||
```
|
||||
📍 Chicago Market Details
|
||||
════════════════════
|
||||
|
||||
🕐 Local Time: 08:30
|
||||
|
||||
📊 Open-Meteo Forecast
|
||||
👉 Today: High 38°F
|
||||
02-08: High 42°F
|
||||
02-09: High 45°F
|
||||
|
||||
✈️ Airport Obs (KORD)
|
||||
🌡️ 32.0°F
|
||||
💨 Wind: 12kt
|
||||
🕐 Observed: 14:00 UTC
|
||||
|
||||
📅 2026-02-07 Forecast:38°F
|
||||
──────────────────
|
||||
🔥 40-41°F: No 94¢ →Buy NO
|
||||
🔥 38-39°F: Yes 91¢ →Buy YES
|
||||
⭐ 36-37°F: No 87¢ →Buy NO
|
||||
```
|
||||
|
||||
Supported abbreviations: `chi` (Chicago), `nyc` (New York), `atl` (Atlanta), `sea` (Seattle), `dal` (Dallas), `mia` (Miami)
|
||||
|
||||
---
|
||||
|
||||
@@ -80,29 +113,66 @@ The system automatically decides the position size based on **Open-Meteo Weather
|
||||
- **Push Format**:
|
||||
|
||||
```
|
||||
⚡ 40-41°F (2026-02-06): Buy No 87¢ | Prediction:38°F [🛒 $10.0 🔥High Conf]
|
||||
📍 Chicago Market Update
|
||||
🕐 Local 08:30 | Forecast High:38°F
|
||||
═══════════════════════
|
||||
|
||||
✈️ Airport Obs (KORD):
|
||||
🌡️ 32.0°F | Wind:12kt
|
||||
🕐 Observed: 14:00 UTC
|
||||
|
||||
⚡ 40-41°F (2026-02-07): Buy No 87¢ | Prediction:38°F [🛒 $10.0 🔥High Conf]
|
||||
|
||||
💡 Strategy Tips:
|
||||
• Predicted temp 38.0°C falls within 40-41°F range, market aligns with model
|
||||
• Predicted temp 38.0°F falls within 40-41°F range, market aligns with model
|
||||
```
|
||||
|
||||
### 2. ⚡ Price Alerts (Auto Paper Trade)
|
||||
### 2. ✈️ METAR Aviation Weather Data
|
||||
|
||||
For same-day settlement markets, the system fetches **METAR airport observation data** and displays real measurements:
|
||||
|
||||
```
|
||||
✈️ Airport Obs (KORD):
|
||||
🌡️ 12.0°F | Wind:15kt
|
||||
🕐 Observed: 11:00 UTC
|
||||
```
|
||||
|
||||
**ICAO Airport Code Mapping**:
|
||||
|
||||
| City | ICAO | Airport |
|
||||
| ------------- | ---- | ---------------------------- |
|
||||
| Seattle | KSEA | Seattle-Tacoma International |
|
||||
| London | EGLC | London City Airport |
|
||||
| Dallas | KDAL | Dallas Love Field |
|
||||
| Miami | KMIA | Miami International |
|
||||
| Atlanta | KATL | Hartsfield-Jackson Atlanta |
|
||||
| Chicago | KORD | O'Hare International |
|
||||
| New York | KLGA | LaGuardia Airport |
|
||||
| Seoul | RKSI | Incheon International |
|
||||
| Ankara | LTAC | Esenboga Airport |
|
||||
| Toronto | CYYZ | Pearson International |
|
||||
| Wellington | NZWN | Wellington International |
|
||||
| Buenos Aires | SAEZ | Ministro Pistarini |
|
||||
|
||||
**Data Source**: NOAA Aviation Weather Center (Free API, no key required)
|
||||
|
||||
### 3. ⚡ Price Alerts (Auto Paper Trade)
|
||||
|
||||
- **Trigger**: Buy Yes or Buy No price enters the **85¢-95¢** range.
|
||||
- **Auto Action**: System executes a **$3-$10 Paper Trade** based on the dynamic position strategy.
|
||||
- **Purpose**: High-probability / Near-settlement reminders.
|
||||
|
||||
### 3. 👀 Market Anomalies
|
||||
### 4. 👀 Market Anomalies
|
||||
|
||||
- **Whale Inflow**: Large single trades (>$5,000) with imbalanced ratios.
|
||||
- **Volume Spikes**: Sudden increase in volume (>2x historical std dev).
|
||||
|
||||
### 4. 📅 Daily PnL Summary
|
||||
### 5. 📅 Daily PnL Summary
|
||||
|
||||
- **Trigger**: Triggered automatically around 23:55 (Beijing Time).
|
||||
- **Content**: Summarizes daily floating PnL, balance changes, and win rate.
|
||||
|
||||
### 5. 🎯 Trading Signals (`/signal`)
|
||||
### 6. 🎯 Trading Signals (`/signal`)
|
||||
|
||||
Prioritizes markets with the **earliest settlement date**, sorted by opportunity value, returns **Top 5**:
|
||||
|
||||
|
||||
+82
-12
@@ -40,12 +40,45 @@ python3.11 run.py
|
||||
|
||||
## 🤖 电报机器人指令集
|
||||
|
||||
| 指令 | 描述 | 用法 |
|
||||
| :----------- | :--------------- | :---------------------------- |
|
||||
| `/signal` | **获取交易信号** | 返回最早结算市场的 Top 5 档位 |
|
||||
| `/portfolio` | **查看模拟仓位** | 获取实时模拟交易盈亏汇总报告 |
|
||||
| `/status` | **检查系统状态** | 确认监控引擎是否在线 |
|
||||
| `/help` | **指令帮助** | 显示所有可用指令 |
|
||||
| 指令 | 描述 | 用法 |
|
||||
| :---------------- | :--------------- | :---------------------------- |
|
||||
| `/signal` | **获取交易信号** | 返回最早结算市场的 Top 5 档位 |
|
||||
| `/city [城市名]` | **查询城市详情** | 获取指定城市的市场、预测和实测温度 |
|
||||
| `/portfolio` | **查看模拟仓位** | 获取实时模拟交易盈亏汇总报告 |
|
||||
| `/status` | **检查系统状态** | 确认监控引擎是否在线 |
|
||||
| `/help` | **指令帮助** | 显示所有可用指令 |
|
||||
|
||||
### /city 指令示例
|
||||
|
||||
```
|
||||
/city chicago
|
||||
```
|
||||
|
||||
返回内容:
|
||||
```
|
||||
📍 Chicago 市场详情
|
||||
════════════════════
|
||||
|
||||
🕐 当地时间: 08:30
|
||||
|
||||
📊 Open-Meteo 预测
|
||||
👉 今天: 最高 38°F
|
||||
02-08: 最高 42°F
|
||||
02-09: 最高 45°F
|
||||
|
||||
✈️ 机场实测 (KORD)
|
||||
🌡️ 32.0°F
|
||||
💨 风速: 12kt
|
||||
🕐 观测: 14:00 UTC
|
||||
|
||||
📅 2026-02-07 预测:38°F
|
||||
──────────────────
|
||||
🔥 40-41°F: No 94¢ →买NO
|
||||
🔥 38-39°F: Yes 91¢ →买YES
|
||||
⭐ 36-37°F: No 87¢ →买NO
|
||||
```
|
||||
|
||||
支持城市缩写: `chi` (Chicago), `nyc` (New York), `atl` (Atlanta), `sea` (Seattle), `dal` (Dallas), `mia` (Miami)
|
||||
|
||||
---
|
||||
|
||||
@@ -81,29 +114,66 @@ python3.11 run.py
|
||||
- **推送格式**:
|
||||
|
||||
```
|
||||
⚡ 40-41°F (2026-02-06): Buy No 87¢ | 预测:38°F [🛒 $10.0 🔥高置信]
|
||||
📍 Chicago 市场动态
|
||||
🕐 当地 08:30 | 预测最高:38°F
|
||||
═══════════════════════
|
||||
|
||||
✈️ 机场实测 (KORD):
|
||||
🌡️ 32.0°F | 风速:12kt
|
||||
🕐 观测: 14:00 UTC
|
||||
|
||||
⚡ 40-41°F (2026-02-07): Buy No 87¢ | 预测:38°F [🛒 $10.0 🔥高置信]
|
||||
|
||||
💡 策略建议:
|
||||
• 预测温度38.0°C落在40-41°F区间,市场与模型一致
|
||||
• 预测温度38.0°F落在40-41°F区间,市场与模型一致
|
||||
```
|
||||
|
||||
### 2. ⚡ 价格预警 (触发模拟买入)
|
||||
### 2. ✈️ METAR 航空气象实测数据
|
||||
|
||||
当天结算的市场会自动获取 **METAR 机场实测数据**,在预警中显示真实观测值:
|
||||
|
||||
```
|
||||
✈️ 机场实测 (KORD):
|
||||
🌡️ 12.0°F | 风速:15kt
|
||||
🕐 观测: 11:00 UTC
|
||||
```
|
||||
|
||||
**ICAO 机场代码映射表**:
|
||||
|
||||
| 城市 | ICAO | 机场名称 |
|
||||
| ------------- | ---- | --------------------------- |
|
||||
| Seattle | KSEA | Seattle-Tacoma International |
|
||||
| London | EGLC | London City Airport |
|
||||
| Dallas | KDAL | Dallas Love Field |
|
||||
| Miami | KMIA | Miami International |
|
||||
| Atlanta | KATL | Hartsfield-Jackson Atlanta |
|
||||
| Chicago | KORD | O'Hare International |
|
||||
| New York | KLGA | LaGuardia Airport |
|
||||
| Seoul | RKSI | Incheon International |
|
||||
| Ankara | LTAC | Esenboga Airport |
|
||||
| Toronto | CYYZ | Pearson International |
|
||||
| Wellington | NZWN | Wellington International |
|
||||
| Buenos Aires | SAEZ | Ministro Pistarini |
|
||||
|
||||
**数据源**: NOAA Aviation Weather Center (免费 API,无需 Key)
|
||||
|
||||
### 3. ⚡ 价格预警 (触发模拟买入)
|
||||
|
||||
- **触发条件**: Buy Yes 或 Buy No 价格在 **85¢-95¢** 区间。
|
||||
- **关联动作**: 系统根据智能仓位策略自动执行 **$3-$10** 的模拟开仓。
|
||||
- **用途**: 高胜率/即将锁定区间提醒,适合平仓或收割。
|
||||
|
||||
### 3. 👀 市场异常
|
||||
### 4. 👀 市场异常
|
||||
|
||||
- **大户入场**: 检测到单笔 >$5000 的大额交易且买卖比失衡。
|
||||
- **异常交易流**: 成交量突然放大 (>2倍历史标准差)。
|
||||
|
||||
### 4. 📅 每日盈亏总结
|
||||
### 5. 📅 每日盈亏总结
|
||||
|
||||
- **触发时间**: 北京时间 23:55 左右自动推送。
|
||||
- **内容**: 汇总当日所有模拟仓位的浮动盈亏、余额变动及胜率统计。
|
||||
|
||||
### 5. 🎯 交易信号 (`/signal` 指令)
|
||||
### 6. 🎯 交易信号 (`/signal` 指令)
|
||||
|
||||
优先显示**最早结算日期**的市场,按机会价值排序,返回 **Top 5** 档位:
|
||||
|
||||
|
||||
+218
-50
@@ -7,6 +7,7 @@ from datetime import datetime
|
||||
from src.utils.config_loader import load_config
|
||||
from src.utils.notifier import TelegramNotifier
|
||||
from src.data_collection.polymarket_api import PolymarketClient
|
||||
from src.data_collection.weather_sources import WeatherDataCollector
|
||||
|
||||
|
||||
def start_bot():
|
||||
@@ -20,6 +21,7 @@ def start_bot():
|
||||
|
||||
bot = telebot.TeleBot(bot_token)
|
||||
notifier = TelegramNotifier(config["telegram"])
|
||||
weather = WeatherDataCollector(config.get("weather", {}))
|
||||
|
||||
print(f"Bot is starting and listening for commands...")
|
||||
|
||||
@@ -29,9 +31,11 @@ def start_bot():
|
||||
"🌡️ <b>PolyWeather 监控机器人</b>\n\n"
|
||||
"可用指令:\n"
|
||||
"/signal - 获取当前高置信度交易信号\n"
|
||||
"/city [城市名] - 查询城市市场详情与天气\n"
|
||||
"/portfolio - 查看当前模拟交易报告\n"
|
||||
"/status - 检查监控系统状态\n"
|
||||
"/id - 获取当前聊天的 Chat ID"
|
||||
"/id - 获取当前聊天的 Chat ID\n\n"
|
||||
"示例: <code>/city chicago</code>"
|
||||
)
|
||||
bot.reply_to(message, welcome_text, parse_mode="HTML")
|
||||
|
||||
@@ -70,20 +74,22 @@ def start_bot():
|
||||
price = s.get("price", 50)
|
||||
if 5 <= price <= 95 and s.get("target_date"):
|
||||
active_signals.append(s)
|
||||
|
||||
|
||||
if not active_signals:
|
||||
bot.send_message(message.chat.id, "📭 当前没有值得关注的活跃市场。")
|
||||
return
|
||||
|
||||
# 按日期排序,优先最早结算的
|
||||
active_signals.sort(key=lambda x: x.get("target_date", "9999-99-99"))
|
||||
|
||||
|
||||
# 获取最早的日期
|
||||
earliest_date = active_signals[0].get("target_date")
|
||||
|
||||
|
||||
# 只取最早日期的市场
|
||||
earliest_markets = [s for s in active_signals if s.get("target_date") == earliest_date]
|
||||
|
||||
earliest_markets = [
|
||||
s for s in active_signals if s.get("target_date") == earliest_date
|
||||
]
|
||||
|
||||
# 按"机会价值"排序:接近锁定区间(85-95¢)的优先
|
||||
def opportunity_score(s):
|
||||
price = s.get("price", 50)
|
||||
@@ -97,16 +103,16 @@ def start_bot():
|
||||
return max_price # 接近锁定
|
||||
else:
|
||||
return max_price / 2 # 远离锁定
|
||||
|
||||
|
||||
earliest_markets.sort(key=opportunity_score, reverse=True)
|
||||
top_markets = earliest_markets[:5]
|
||||
|
||||
# 构建消息
|
||||
msg_lines = [
|
||||
f"🎯 <b>即将结算市场 ({earliest_date})</b>\n",
|
||||
f"共 {len(earliest_markets)} 个活跃选项\n"
|
||||
f"共 {len(earliest_markets)} 个活跃选项\n",
|
||||
]
|
||||
|
||||
|
||||
for i, s in enumerate(top_markets, 1):
|
||||
city = s.get("city", "Unknown")
|
||||
option = s.get("option", "Unknown")
|
||||
@@ -115,17 +121,18 @@ def start_bot():
|
||||
buy_no = s.get("buy_no", 100 - s.get("price", 50))
|
||||
volume = s.get("volume", 0)
|
||||
url = s.get("url", "")
|
||||
|
||||
|
||||
# 解析选项区间
|
||||
import re
|
||||
range_match = re.search(r'(\d+)-(\d+)', option)
|
||||
below_match = re.search(r'(\d+).*or below', option, re.I)
|
||||
higher_match = re.search(r'(\d+).*or higher', option, re.I)
|
||||
|
||||
|
||||
range_match = re.search(r"(\d+)-(\d+)", option)
|
||||
below_match = re.search(r"(\d+).*or below", option, re.I)
|
||||
higher_match = re.search(r"(\d+).*or higher", option, re.I)
|
||||
|
||||
# 判断预测与区间关系
|
||||
analysis = ""
|
||||
try:
|
||||
pred_val = float(re.search(r'[\d.]+', str(prediction)).group())
|
||||
pred_val = float(re.search(r"[\d.]+", str(prediction)).group())
|
||||
if range_match:
|
||||
low, high = int(range_match.group(1)), int(range_match.group(2))
|
||||
if pred_val < low:
|
||||
@@ -148,7 +155,7 @@ def start_bot():
|
||||
analysis = f"预测{pred_val}°低于{threshold}° → 买NO ✓"
|
||||
except:
|
||||
analysis = f"预测: {prediction}"
|
||||
|
||||
|
||||
# 判断最佳方向
|
||||
if buy_no >= 85:
|
||||
direction = f"Buy No {buy_no}¢"
|
||||
@@ -170,18 +177,18 @@ def start_bot():
|
||||
direction = f"Yes:{buy_yes}¢ No:{buy_no}¢"
|
||||
lock_status = "⚖️均衡"
|
||||
confidence = "📊"
|
||||
|
||||
|
||||
# 提取修复后的精确当地时间
|
||||
local_time = s.get("local_time", "")
|
||||
time_only = local_time.split(" ")[1] if " " in local_time else ""
|
||||
time_suffix = f" | 🕒{time_only}" if time_only else ""
|
||||
|
||||
|
||||
msg_lines.append(
|
||||
f"{confidence} <b>{i}. {city} {option}</b>\n"
|
||||
f" 💡 {analysis}\n"
|
||||
f" 📊 {direction} | {lock_status}{time_suffix}\n"
|
||||
)
|
||||
|
||||
|
||||
bot.send_message(message.chat.id, "\n".join(msg_lines), parse_mode="HTML")
|
||||
|
||||
except Exception as e:
|
||||
@@ -215,9 +222,9 @@ def start_bot():
|
||||
html_path = generate_portfolio_html(data)
|
||||
with open(html_path, "rb") as f:
|
||||
bot.send_document(
|
||||
message.chat.id,
|
||||
f,
|
||||
caption=f"📊 完整持仓报告 ({len(positions)}个持仓)\n💳 余额: ${balance:.2f}"
|
||||
message.chat.id,
|
||||
f,
|
||||
caption=f"📊 完整持仓报告 ({len(positions)}个持仓)\n💳 余额: ${balance:.2f}",
|
||||
)
|
||||
return
|
||||
|
||||
@@ -229,20 +236,28 @@ def start_bot():
|
||||
for pid, pos in positions.items():
|
||||
target_date = pos.get("target_date") or "未知"
|
||||
if target_date not in positions_by_date:
|
||||
positions_by_date[target_date] = {"count": 0, "pnl": 0, "cost": 0}
|
||||
positions_by_date[target_date] = {
|
||||
"count": 0,
|
||||
"pnl": 0,
|
||||
"cost": 0,
|
||||
}
|
||||
positions_by_date[target_date]["count"] += 1
|
||||
positions_by_date[target_date]["pnl"] += pos.get("pnl_usd", 0)
|
||||
positions_by_date[target_date]["cost"] += pos.get("cost_usd", 0)
|
||||
|
||||
|
||||
msg_lines.append(f"\n📌 <b>持仓概览</b> (共{len(positions)}个)")
|
||||
for target_date in sorted(positions_by_date.keys()):
|
||||
info = positions_by_date[target_date]
|
||||
icon = "📈" if info["pnl"] >= 0 else "📉"
|
||||
msg_lines.append(f"{icon} {target_date}: {info['count']}笔 ${info['cost']:.0f}投入 {info['pnl']:+.2f}$")
|
||||
|
||||
msg_lines.append(
|
||||
f"{icon} {target_date}: {info['count']}笔 ${info['cost']:.0f}投入 {info['pnl']:+.2f}$"
|
||||
)
|
||||
|
||||
total_pnl = sum(p.get("pnl_usd", 0) for p in positions.values())
|
||||
total_cost = sum(p.get("cost_usd", 0) for p in positions.values())
|
||||
msg_lines.append(f"<b>💰 合计: ${total_cost:.0f}投入 {total_pnl:+.2f}$</b>")
|
||||
msg_lines.append(
|
||||
f"<b>💰 合计: ${total_cost:.0f}投入 {total_pnl:+.2f}$</b>"
|
||||
)
|
||||
|
||||
msg_lines.append("\n📋 <b>最新持仓:</b>")
|
||||
recent_positions = list(positions.values())[-5:]
|
||||
@@ -251,14 +266,20 @@ def start_bot():
|
||||
icon = "🟢" if pnl >= 0 else "🔴"
|
||||
pred = pos.get("predicted_temp", "")
|
||||
pred_text = f"预测:{pred}" if pred else ""
|
||||
msg_lines.append(f"{icon} {pos['city']} {pos['option']} {pred_text} {pnl:+.2f}$")
|
||||
msg_lines.append(
|
||||
f"{icon} {pos['city']} {pos['option']} {pred_text} {pnl:+.2f}$"
|
||||
)
|
||||
|
||||
trades = data.get("trades", [])
|
||||
if trades:
|
||||
msg_lines.append("\n📝 <b>最近操作:</b>")
|
||||
for t in reversed(trades[-3:]):
|
||||
t_type = "🛒" if t["type"] == "BUY" else "💰"
|
||||
t_time = t.get("time", "").split(" ")[1] if " " in t.get("time", "") else ""
|
||||
t_time = (
|
||||
t.get("time", "").split(" ")[1]
|
||||
if " " in t.get("time", "")
|
||||
else ""
|
||||
)
|
||||
msg_lines.append(f"• {t_time} {t_type} {t['city']} {t['option']}")
|
||||
|
||||
if history:
|
||||
@@ -267,7 +288,9 @@ def start_bot():
|
||||
total_cost = sum(p.get("cost_usd", 0) for p in history)
|
||||
total_profit = sum(p.get("pnl_usd", 0) for p in history)
|
||||
win_rate = (wins / total_trades) * 100 if total_trades > 0 else 0
|
||||
msg_lines.append(f"\n📈 <b>历史:</b> {total_trades}笔 胜率{win_rate:.0f}% 盈亏{total_profit:+.2f}$")
|
||||
msg_lines.append(
|
||||
f"\n📈 <b>历史:</b> {total_trades}笔 胜率{win_rate:.0f}% 盈亏{total_profit:+.2f}$"
|
||||
)
|
||||
|
||||
msg_lines.append(f"\n💳 余额: <b>${balance:.2f}</b>")
|
||||
|
||||
@@ -276,15 +299,14 @@ def start_bot():
|
||||
except Exception as e:
|
||||
bot.reply_to(message, f"❌ 获取持仓失败: {e}")
|
||||
|
||||
|
||||
def generate_portfolio_html(data):
|
||||
"""生成漂亮的 HTML 持仓报告"""
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
|
||||
positions = data.get("positions", {})
|
||||
history = data.get("history", [])
|
||||
balance = data.get("balance", 1000.0)
|
||||
|
||||
|
||||
# 按日期分组
|
||||
positions_by_date = {}
|
||||
for pid, pos in positions.items():
|
||||
@@ -292,13 +314,13 @@ def start_bot():
|
||||
if target_date not in positions_by_date:
|
||||
positions_by_date[target_date] = []
|
||||
positions_by_date[target_date].append(pos)
|
||||
|
||||
|
||||
total_pnl = sum(p.get("pnl_usd", 0) for p in positions.values())
|
||||
total_cost = sum(p.get("cost_usd", 0) for p in positions.values())
|
||||
|
||||
|
||||
# 生成 HTML
|
||||
now_bj = (datetime.utcnow() + timedelta(hours=8)).strftime("%Y-%m-%d %H:%M")
|
||||
|
||||
|
||||
html = f"""<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
@@ -324,19 +346,19 @@ def start_bot():
|
||||
<div class="summary-item">💳 余额: <b>${balance:.2f}</b></div>
|
||||
<div class="summary-item">📦 持仓: <b>{len(positions)}</b> 个</div>
|
||||
<div class="summary-item">💰 投入: <b>${total_cost:.2f}</b></div>
|
||||
<div class="summary-item">📈 浮盈: <b class="{'positive' if total_pnl >= 0 else 'negative'}">{total_pnl:+.2f}$</b></div>
|
||||
<div class="summary-item">📈 浮盈: <b class="{"positive" if total_pnl >= 0 else "negative"}">{total_pnl:+.2f}$</b></div>
|
||||
</div>
|
||||
"""
|
||||
|
||||
|
||||
for target_date in sorted(positions_by_date.keys()):
|
||||
date_positions = positions_by_date[target_date]
|
||||
date_pnl = sum(p.get("pnl_usd", 0) for p in date_positions)
|
||||
date_cost = sum(p.get("cost_usd", 0) for p in date_positions)
|
||||
|
||||
|
||||
html += f"""
|
||||
<div class="date-header">
|
||||
📅 <b>{target_date}</b> | {len(date_positions)}笔 | 投入${date_cost:.0f} |
|
||||
<span class="{'positive' if date_pnl >= 0 else 'negative'}">{date_pnl:+.2f}$</span>
|
||||
<span class="{"positive" if date_pnl >= 0 else "negative"}">{date_pnl:+.2f}$</span>
|
||||
</div>
|
||||
<table>
|
||||
<tr><th>城市</th><th>选项</th><th>方向</th><th>入场</th><th>当前</th><th>预测</th><th>盈亏</th></tr>
|
||||
@@ -346,30 +368,29 @@ def start_bot():
|
||||
pnl_class = "positive" if pnl >= 0 else "negative"
|
||||
pred = pos.get("predicted_temp", "-")
|
||||
html += f""" <tr>
|
||||
<td>{pos.get('city', '-')}</td>
|
||||
<td>{pos.get('option', '-')}</td>
|
||||
<td>{pos.get('side', '-')}</td>
|
||||
<td>{pos.get('entry_price', 0)}¢</td>
|
||||
<td>{pos.get('current_price', 0)}¢</td>
|
||||
<td>{pos.get("city", "-")}</td>
|
||||
<td>{pos.get("option", "-")}</td>
|
||||
<td>{pos.get("side", "-")}</td>
|
||||
<td>{pos.get("entry_price", 0)}¢</td>
|
||||
<td>{pos.get("current_price", 0)}¢</td>
|
||||
<td>{pred}</td>
|
||||
<td class="{pnl_class}">{pnl:+.2f}$</td>
|
||||
</tr>
|
||||
"""
|
||||
html += " </table>\n"
|
||||
|
||||
|
||||
html += f"""
|
||||
<div class="footer">
|
||||
生成时间: {now_bj} (北京时间) | PolyWeather Monitor
|
||||
</div>
|
||||
</body>
|
||||
</html>"""
|
||||
|
||||
|
||||
html_path = "data/portfolio_report.html"
|
||||
with open(html_path, "w", encoding="utf-8") as f:
|
||||
f.write(html)
|
||||
|
||||
return html_path
|
||||
|
||||
return html_path
|
||||
|
||||
@bot.message_handler(commands=["status"])
|
||||
def get_status(message):
|
||||
@@ -377,10 +398,157 @@ def start_bot():
|
||||
message, "✅ 监控引擎正在运行中...\n7x24h 实时扫码 Polymarket 气温市场。"
|
||||
)
|
||||
|
||||
@bot.message_handler(commands=["city"])
|
||||
def get_city_info(message):
|
||||
"""查询指定城市的市场详情、天气预测和实时温度"""
|
||||
try:
|
||||
# 解析城市名称
|
||||
parts = message.text.split(maxsplit=1)
|
||||
if len(parts) < 2:
|
||||
bot.reply_to(
|
||||
message,
|
||||
"❓ 请输入城市名称\n\n用法: <code>/city chicago</code>\n\n"
|
||||
"支持城市: Seattle, London, Dallas, Miami, Atlanta, Chicago, "
|
||||
"New York, Seoul, Ankara, Toronto, Wellington, Buenos Aires",
|
||||
parse_mode="HTML",
|
||||
)
|
||||
return
|
||||
|
||||
city_input = parts[1].strip().lower()
|
||||
|
||||
# 城市别名映射
|
||||
city_aliases = {
|
||||
"nyc": "new york",
|
||||
"ny": "new york",
|
||||
"la": "los angeles",
|
||||
"chi": "chicago",
|
||||
"atl": "atlanta",
|
||||
"sea": "seattle",
|
||||
"dal": "dallas",
|
||||
"mia": "miami",
|
||||
"tor": "toronto",
|
||||
"ank": "ankara",
|
||||
"sel": "seoul",
|
||||
"wel": "wellington",
|
||||
"ba": "buenos aires",
|
||||
"buenosaires": "buenos aires",
|
||||
"伦敦": "london",
|
||||
"纽约": "new york",
|
||||
"西雅图": "seattle",
|
||||
"芝加哥": "chicago",
|
||||
"多伦多": "toronto",
|
||||
"首尔": "seoul",
|
||||
"惠灵顿": "wellington",
|
||||
"达拉斯": "dallas",
|
||||
"亚特兰大": "atlanta",
|
||||
}
|
||||
city_name = city_aliases.get(city_input, city_input)
|
||||
|
||||
bot.send_message(
|
||||
message.chat.id, f"🔍 正在查询 {city_name.title()} 的市场信息..."
|
||||
)
|
||||
|
||||
# 1. 获取城市坐标
|
||||
coords = weather.get_coordinates(city_name)
|
||||
if not coords:
|
||||
bot.reply_to(message, f"❌ 未找到城市: {city_name}")
|
||||
return
|
||||
|
||||
# 2. 获取天气数据 (Open-Meteo + METAR)
|
||||
weather_data = weather.fetch_all_sources(
|
||||
city_name, lat=coords["lat"], lon=coords["lon"]
|
||||
)
|
||||
|
||||
# 3. 从缓存中获取该城市的市场数据
|
||||
city_markets = []
|
||||
if os.path.exists("data/active_signals.json"):
|
||||
with open("data/active_signals.json", "r", encoding="utf-8") as f:
|
||||
all_signals = json.load(f)
|
||||
city_markets = [
|
||||
s
|
||||
for s in all_signals
|
||||
if s.get("city", "").lower() == city_name.lower()
|
||||
]
|
||||
|
||||
# 4. 构建消息
|
||||
msg_lines = [f"📍 <b>{city_name.title()} 市场详情</b>"]
|
||||
msg_lines.append("═" * 20)
|
||||
|
||||
# 天气信息
|
||||
open_meteo = weather_data.get("open-meteo", {})
|
||||
metar = weather_data.get("metar", {})
|
||||
|
||||
temp_unit = open_meteo.get("unit", "celsius")
|
||||
temp_symbol = "°F" if temp_unit == "fahrenheit" else "°C"
|
||||
|
||||
# 当前时间
|
||||
local_time = open_meteo.get("current", {}).get("local_time", "")
|
||||
if local_time:
|
||||
time_only = (
|
||||
local_time.split(" ")[1] if " " in local_time else local_time
|
||||
)
|
||||
msg_lines.append(f"🕐 当地时间: {time_only}")
|
||||
|
||||
# Open-Meteo 预测
|
||||
daily = open_meteo.get("daily", {})
|
||||
dates = daily.get("time", [])
|
||||
max_temps = daily.get("temperature_2m_max", [])
|
||||
|
||||
today_str = datetime.now().strftime("%Y-%m-%d")
|
||||
|
||||
msg_lines.append(f"\n📊 <b>Open-Meteo 预测</b>")
|
||||
for i, (d, t) in enumerate(zip(dates[:7], max_temps[:7])):
|
||||
day_label = "今天" if d == today_str else d[5:] # MM-DD
|
||||
is_today = "👉 " if d == today_str else " "
|
||||
msg_lines.append(f"{is_today}{day_label}: 最高 {t}{temp_symbol}")
|
||||
|
||||
# METAR 实测
|
||||
if metar:
|
||||
icao = metar.get("icao", "")
|
||||
metar_temp = metar.get("current", {}).get("temp")
|
||||
wind_speed = metar.get("current", {}).get("wind_speed_kt")
|
||||
obs_time = metar.get("observation_time", "")
|
||||
|
||||
# 解析观测时间
|
||||
if obs_time:
|
||||
try:
|
||||
obs_dt = datetime.fromisoformat(obs_time.replace("Z", "+00:00"))
|
||||
obs_time_str = obs_dt.strftime("%H:%M UTC")
|
||||
except:
|
||||
obs_time_str = obs_time[:16] if len(obs_time) > 16 else obs_time
|
||||
else:
|
||||
obs_time_str = "N/A"
|
||||
|
||||
msg_lines.append(f"\n✈️ <b>机场实测 ({icao})</b>")
|
||||
if metar_temp is not None:
|
||||
msg_lines.append(f" 🌡️ {metar_temp}{temp_symbol}")
|
||||
if wind_speed is not None:
|
||||
msg_lines.append(f" 💨 风速: {wind_speed}kt")
|
||||
msg_lines.append(f" 🕐 观测: {obs_time_str}")
|
||||
|
||||
# 市场信息已根据需求暂时移除
|
||||
# (已在此处删除了之前的市场数据处理逻辑)
|
||||
|
||||
# 发送消息
|
||||
final_msg = "\n".join(msg_lines)
|
||||
if len(final_msg) > 4000:
|
||||
# 消息太长,分段发送
|
||||
bot.send_message(message.chat.id, final_msg[:4000], parse_mode="HTML")
|
||||
bot.send_message(message.chat.id, final_msg[4000:], parse_mode="HTML")
|
||||
else:
|
||||
bot.send_message(message.chat.id, final_msg, parse_mode="HTML")
|
||||
|
||||
except Exception as e:
|
||||
import traceback
|
||||
|
||||
traceback.print_exc()
|
||||
bot.reply_to(message, f"❌ 查询失败: {e}")
|
||||
|
||||
import logging
|
||||
|
||||
# 强制关闭 telebot 内部的刷屏日志
|
||||
telebot.logger.setLevel(logging.CRITICAL)
|
||||
|
||||
|
||||
while True:
|
||||
try:
|
||||
bot.infinity_polling(timeout=60, long_polling_timeout=60)
|
||||
|
||||
-21
@@ -1,21 +0,0 @@
|
||||
import requests
|
||||
import os
|
||||
from dotenv import load_dotenv
|
||||
|
||||
load_dotenv()
|
||||
|
||||
def get_updates():
|
||||
token = os.getenv("TELEGRAM_BOT_TOKEN")
|
||||
proxy = os.getenv("HTTPS_PROXY")
|
||||
proxies = {"http": proxy, "https": proxy} if proxy else None
|
||||
|
||||
url = f"https://api.telegram.org/bot{token}/getUpdates"
|
||||
try:
|
||||
resp = requests.get(url, proxies=proxies)
|
||||
data = resp.json()
|
||||
print(f"Updates: {data}")
|
||||
except Exception as e:
|
||||
print(f"Error: {e}")
|
||||
|
||||
if __name__ == "__main__":
|
||||
get_updates()
|
||||
@@ -12,9 +12,6 @@ from src.data_collection.polymarket_api import PolymarketClient
|
||||
from src.data_collection.weather_sources import WeatherDataCollector
|
||||
from src.data_collection.onchain_tracker import OnchainTracker
|
||||
from src.models.statistical_model import TemperaturePredictor
|
||||
from src.analysis.volume_analyzer import VolumeAnalyzer
|
||||
from src.analysis.orderbook_analyzer import OrderbookAnalyzer
|
||||
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
|
||||
@@ -38,7 +35,6 @@ def main():
|
||||
# 3. 初始化分析与交易组件
|
||||
predictor = TemperaturePredictor()
|
||||
risk_manager = RiskManager(config_data.get("config", {}))
|
||||
orderbook_analyzer = OrderbookAnalyzer(config_data.get("config", {}))
|
||||
decision_engine = DecisionEngine(config_data.get("config", {}))
|
||||
whale_tracker = WhaleTracker(config_data.get("config", {}), onchain)
|
||||
paper_trader = PaperTrader()
|
||||
@@ -139,16 +135,18 @@ def main():
|
||||
|
||||
active_tid = m.get("active_token_id")
|
||||
|
||||
# 如果是多选一市场(比如 Dallas 76-77°F)
|
||||
if len(ts) > 2 and active_tid:
|
||||
# 智能识别买入/买否 Token
|
||||
if active_tid and isinstance(ts, list):
|
||||
# 获取该档位的买入价 (Ask)
|
||||
price_requests.append({"token_id": active_tid, "side": "ask"})
|
||||
# 获取该档位的买入“否”价所需的 Bid 价
|
||||
price_requests.append({"token_id": active_tid, "side": "bid"})
|
||||
# 如果是传统的 Yes/No 二选一市场
|
||||
elif len(ts) == 2:
|
||||
price_requests.append({"token_id": ts[0], "side": "ask"}) # Buy Yes
|
||||
price_requests.append({"token_id": ts[1], "side": "ask"}) # Buy No
|
||||
|
||||
if len(ts) == 2:
|
||||
# 传统的二选一,直接获取 No Token 的 Ask
|
||||
no_tid = ts[1] if ts[0] == active_tid else ts[0]
|
||||
price_requests.append({"token_id": no_tid, "side": "ask"})
|
||||
else:
|
||||
# 多选一,需要用 1 - Bid(Yes) 来模拟 Buy No
|
||||
price_requests.append({"token_id": active_tid, "side": "bid"})
|
||||
|
||||
if price_requests:
|
||||
logger.info(f"正在同步 {len(price_requests)} 个档位的真实盘口价格...")
|
||||
@@ -157,13 +155,13 @@ def main():
|
||||
|
||||
# 3. 按城市分组(按condition_id去重)
|
||||
markets_by_city = {}
|
||||
seen_condition_ids = set()
|
||||
|
||||
seen_condition_ids = set() # Initialize seen_condition_ids here
|
||||
for i, m in enumerate(all_weather_markets):
|
||||
c_id = m.get("condition_id")
|
||||
if c_id in seen_condition_ids:
|
||||
continue # 跳过重复
|
||||
seen_condition_ids.add(c_id)
|
||||
# Use condition_id + active_token_id as unique key to support multi-bracket markets
|
||||
unique_market_key = f"{m.get('condition_id')}_{m.get('active_token_id')}"
|
||||
if unique_market_key in seen_condition_ids:
|
||||
continue
|
||||
seen_condition_ids.add(unique_market_key)
|
||||
|
||||
# 注入实时批量价格
|
||||
ts = m.get("tokens", [])
|
||||
@@ -175,24 +173,24 @@ def main():
|
||||
|
||||
active_tid = m.get("active_token_id")
|
||||
|
||||
# 多选一市场逻辑
|
||||
if len(ts) > 2 and active_tid:
|
||||
if active_tid and isinstance(ts, list):
|
||||
m["buy_yes_live"] = token_price_map.get(f"{active_tid}:ask")
|
||||
# 买入“否”的价格 = 1 - 该档位的 Bid
|
||||
bid_val = token_price_map.get(f"{active_tid}:bid")
|
||||
if bid_val:
|
||||
m["buy_no_live"] = 1.0 - bid_val
|
||||
# 二选一市场逻辑
|
||||
elif len(ts) == 2:
|
||||
m["buy_yes_live"] = token_price_map.get(f"{ts[0]}:ask")
|
||||
m["buy_no_live"] = token_price_map.get(f"{ts[1]}:ask")
|
||||
|
||||
if len(ts) == 2:
|
||||
no_tid = ts[1] if ts[0] == active_tid else ts[0]
|
||||
m["buy_no_live"] = token_price_map.get(f"{no_tid}:ask")
|
||||
else:
|
||||
# 1 - Bid(Yes) = Ask(No)
|
||||
bid_val = token_price_map.get(f"{active_tid}:bid")
|
||||
if bid_val:
|
||||
m["buy_no_live"] = 1.0 - bid_val
|
||||
|
||||
# 优先使用发现阶段已经识别出的城市名
|
||||
city = m.get("city")
|
||||
|
||||
# 如果发现阶段没识别出,再尝试从问题文本提取
|
||||
# 如果发现阶段没识别出,再尝试从问题文本或 Slug 提取
|
||||
if not city or city == "Unknown":
|
||||
full_context = f"{m.get('event_title', '')} {m.get('question', '')}"
|
||||
full_context = f"{m.get('event_title', '')} {m.get('question', '')} {m.get('slug', '')}"
|
||||
city = weather.extract_city_from_question(full_context)
|
||||
|
||||
if i < 5:
|
||||
@@ -341,34 +339,21 @@ def main():
|
||||
# 严格触发条件: 价格必须处于 85-95¢ 区间 (真正的高概率信号)
|
||||
yes_in_range = buy_yes_price and 0.85 <= buy_yes_price <= 0.95
|
||||
no_in_range = buy_no_price and 0.85 <= buy_no_price <= 0.95
|
||||
|
||||
|
||||
# 50¢ 保护:价格接近 50% 说明市场无明确方向,跳过
|
||||
is_undecided = 0.45 <= current_prob <= 0.55
|
||||
|
||||
|
||||
if (yes_in_range or no_in_range) and not is_undecided:
|
||||
alert_key = f"alert_{market_id}_{int(current_prob * 100)}"
|
||||
if alert_key not in pushed_signals:
|
||||
# 深度分析订单簿
|
||||
ob_data = (
|
||||
polymarket.get_orderbook(active_tid)
|
||||
if active_tid
|
||||
else None
|
||||
# 获取温度符号(在此处定义以便后续使用)
|
||||
temp_unit = weather_data.get("open-meteo", {}).get(
|
||||
"unit", "celsius"
|
||||
)
|
||||
ob_analysis = (
|
||||
orderbook_analyzer.analyze(ob_data)
|
||||
if ob_data
|
||||
else {
|
||||
"tradeable": False,
|
||||
"liquidity": "枯竭",
|
||||
"spread": 0,
|
||||
"mid_price": current_prob,
|
||||
}
|
||||
temp_symbol = (
|
||||
"°F" if temp_unit == "fahrenheit" else "°C"
|
||||
)
|
||||
|
||||
# 获取温度符号(在此处定义以便后续使用)
|
||||
temp_unit = weather_data.get("open-meteo", {}).get("unit", "celsius")
|
||||
temp_symbol = "°F" if temp_unit == "fahrenheit" else "°C"
|
||||
|
||||
# 预测偏差分析
|
||||
if ref_temp:
|
||||
city_pred_high = ref_temp # 记录到城市概览
|
||||
@@ -391,7 +376,6 @@ def main():
|
||||
f"预测温度{ref_temp}{temp_symbol}落在{question}区间,市场与模型一致"
|
||||
)
|
||||
|
||||
|
||||
# 模拟下单 - 使用 Ask 价格(实际可成交价格)
|
||||
if buy_yes_price and buy_yes_price > 0.5:
|
||||
trigger_side = "Buy Yes"
|
||||
@@ -405,10 +389,14 @@ def main():
|
||||
)
|
||||
|
||||
# 构建预测文本
|
||||
forecast_text = f"{ref_temp}{temp_symbol}" if ref_temp else "N/A"
|
||||
|
||||
forecast_text = (
|
||||
f"{ref_temp}{temp_symbol}" if ref_temp else "N/A"
|
||||
)
|
||||
|
||||
# 构建简约版消息
|
||||
side_display = "Buy No" if trigger_side == "Buy No" else "Buy Yes"
|
||||
side_display = (
|
||||
"Buy No" if trigger_side == "Buy No" else "Buy Yes"
|
||||
)
|
||||
msg = f"⚡ {question} ({target_date}): {side_display} {trigger_price}¢ | 预测:{forecast_text}"
|
||||
|
||||
success = paper_trader.open_position(
|
||||
@@ -421,7 +409,7 @@ def main():
|
||||
target_date=target_date,
|
||||
predicted_temp=ref_temp,
|
||||
)
|
||||
|
||||
|
||||
# 添加模拟交易标签
|
||||
if success:
|
||||
msg += " [🛒 $5.0 💡试探]"
|
||||
@@ -534,10 +522,8 @@ def main():
|
||||
if is_categorical:
|
||||
# 语义转换逻辑保持一致
|
||||
if buy_no_price and buy_no_price >= 0.85:
|
||||
trigger_side = "Sell Yes"
|
||||
trigger_price = int(
|
||||
buy_no_price * 100
|
||||
) # 预估价
|
||||
trigger_side = "Buy No" # 直接统一为 Buy No
|
||||
trigger_price = int(buy_no_price * 100)
|
||||
else:
|
||||
trigger_side = "Buy Yes"
|
||||
trigger_price = int(buy_yes_price * 100)
|
||||
@@ -551,84 +537,6 @@ def main():
|
||||
else int(buy_no_price * 100)
|
||||
)
|
||||
|
||||
# --- 深度流动性与 Spread 检查 ---
|
||||
target_tid = (
|
||||
active_tid
|
||||
if is_categorical
|
||||
else (ts[0] if trigger_side == "Buy Yes" else ts[1])
|
||||
)
|
||||
ob_data = (
|
||||
polymarket.get_orderbook(target_tid)
|
||||
if target_tid
|
||||
else None
|
||||
)
|
||||
|
||||
ob_analysis = {
|
||||
"tradeable": True,
|
||||
"liquidity": "未知",
|
||||
"spread": 0,
|
||||
"mid_price": trigger_price / 100,
|
||||
}
|
||||
if ob_data:
|
||||
ob_analysis = orderbook_analyzer.analyze(ob_data)
|
||||
|
||||
if not ob_analysis.get("tradeable", True):
|
||||
confidence_tag = (
|
||||
f"🔴不可交易 ({ob_analysis.get('liquidity')})"
|
||||
)
|
||||
if not is_categorical:
|
||||
logger.warning(
|
||||
f"跳过不可交易信号 (Spread {ob_analysis.get('spread')}): {city} {question}"
|
||||
)
|
||||
continue
|
||||
|
||||
# 更新实时数据显示
|
||||
mid_c = round(ob_analysis.get("mid_price", 0) * 100, 1)
|
||||
spr_c = round(ob_analysis.get("spread", 0) * 100, 1)
|
||||
depth = ob_analysis.get(
|
||||
"ask_depth"
|
||||
if trigger_side.startswith("Buy")
|
||||
else "bid_depth",
|
||||
0,
|
||||
)
|
||||
|
||||
# 流动性图标
|
||||
liq_map = {
|
||||
"充裕": "✅ 充裕",
|
||||
"正常": "🟡 正常",
|
||||
"稀薄": "🟠 稀薄",
|
||||
"枯竭": "🔴 枯竭",
|
||||
}
|
||||
liq_status = liq_map.get(
|
||||
ob_analysis.get("liquidity", "未知"), "❓ 未知"
|
||||
)
|
||||
|
||||
if is_categorical:
|
||||
ask_str = (
|
||||
"--"
|
||||
if trigger_side == "Sell Yes"
|
||||
else f"{trigger_price}¢"
|
||||
)
|
||||
bid_str = (
|
||||
f"{trigger_price}¢"
|
||||
if trigger_side == "Sell Yes"
|
||||
else "--"
|
||||
)
|
||||
|
||||
display_side = (
|
||||
f"📊 <b>{question}</b>\n"
|
||||
f"Ask: {ask_str} | Bid: {bid_str} | Mid: {mid_c}¢\n"
|
||||
f"Spread: {spr_c}¢ | 深度: ${depth}\n"
|
||||
f"流动性: {liq_status}"
|
||||
)
|
||||
else:
|
||||
display_side = (
|
||||
f"📊 <b>{question}</b>\n"
|
||||
f"报价: {trigger_side} {trigger_price}¢ | Mid: {mid_c}¢\n"
|
||||
f"Spread: {spr_c}¢ | 深度: ${depth}\n"
|
||||
f"流动性: {liq_status}"
|
||||
)
|
||||
|
||||
# --- 智能动态仓位计算 ---
|
||||
# 1. 获取 Open-Meteo 对目标日期的最高温预测
|
||||
predicted_high = None
|
||||
@@ -745,11 +653,10 @@ def main():
|
||||
elif trigger_price >= 92:
|
||||
base_pos, confidence_tag = 5.0, "📌价格锁定"
|
||||
|
||||
# 4. 四层过滤决策
|
||||
# 4. 仓位决策
|
||||
amount_usd, risk_reason = (
|
||||
risk_manager.calculate_position_size(
|
||||
base_confidence_usd=base_pos,
|
||||
depth=depth,
|
||||
hours_to_settle=hours_to_settle,
|
||||
is_high_relative_volume=is_rel_high_vol,
|
||||
)
|
||||
@@ -758,7 +665,7 @@ def main():
|
||||
logger.info(
|
||||
f"【Pro仓位】{city} {question} | "
|
||||
f"基础:{base_pos}$ -> 最终:{amount_usd}$ | 原因:{risk_reason} | "
|
||||
f"深度:${depth} | 剩:{hours_to_settle:.1f}h"
|
||||
f"剩:{hours_to_settle:.1f}h"
|
||||
)
|
||||
|
||||
# --- 模拟交易触发逻辑 ---
|
||||
@@ -798,9 +705,7 @@ def main():
|
||||
)
|
||||
|
||||
# 构建简约版消息: ⚡ {question} ({date}): {side} {price}¢ | 预测:{forecast} [🛒 ${amount} {tag}]
|
||||
side_display = (
|
||||
"Buy No" if trigger_side == "Buy No" else "Buy Yes"
|
||||
)
|
||||
side_display = trigger_side
|
||||
msg = (
|
||||
f"⚡ {question} ({target_date}): {side_display} {trigger_price}¢ | "
|
||||
f"预测:{forecast_text} [🛒 ${amount_usd} {confidence_tag}]"
|
||||
@@ -825,17 +730,28 @@ def main():
|
||||
if city_alerts:
|
||||
# 去重策略建议
|
||||
unique_tips = list(dict.fromkeys(city_strategy_tips))
|
||||
notifier.send_combined_alert(
|
||||
city=city,
|
||||
alerts=city_alerts,
|
||||
local_time=city_local_time,
|
||||
forecast_temp=f"{city_pred_high}{temp_symbol}"
|
||||
if city_pred_high
|
||||
else "N/A",
|
||||
total_volume=city_total_vol,
|
||||
brackets_count=len(city_markets),
|
||||
strategy_tips=unique_tips,
|
||||
# 获取 METAR 数据(仅当天结算的市场才显示)
|
||||
today_str = datetime.now().strftime("%Y-%m-%d")
|
||||
# 检查是否有当天结算的市场
|
||||
has_today_market = any(
|
||||
a.get("market") == today_str or a.get("market") == "今日"
|
||||
for a in city_alerts
|
||||
)
|
||||
metar_data = (
|
||||
weather_data.get("metar") if has_today_market else None
|
||||
)
|
||||
# notifier.send_combined_alert(
|
||||
# city=city,
|
||||
# alerts=city_alerts,
|
||||
# local_time=city_local_time,
|
||||
# forecast_temp=f"{city_pred_high}{temp_symbol}"
|
||||
# if city_pred_high
|
||||
# else "N/A",
|
||||
# total_volume=city_total_vol,
|
||||
# brackets_count=len(city_markets),
|
||||
# strategy_tips=unique_tips,
|
||||
# metar_data=metar_data,
|
||||
# )
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"分析城市 {city} 时出错: {e}")
|
||||
@@ -844,13 +760,17 @@ def main():
|
||||
# --- 周期性结算:保存高价值信号 ---
|
||||
active_signals = []
|
||||
for mid, entry in all_markets_cache.items():
|
||||
# 核心过滤:只有 ACTIVE 且 价格未锁定、日期未过期的才进入 signals 列表
|
||||
if entry.get("rationale") not in ["ENDED", "EXPIRED", "ERROR"]:
|
||||
# 再次双重检查日期 (硬核拦截 2026-02-06)
|
||||
target_dt = entry.get("target_date")
|
||||
if target_dt and target_dt < "2026-02-06":
|
||||
continue
|
||||
active_signals.append(entry)
|
||||
# Relaxed filtering: Let the bot decide, but mark ENDED
|
||||
rationale = entry.get("rationale")
|
||||
if rationale == "ERROR":
|
||||
continue
|
||||
|
||||
target_dt = entry.get("target_date")
|
||||
# Only filter out truly ancient history
|
||||
if target_dt and target_dt < "2026-02-01":
|
||||
continue
|
||||
|
||||
active_signals.append(entry)
|
||||
|
||||
# 按分数排序
|
||||
active_signals.sort(key=lambda x: x.get("score", 0), reverse=True)
|
||||
@@ -926,7 +846,7 @@ def main():
|
||||
report.append(
|
||||
f"📈 累计浮动盈亏: <b>{total_pnl:+.2f}$</b>"
|
||||
)
|
||||
notifier._send_message("\n".join(report))
|
||||
# notifier._send_message("\n".join(report))
|
||||
pushed_signals[summary_key] = time.time()
|
||||
|
||||
except Exception as e:
|
||||
|
||||
@@ -4,5 +4,4 @@ pyTelegramBotAPI
|
||||
python-dotenv
|
||||
pytz
|
||||
numpy
|
||||
py-clob-client
|
||||
web3
|
||||
|
||||
@@ -7,587 +7,285 @@ from loguru import logger
|
||||
from datetime import datetime
|
||||
from concurrent.futures import ThreadPoolExecutor
|
||||
|
||||
from py_clob_client.client import ClobClient
|
||||
from py_clob_client.constants import POLYGON
|
||||
from py_clob_client.clob_types import ApiCreds, BookParams, OpenOrderParams
|
||||
|
||||
|
||||
class PolymarketClient:
|
||||
"""
|
||||
Polymarket API Client for market data and trading (Exclusive py-clob-client mode)
|
||||
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.base_url = config.get("base_url", "https://clob.polymarket.com")
|
||||
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 分钟缓存
|
||||
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"正在使用代理: {proxy}")
|
||||
logger.info(f"Requests session using proxy: {proxy}")
|
||||
|
||||
# 设置公开接口通用的 User-Agent
|
||||
# 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"
|
||||
}
|
||||
)
|
||||
|
||||
# 初始化官方 CLOB 客户端
|
||||
self.api_key = config.get("api_key")
|
||||
self.api_secret = config.get("api_secret")
|
||||
self.api_passphrase = config.get("api_passphrase")
|
||||
|
||||
try:
|
||||
|
||||
# 组装凭据对象 (如果提供)
|
||||
creds = None
|
||||
if self.api_key and self.api_secret:
|
||||
creds = ApiCreds(
|
||||
api_key=self.api_key,
|
||||
api_secret=self.api_secret,
|
||||
api_passphrase=self.api_passphrase
|
||||
)
|
||||
|
||||
self.clob_client = ClobClient(
|
||||
host=self.base_url,
|
||||
key=None, # 除非有 0x 开头的私钥,否则传入 None 以避免报错
|
||||
creds=creds,
|
||||
chain_id=POLYGON
|
||||
)
|
||||
# 注入代理到官方客户端 (官方库使用 httpx 或 requests)
|
||||
if proxy:
|
||||
# 尝试给官方 client 的内部 session 设置代理 (取决于版本实现)
|
||||
try:
|
||||
if hasattr(self.clob_client, 'session'):
|
||||
self.clob_client.session.proxies = {"http": proxy, "https": proxy}
|
||||
except: pass
|
||||
|
||||
logger.info("✅ 官方 py-clob-client 已满血上线,Requests 模式已彻底退役。")
|
||||
except Exception as e:
|
||||
logger.error(f"官方客户端启动失败: {e}")
|
||||
raise RuntimeError("必须安装并配置正确的 py-clob-client 才能运行。")
|
||||
|
||||
self._setup_headers()
|
||||
logger.info(f"Polymarket 客户端初始化完成。Base URL: {self.base_url}")
|
||||
|
||||
def _setup_headers(self):
|
||||
"""Setup default headers for API requests"""
|
||||
self.session.headers.update(
|
||||
{"Content-Type": "application/json", "Accept": "application/json"}
|
||||
)
|
||||
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:
|
||||
return self.clob_client.get_markets(next_cursor=next_cursor)
|
||||
except: return None
|
||||
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:
|
||||
return self.clob_client.get_market(market_id=market_id)
|
||||
except: return None
|
||||
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]:
|
||||
"""
|
||||
获取 Token 的实时盘口价格
|
||||
优先使用官方库,失败时使用直接 REST API
|
||||
"""
|
||||
# 方法1: 尝试官方库
|
||||
"""Fetch real-time price for a token via CLOB REST API"""
|
||||
try:
|
||||
sdk_side = "BUY" if side == "ask" else "SELL"
|
||||
price_str = self.clob_client.get_price(token_id=token_id, side=sdk_side)
|
||||
if price_str:
|
||||
return float(price_str)
|
||||
except Exception as e:
|
||||
logger.debug(f"官方库 get_price 失败 ({token_id}): {e}")
|
||||
|
||||
# 方法2: 直接调用 REST API (与 test_price.py 一致)
|
||||
try:
|
||||
resp = self.session.get(f"{self.base_url}/price", params={
|
||||
"token_id": token_id,
|
||||
"side": side.upper()
|
||||
}, timeout=10)
|
||||
# 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))
|
||||
return float(data.get("price", 0)) if resp.status_code == 200 else None
|
||||
except Exception as e:
|
||||
logger.debug(f"REST API get_price 失败 ({token_id}): {e}")
|
||||
logger.debug(f"get_price failed ({token_id}): {e}")
|
||||
return None
|
||||
|
||||
def get_orderbook(self, token_id: str) -> Optional[Dict]:
|
||||
"""
|
||||
获取订单簿深度
|
||||
优先使用官方库,失败时使用直接 REST API
|
||||
"""
|
||||
# 方法1: 尝试官方库
|
||||
"""Fetch orderbook for a token via CLOB REST API"""
|
||||
try:
|
||||
return self.clob_client.get_orderbook(token_id=token_id)
|
||||
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 失败 ({token_id}): {e}")
|
||||
|
||||
# 方法2: 直接调用 REST API (与 test_price.py 一致)
|
||||
try:
|
||||
resp = self.session.get(f"{self.base_url}/book", params={
|
||||
"token_id": token_id
|
||||
}, timeout=10)
|
||||
return resp.json()
|
||||
except Exception as e:
|
||||
logger.debug(f"REST API get_orderbook 失败 ({token_id}): {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]:
|
||||
"""
|
||||
获取买入价格 (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
|
||||
"""
|
||||
"""Fetch buy prices for both YES and NO tokens"""
|
||||
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")
|
||||
and len(yes_book["asks"]) > 0
|
||||
):
|
||||
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")
|
||||
and len(no_book["asks"]) > 0
|
||||
):
|
||||
buy_no = float(no_book["asks"][0].get("price", 0))
|
||||
# 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"获取买入价格失败: {e}")
|
||||
|
||||
logger.debug(f"get_buy_prices failed: {e}")
|
||||
return None
|
||||
|
||||
def get_multiple_prices(self, token_requests: List[Dict]) -> Dict[str, float]:
|
||||
"""
|
||||
批量获取多个 token 的价格
|
||||
优先使用官方库,失败时使用直接 REST API
|
||||
"""
|
||||
"""Batch fetch prices for multiple tokens using ThreadPoolExecutor"""
|
||||
if not token_requests:
|
||||
return {}
|
||||
|
||||
all_prices = {}
|
||||
batch_size = 20
|
||||
|
||||
def robust_float(val):
|
||||
if isinstance(val, (int, float)): return float(val)
|
||||
if isinstance(val, str):
|
||||
try: return float(val)
|
||||
except: return 0.0
|
||||
return 0.0
|
||||
try: return float(val)
|
||||
except: return 0.0
|
||||
|
||||
chunks = [token_requests[i : i + batch_size] for i in range(0, len(token_requests), batch_size)]
|
||||
|
||||
def fetch_chunk_sdk(chunk):
|
||||
"""使用官方 SDK 批量获取"""
|
||||
try:
|
||||
batch_req = []
|
||||
for r in chunk:
|
||||
sdk_side = "BUY" if r.get("side") == "ask" else "SELL"
|
||||
batch_req.append(BookParams(token_id=r["token_id"], side=sdk_side))
|
||||
|
||||
results = self.clob_client.get_prices(batch_req)
|
||||
|
||||
chunk_prices = {}
|
||||
if isinstance(results, list):
|
||||
for item in results:
|
||||
tid = item.get("token_id")
|
||||
price_raw = item.get("price")
|
||||
res_side = item.get("side")
|
||||
if tid and price_raw:
|
||||
val = robust_float(price_raw)
|
||||
key_side = "ask" if res_side == "BUY" else "bid"
|
||||
chunk_prices[f"{tid}:{key_side}"] = val
|
||||
return chunk_prices
|
||||
except Exception as e:
|
||||
logger.debug(f"SDK batch fetch failed: {e}")
|
||||
return None
|
||||
|
||||
def fetch_chunk_rest(chunk):
|
||||
"""使用 REST API 逐个获取 (备用方案)"""
|
||||
chunk_prices = {}
|
||||
for r in chunk:
|
||||
try:
|
||||
resp = self.session.get(f"{self.base_url}/price", params={
|
||||
"token_id": r["token_id"],
|
||||
"side": r.get("side", "ask").upper()
|
||||
}, timeout=10)
|
||||
data = resp.json()
|
||||
price = robust_float(data.get("price", 0))
|
||||
if price > 0:
|
||||
chunk_prices[f"{r['token_id']}:{r.get('side', 'ask')}"] = price
|
||||
except Exception as e:
|
||||
logger.debug(f"REST API price fetch failed for {r['token_id'][:16]}...: {e}")
|
||||
return chunk_prices
|
||||
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=3) as executor:
|
||||
future_results = list(executor.map(fetch_chunk_sdk, chunks))
|
||||
|
||||
# 检查结果,如果 SDK 全部失败,使用 REST API 备用
|
||||
sdk_success = False
|
||||
for chunk_result in future_results:
|
||||
if chunk_result:
|
||||
all_prices.update(chunk_result)
|
||||
sdk_success = True
|
||||
|
||||
# 如果 SDK 完全失败,使用 REST API
|
||||
if not sdk_success and token_requests:
|
||||
logger.info("SDK 批量获取失败,使用 REST API 逐个获取...")
|
||||
with ThreadPoolExecutor(max_workers=5) as executor:
|
||||
future_results = list(executor.map(fetch_chunk_rest, chunks))
|
||||
for chunk_result in future_results:
|
||||
all_prices.update(chunk_result)
|
||||
|
||||
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:
|
||||
res = self.clob_client.get_midpoint(token_id)
|
||||
if res and "mid" in res:
|
||||
return float(res["mid"])
|
||||
except: pass
|
||||
return None
|
||||
|
||||
def search_markets(self, query: str) -> Optional[Dict]:
|
||||
"""
|
||||
搜索市场
|
||||
"""
|
||||
try:
|
||||
# Note: The py-clob-client's get_markets method does not directly support a 'query' parameter for searching.
|
||||
# It primarily supports pagination (next_cursor).
|
||||
# This implementation will fetch the first page of markets and return them.
|
||||
# A more robust search would involve fetching all markets and filtering locally,
|
||||
# or using a different API endpoint if available.
|
||||
return self.clob_client.get_markets(next_cursor=None)
|
||||
except Exception as e:
|
||||
logger.error(f"搜索市场失败: {e}")
|
||||
return None
|
||||
|
||||
# --- 交易指令 (强依赖官方库) ---
|
||||
def create_order(
|
||||
self,
|
||||
token_id: str,
|
||||
side: str,
|
||||
price: float,
|
||||
size: float,
|
||||
order_type: str = "GTC",
|
||||
) -> Optional[Dict]:
|
||||
"""
|
||||
创建新订单
|
||||
"""
|
||||
try:
|
||||
# 转换方向
|
||||
side_val = "BUY" if side.upper() == "BUY" else "SELL"
|
||||
# 使用官方签名下单 (SDK 会自动处理签名)
|
||||
return self.clob_client.create_order(
|
||||
token_id=token_id,
|
||||
price=price,
|
||||
size=size,
|
||||
side=side_val
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(f"下单失败: {e}")
|
||||
return None
|
||||
|
||||
def cancel_order(self, order_id: str) -> Optional[Dict]:
|
||||
"""
|
||||
取消订单
|
||||
"""
|
||||
try:
|
||||
return self.clob_client.cancel_order(order_id)
|
||||
except Exception as e:
|
||||
logger.error(f"取消订单失败: {e}")
|
||||
return None
|
||||
|
||||
def get_orders(self, market_id: str = None) -> Optional[Dict]:
|
||||
"""
|
||||
获取当前活跃挂单
|
||||
"""
|
||||
try:
|
||||
params = OpenOrderParams(market=market_id) if market_id else None
|
||||
return self.clob_client.get_orders(params=params)
|
||||
except Exception as e:
|
||||
logger.error(f"获取挂单失败: {e}")
|
||||
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"使用缓存的市场列表 (剩余寿命: {int(self._cache_ttl - (current_time - self._last_discovery_time))}s)")
|
||||
logger.debug(f"Using cached market list ({len(self._weather_markets_cache)} items)")
|
||||
return self._weather_markets_cache
|
||||
|
||||
logger.info("📡 正在全量扫描 Polymarket 发现天气市场...")
|
||||
gamma_url = "https://gamma-api.polymarket.com/events"
|
||||
logger.info("📡 Scanning Polymarket via Gamma API for weather markets...")
|
||||
all_weather_markets = []
|
||||
seen_condition_ids = set()
|
||||
|
||||
def process_events(events, source_label):
|
||||
if not isinstance(events, list):
|
||||
return
|
||||
|
||||
new_markets_count = 0
|
||||
for event in events:
|
||||
title = event.get("title", "")
|
||||
is_weather_event = (
|
||||
"Highest temperature" in title or "temperature in" in title.lower()
|
||||
)
|
||||
|
||||
event_slug = event.get("slug", "")
|
||||
for m in event.get("markets", []):
|
||||
question = m.get("groupItemTitle") or m.get("question") or ""
|
||||
|
||||
# 强化过滤:必须在标题中包含明确的气温气象词,且排除非气温市场
|
||||
t_lower = title.lower()
|
||||
q_lower = question.lower()
|
||||
|
||||
# 1. 标题必须像个气温市场
|
||||
if not any(k in t_lower for k in ["highest temperature", "high temperature", "will temperature", "daily temperature"]):
|
||||
continue
|
||||
|
||||
# 2. 排除干扰项
|
||||
if "climate" in t_lower or "rain" in t_lower or "snow" in t_lower:
|
||||
continue
|
||||
|
||||
# 3. 确保这个具体的 market (bracket) 是我们想要的
|
||||
if not any(k in q_lower for k in ["temperature", "be", "highest", "range"]):
|
||||
# 补充:如果是多选一市场的子项,question 可能只是一个数字或范围,此时看 title
|
||||
if not any(k in t_lower for k in ["temperature", "highest"]):
|
||||
continue
|
||||
|
||||
c_id = m.get("conditionId")
|
||||
# 识别 outcome_index
|
||||
t_ids = m.get("clobTokenIds", [])
|
||||
active_id = m.get("activeTokenId")
|
||||
idx = 0
|
||||
if isinstance(t_ids, list) and active_id in t_ids:
|
||||
idx = t_ids.index(active_id)
|
||||
|
||||
# 对于多选一市场,不同档位共享 conditionId,但 tokenId 不同
|
||||
unique_key = f"{c_id}_{active_id}"
|
||||
if c_id and unique_key not in seen_condition_ids:
|
||||
all_weather_markets.append(
|
||||
{
|
||||
"condition_id": c_id,
|
||||
"question": question,
|
||||
"active_token_id": active_id,
|
||||
"outcome_index": idx,
|
||||
"tokens": t_ids,
|
||||
"prices": m.get("outcomePrices"),
|
||||
"event_title": title,
|
||||
"slug": event_slug,
|
||||
}
|
||||
)
|
||||
seen_condition_ids.add(unique_key)
|
||||
new_markets_count += 1
|
||||
if new_markets_count > 0:
|
||||
logger.debug(f"[{source_label}] 发现 {new_markets_count} 个新市场合约")
|
||||
|
||||
seen_keys = set()
|
||||
|
||||
# 1. Target newest markets by query and ID sorting
|
||||
search_queries = ["highest temperature", "temperature in", "daily weather"]
|
||||
|
||||
try:
|
||||
# 1. 扫描活跃且未合并的 (全量)
|
||||
for offset in range(0, 20000, 1000):
|
||||
params = {
|
||||
"active": "true",
|
||||
"closed": "false",
|
||||
"limit": 1000,
|
||||
"offset": offset,
|
||||
}
|
||||
|
||||
# 增加重试机制
|
||||
success = False
|
||||
for retry in range(3):
|
||||
try:
|
||||
response = self.session.get(
|
||||
gamma_url, params=params, timeout=self.timeout
|
||||
)
|
||||
if response.status_code == 200:
|
||||
events = response.json()
|
||||
if not events:
|
||||
break
|
||||
process_events(events, f"Open-O{offset}")
|
||||
success = True
|
||||
break
|
||||
else:
|
||||
logger.warning(f"Gamma API 状态码异常 ({response.status_code}),第 {retry+1} 次重试...")
|
||||
except Exception as e:
|
||||
logger.warning(f"发现市场请求出错: {e},第 {retry+1} 次重试...")
|
||||
time.sleep(2)
|
||||
|
||||
if not success:
|
||||
break
|
||||
# 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
|
||||
|
||||
# 2. 扫描活跃但已关闭的
|
||||
for offset in range(0, 20000, 1000):
|
||||
params = {
|
||||
"active": "true",
|
||||
"closed": "true",
|
||||
"limit": 1000,
|
||||
"offset": offset,
|
||||
}
|
||||
|
||||
success = False
|
||||
for retry in range(3):
|
||||
try:
|
||||
response = self.session.get(
|
||||
gamma_url, params=params, timeout=self.timeout
|
||||
)
|
||||
if response.status_code == 200:
|
||||
events = response.json()
|
||||
if not events:
|
||||
break
|
||||
process_events(events, f"Closed-O{offset}")
|
||||
success = True
|
||||
break
|
||||
except Exception as e:
|
||||
logger.warning(f"发现关闭市场请求出错: {e},第 {retry+1} 次重试...")
|
||||
time.sleep(2)
|
||||
|
||||
if not success:
|
||||
break
|
||||
|
||||
# 3. 扫描非活跃但未关闭的市场
|
||||
for offset in range(0, 10000, 1000):
|
||||
params = {
|
||||
"active": "false",
|
||||
"closed": "false",
|
||||
"limit": 1000,
|
||||
"offset": offset,
|
||||
}
|
||||
response = self.session.get(
|
||||
gamma_url, params=params, timeout=self.timeout
|
||||
)
|
||||
if response.status_code == 200:
|
||||
events = response.json()
|
||||
if not events:
|
||||
break
|
||||
process_events(events, f"Inactive-O{offset}")
|
||||
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
|
||||
|
||||
# 4. 扫描非活跃且已关闭的市场(某些即将结算的市场可能在这里)
|
||||
for offset in range(0, 10000, 1000):
|
||||
params = {
|
||||
"active": "false",
|
||||
"closed": "true",
|
||||
"limit": 1000,
|
||||
"offset": offset,
|
||||
}
|
||||
response = self.session.get(
|
||||
gamma_url, params=params, timeout=self.timeout
|
||||
)
|
||||
if response.status_code == 200:
|
||||
events = response.json()
|
||||
if not events:
|
||||
break
|
||||
process_events(events, f"InactiveClosed-O{offset}")
|
||||
else:
|
||||
break
|
||||
|
||||
logger.info(
|
||||
f"全量发现结束,共获取 {len(all_weather_markets)} 个天气档位合约"
|
||||
)
|
||||
|
||||
# 更新缓存
|
||||
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"全量发现天气市场失败: {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 get_event_by_slug(self, slug: str) -> Optional[Dict]:
|
||||
"""
|
||||
通过slug直接获取特定事件(用于捕获部分结算等特殊状态的市场)
|
||||
"""
|
||||
try:
|
||||
url = f"{self.base_url.replace('clob', 'gamma-api')}/events"
|
||||
params = {"slug": slug}
|
||||
response = self.session.get(url, params=params, timeout=self.timeout)
|
||||
|
||||
if response.status_code == 200:
|
||||
events = response.json()
|
||||
if events and len(events) > 0:
|
||||
return events[0]
|
||||
except Exception as e:
|
||||
logger.debug(f"通过slug获取事件失败 ({slug}): {e}")
|
||||
return None
|
||||
|
||||
def find_weather_market(self, city: str, date_str: str = None) -> Optional[Dict]:
|
||||
"""
|
||||
根据城市和日期精准查找
|
||||
"""
|
||||
weather_markets = self.get_weather_markets()
|
||||
for m in weather_markets:
|
||||
content = (
|
||||
str(m.get("question", "")) + str(m.get("event_title", ""))
|
||||
).lower()
|
||||
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
|
||||
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", ""))).lower()
|
||||
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
|
||||
|
||||
@@ -13,8 +13,27 @@ class WeatherDataCollector:
|
||||
- OpenWeatherMap (free, fast updates)
|
||||
- Weather Underground (Polymarket settlement source)
|
||||
- Visual Crossing (rich historical data)
|
||||
- NOAA Aviation Weather (METAR - airport observations)
|
||||
"""
|
||||
|
||||
# Polymarket 12 个天气市场对应的 ICAO 机场代码
|
||||
# 这些是 Weather Underground 结算源使用的气象站
|
||||
CITY_TO_ICAO = {
|
||||
"seattle": "KSEA", # Seattle-Tacoma Airport
|
||||
"london": "EGLC", # London City Airport
|
||||
"dallas": "KDAL", # Dallas Love Field
|
||||
"miami": "KMIA", # Miami International
|
||||
"atlanta": "KATL", # Hartsfield-Jackson
|
||||
"chicago": "KORD", # O'Hare International
|
||||
"new york": "KLGA", # LaGuardia Airport
|
||||
"nyc": "KLGA", # Alias
|
||||
"seoul": "RKSI", # Incheon International
|
||||
"ankara": "LTAC", # Esenboğa International
|
||||
"toronto": "CYYZ", # Toronto Pearson
|
||||
"wellington": "NZWN", # Wellington International
|
||||
"buenos aires": "SAEZ", # Ezeiza International
|
||||
}
|
||||
|
||||
def __init__(self, config: dict):
|
||||
self.config = config
|
||||
self.wunderground_key = config.get("wunderground_api_key")
|
||||
@@ -167,6 +186,113 @@ class WeatherDataCollector:
|
||||
logger.error(f"Visual Crossing request failed: {e}")
|
||||
return None
|
||||
|
||||
def get_icao_code(self, city: str) -> Optional[str]:
|
||||
"""
|
||||
根据城市名获取对应的 ICAO 机场代码
|
||||
"""
|
||||
normalized = city.lower().strip()
|
||||
|
||||
# 直接匹配
|
||||
if normalized in self.CITY_TO_ICAO:
|
||||
return self.CITY_TO_ICAO[normalized]
|
||||
|
||||
# 模糊匹配
|
||||
for key, icao in self.CITY_TO_ICAO.items():
|
||||
if key in normalized or normalized in key:
|
||||
return icao
|
||||
|
||||
return None
|
||||
|
||||
def fetch_metar(self, city: str, use_fahrenheit: bool = False) -> Optional[Dict]:
|
||||
"""
|
||||
从 NOAA Aviation Weather Center 获取 METAR 航空气象数据
|
||||
|
||||
这是 Polymarket 天气市场的结算数据源 (Weather Underground) 使用的相同气象站
|
||||
|
||||
Args:
|
||||
city: 城市名称
|
||||
use_fahrenheit: 是否转换为华氏度
|
||||
|
||||
Returns:
|
||||
dict: METAR 数据,包含温度、露点、风速等
|
||||
"""
|
||||
icao = self.get_icao_code(city)
|
||||
if not icao:
|
||||
logger.warning(f"未找到城市 {city} 对应的 ICAO 代码")
|
||||
return None
|
||||
|
||||
try:
|
||||
# NOAA Aviation Weather API (免费,无需 Key)
|
||||
url = "https://aviationweather.gov/api/data/metar"
|
||||
params = {
|
||||
"ids": icao,
|
||||
"format": "json",
|
||||
"hours": 3, # 获取最近3小时的观测
|
||||
}
|
||||
|
||||
response = self.session.get(url, params=params, timeout=self.timeout)
|
||||
response.raise_for_status()
|
||||
|
||||
data = response.json()
|
||||
if not data:
|
||||
logger.warning(f"METAR 数据为空: {icao}")
|
||||
return None
|
||||
|
||||
# 取最新的观测记录
|
||||
latest = data[0]
|
||||
|
||||
# 提取温度 (METAR 原始单位是摄氏度)
|
||||
temp_c = latest.get("temp")
|
||||
dewp_c = latest.get("dewp")
|
||||
|
||||
# 转换为华氏度(如果需要)
|
||||
if use_fahrenheit and temp_c is not None:
|
||||
temp = temp_c * 9 / 5 + 32
|
||||
dewp = dewp_c * 9 / 5 + 32 if dewp_c is not None else None
|
||||
unit = "fahrenheit"
|
||||
else:
|
||||
temp = temp_c
|
||||
dewp = dewp_c
|
||||
unit = "celsius"
|
||||
|
||||
# 解析观测时间
|
||||
obs_time = latest.get("reportTime", "")
|
||||
|
||||
result = {
|
||||
"source": "metar",
|
||||
"icao": icao,
|
||||
"station_name": latest.get("name", icao),
|
||||
"timestamp": datetime.utcnow().isoformat(),
|
||||
"observation_time": obs_time,
|
||||
"raw_metar": latest.get("rawOb", ""),
|
||||
"current": {
|
||||
"temp": round(temp, 1) if temp is not None else None,
|
||||
"dewpoint": round(dewp, 1) if dewp is not None else None,
|
||||
"humidity": latest.get("rh"), # 相对湿度
|
||||
"wind_speed_kt": latest.get("wspd"), # 风速 (knots)
|
||||
"wind_dir": latest.get("wdir"), # 风向 (度)
|
||||
"visibility_miles": latest.get("visib"), # 能见度 (英里)
|
||||
"altimeter": latest.get("altim"), # 气压
|
||||
"flight_category": latest.get("fltcat"), # VFR/IFR 等
|
||||
"clouds": latest.get("clouds", []),
|
||||
},
|
||||
"unit": unit,
|
||||
}
|
||||
|
||||
logger.info(
|
||||
f"✈️ METAR {icao}: {temp:.1f}°{'F' if use_fahrenheit else 'C'} "
|
||||
f"(obs: {obs_time})"
|
||||
)
|
||||
|
||||
return result
|
||||
|
||||
except requests.exceptions.RequestException as e:
|
||||
logger.error(f"METAR 请求失败 ({icao}): {e}")
|
||||
return None
|
||||
except (KeyError, IndexError, TypeError) as e:
|
||||
logger.error(f"METAR 数据解析失败 ({icao}): {e}")
|
||||
return None
|
||||
|
||||
def fetch_from_open_meteo(
|
||||
self,
|
||||
lat: float,
|
||||
@@ -234,32 +360,35 @@ class WeatherDataCollector:
|
||||
def extract_date_from_title(self, title: str) -> Optional[str]:
|
||||
"""
|
||||
从标题中提取日期并标准化为 YYYY-MM-DD
|
||||
例如: "Highest temperature in Seattle on February 6?" -> "2026-02-06"
|
||||
支持: "February 6", "2月6日", "2-6" 等
|
||||
"""
|
||||
# 1. 尝试英文月份
|
||||
months = {
|
||||
"January": "01",
|
||||
"February": "02",
|
||||
"March": "03",
|
||||
"April": "04",
|
||||
"May": "05",
|
||||
"June": "06",
|
||||
"July": "07",
|
||||
"August": "08",
|
||||
"September": "09",
|
||||
"October": "10",
|
||||
"November": "11",
|
||||
"December": "12",
|
||||
"January": "01", "February": "02", "March": "03", "April": "04",
|
||||
"May": "05", "June": "06", "July": "07", "August": "08",
|
||||
"September": "09", "October": "10", "November": "11", "December": "12",
|
||||
}
|
||||
|
||||
for month_name, month_val in months.items():
|
||||
if month_name in title:
|
||||
match = re.search(f"{month_name}\\s+(\\d+)", title)
|
||||
if match:
|
||||
day = int(match.group(1))
|
||||
year = datetime.now().year
|
||||
# 简单处理跨年逻辑:如果提取到的月份小于当前月份太多,可能是指明年
|
||||
# 但对于天气预报通常只看近期几天
|
||||
return f"{year}-{month_val}-{day:02d}"
|
||||
|
||||
# 2. 尝试中文格式 "2月7日" 或 "02月07日"
|
||||
zh_match = re.search(r"(\d{1,2})月(\d{1,2})日", title)
|
||||
if zh_match:
|
||||
month = int(zh_match.group(1))
|
||||
day = int(zh_match.group(2))
|
||||
year = datetime.now().year
|
||||
return f"{year}-{month:02d}-{day:02d}"
|
||||
|
||||
# 3. 尝试 ISO 格式 YYYY-MM-DD
|
||||
iso_match = re.search(r"(\d{4})-(\d{2})-(\d{2})", title)
|
||||
if iso_match:
|
||||
return iso_match.group(0)
|
||||
|
||||
return None
|
||||
|
||||
def get_coordinates(self, city: str) -> Optional[Dict[str, float]]:
|
||||
@@ -317,40 +446,36 @@ class WeatherDataCollector:
|
||||
|
||||
def extract_city_from_question(self, question: str) -> Optional[str]:
|
||||
"""
|
||||
从 Polymarket 问题描述中提取城市名称
|
||||
支持多种描述方式:
|
||||
- "Highest temperature in Ankara on February 5?"
|
||||
- "Will the temperature in London be..."
|
||||
- "Temp in New York..."
|
||||
从 Polymarket 问题描述或 Slug 中提取城市名称
|
||||
"""
|
||||
q = question.lower()
|
||||
|
||||
# 移除常见的干扰词
|
||||
for noise in ["highest ", "the ", "will ", "lowest "]:
|
||||
if q.startswith(noise):
|
||||
q = q[len(noise) :]
|
||||
# 1. 优先尝试已知城市列表 (硬编码匹配)
|
||||
known_cities = {
|
||||
"london": "London", "伦敦": "London",
|
||||
"new york": "New York", "new york's central park": "New York", "nyc": "New York", "纽约": "New York",
|
||||
"seattle": "Seattle", "西雅图": "Seattle",
|
||||
"chicago": "Chicago", "芝加哥": "Chicago",
|
||||
"dallas": "Dallas", "达拉斯": "Dallas",
|
||||
"miami": "Miami", "迈阿密": "Miami",
|
||||
"atlanta": "Atlanta", "亚特兰大": "Atlanta",
|
||||
"seoul": "Seoul", "首尔": "Seoul",
|
||||
"toronto": "Toronto", "多伦多": "Toronto",
|
||||
"ankara": "Ankara", "安卡拉": "Ankara",
|
||||
"wellington": "Wellington", "惠灵顿": "Wellington",
|
||||
"buenos aires": "Buenos Aires", "布宜诺斯艾利斯": "Buenos Aires"
|
||||
}
|
||||
|
||||
for key, val in known_cities.items():
|
||||
if key in q:
|
||||
return val
|
||||
|
||||
# 处理 "temperature in [City]" | "temp in [City]"
|
||||
triggers = ["temperature in ", "temp in ", "weather in "]
|
||||
# 2. 从英文模板中提取
|
||||
triggers = ["temperature in ", "temp in ", "weather in ", "highest-temperature-in-", "temperature-in-"]
|
||||
for trigger in triggers:
|
||||
if trigger in q:
|
||||
part = q.split(trigger)[1]
|
||||
# 截断日期和其他后缀
|
||||
# 按照 "on", "at", "above", "below", "?", " ", "be", "is" 分割
|
||||
delimiters = [
|
||||
" on ",
|
||||
" at ",
|
||||
" above ",
|
||||
" below ",
|
||||
" be ",
|
||||
" is ",
|
||||
" will ",
|
||||
" has ",
|
||||
" reached ",
|
||||
"?",
|
||||
" (",
|
||||
", ",
|
||||
]
|
||||
delimiters = [" on ", " at ", " above ", " below ", " be ", " is ", " will ", " has ", " reached ", "?", " (", ", ", "-"]
|
||||
city = part
|
||||
for d in delimiters:
|
||||
if d in city:
|
||||
@@ -404,6 +529,11 @@ class WeatherDataCollector:
|
||||
else:
|
||||
logger.info(f"🌡️ {city} 使用摄氏度 (°C)")
|
||||
|
||||
# METAR (Airport Weather - Same source as Weather Underground settlement)
|
||||
metar_data = self.fetch_metar(city, use_fahrenheit=use_fahrenheit)
|
||||
if metar_data:
|
||||
results["metar"] = metar_data
|
||||
|
||||
# Open-Meteo (Primary Free Source - No Key)
|
||||
if lat and lon:
|
||||
open_meteo = self.fetch_from_open_meteo(
|
||||
|
||||
@@ -1,14 +1,17 @@
|
||||
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_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 = ""
|
||||
@@ -16,82 +19,81 @@ class RiskManager:
|
||||
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,
|
||||
hours_to_settle: float,
|
||||
is_high_relative_volume: bool) -> tuple[float, str]:
|
||||
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(置信度)
|
||||
× liquidity_factor(深度/仓位 >= 5x)
|
||||
× time_decay(离结算衰减)
|
||||
仓位计算方法 (简化版,移除流动性过滤):
|
||||
仓位 = base_position(置信度)
|
||||
× time_decay(离结算衰减)
|
||||
× budget_limit
|
||||
"""
|
||||
self._reset_daily_exposure()
|
||||
|
||||
|
||||
final_pos = base_confidence_usd
|
||||
reason = "Normal"
|
||||
|
||||
# 1. 流动性过滤: 深度 < $50 强制跳过; 深度 < 仓位的 5 倍则缩减
|
||||
if depth < 50:
|
||||
return 0.0, "🚫深度不足 (min $50)"
|
||||
|
||||
if depth < final_pos * 5:
|
||||
# 如果深度不足以承载期望仓位,按比例缩减至深度的 1/5
|
||||
final_pos = depth / 5.0
|
||||
reason = "⚠️深度限流"
|
||||
|
||||
# 2. 时间衰减因子
|
||||
# 1. 时间衰减因子
|
||||
# 离结算时间越近,预测越准但也存在剧烈博弈风险
|
||||
time_factor = 1.0
|
||||
if hours_to_settle <= 1.0:
|
||||
time_factor = 0.0 # 最后 1 小时停止建仓
|
||||
time_factor = 0.0 # 最后 1 小时停止建仓
|
||||
reason = "🚫临近结算"
|
||||
elif hours_to_settle <= 4.0:
|
||||
time_factor = 0.4 # 1-4小时:缩小 60%
|
||||
time_factor = 0.4 # 1-4小时:缩小 60%
|
||||
reason = "⏱️结算冲刺 (40%)"
|
||||
elif hours_to_settle <= 12.0:
|
||||
time_factor = 0.7 # 4-12小时:缩小 30%
|
||||
time_factor = 0.7 # 4-12小时:缩小 30%
|
||||
reason = "⏳接近结算 (70%)"
|
||||
|
||||
final_pos *= time_factor
|
||||
if final_pos <= 0: return 0.0, reason
|
||||
|
||||
# 3. 预算上限过滤
|
||||
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 = "🛑触及日风控上限"
|
||||
|
||||
# 4. 高相对成交量加权 (如果是高成交量市场,且逻辑支持,可保持原状或微增)
|
||||
# 3. 高相对成交量加权 (如果是高成交量市场,且逻辑支持,可保持原状或微增)
|
||||
# 这里逻辑设定为:如果不是高成交量,再次缩减 20% 防御
|
||||
if not is_high_relative_volume:
|
||||
final_pos *= 0.8
|
||||
if reason == "Normal": reason = "📉低活缩减"
|
||||
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}")
|
||||
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:
|
||||
def check_trade_risk(
|
||||
self, trade_size: float, market_data: dict, model_confidence: float
|
||||
) -> dict:
|
||||
"""保持基础接口兼容"""
|
||||
return {"passed": True, "risks": []}
|
||||
|
||||
+31
-7
@@ -134,8 +134,9 @@ class TelegramNotifier:
|
||||
total_volume: float = 0,
|
||||
brackets_count: int = 0,
|
||||
strategy_tips: list = None,
|
||||
metar_data: dict = None,
|
||||
):
|
||||
"""发送简约版合并预警"""
|
||||
"""发送简约版合并预警 (含 METAR 航空气象数据)"""
|
||||
if not alerts:
|
||||
return
|
||||
|
||||
@@ -143,16 +144,38 @@ class TelegramNotifier:
|
||||
|
||||
# UTC+8 北京时间
|
||||
now_bj = datetime.utcnow() + timedelta(hours=8)
|
||||
timestamp_bj = now_bj.strftime(
|
||||
"%H:%M"
|
||||
) # 简化为仅显示时间,日期通常与当地一致或不重要
|
||||
timestamp_bj = now_bj.strftime("%H:%M")
|
||||
|
||||
# 1. 信号详情构建
|
||||
# 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"
|
||||
|
||||
# 2. 策略建议(如果有)
|
||||
# 3. 策略建议(如果有)
|
||||
tips_text = ""
|
||||
if strategy_tips:
|
||||
tips_text = (
|
||||
@@ -161,10 +184,11 @@ class TelegramNotifier:
|
||||
+ "\n\n"
|
||||
)
|
||||
|
||||
# 3. 总体布局 (回归清爽风格)
|
||||
# 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}"
|
||||
|
||||
-111
@@ -1,111 +0,0 @@
|
||||
"""
|
||||
Polymarket 天气市场价格查询
|
||||
直接获取 YES/NO 的真实买入价格(Ask)
|
||||
"""
|
||||
import requests
|
||||
|
||||
# ============ 配置 ============
|
||||
# 替换成你要查的 token_id(从 Gamma API 获取)
|
||||
YES_TOKEN_ID = "你的YES_token_id"
|
||||
NO_TOKEN_ID = "你的NO_token_id"
|
||||
|
||||
CLOB_BASE = "https://clob.polymarket.com"
|
||||
GAMMA_BASE = "https://gamma-api.polymarket.com"
|
||||
# ============ 方法1: 快速查价格 ============
|
||||
def get_price(token_id, side="buy"):
|
||||
"""获取单个 token 的买入/卖出价格"""
|
||||
resp = requests.get(f"{CLOB_BASE}/price", params={
|
||||
"token_id": token_id,
|
||||
"side": side.upper()
|
||||
})
|
||||
data = resp.json()
|
||||
return float(data.get("price", 0))
|
||||
|
||||
# ============ 方法2: 查完整盘口 ============
|
||||
def get_orderbook(token_id):
|
||||
"""获取完整 orderbook,含深度"""
|
||||
resp = requests.get(f"{CLOB_BASE}/book", params={
|
||||
"token_id": token_id
|
||||
})
|
||||
return resp.json()
|
||||
|
||||
# ============ 方法3: 从 Gamma 发现天气市场 ============
|
||||
def discover_weather_markets(city="New York"):
|
||||
"""自动发现天气市场,获取 token_id"""
|
||||
resp = requests.get(f"{GAMMA_BASE}/markets", params={
|
||||
"tag": "weather",
|
||||
"closed": "false",
|
||||
"limit": 50
|
||||
})
|
||||
markets = resp.json()
|
||||
results = []
|
||||
for m in markets:
|
||||
if city.lower() in m.get("question", "").lower():
|
||||
tokens = m.get("tokens", [])
|
||||
if len(tokens) >= 2:
|
||||
yes_token = None
|
||||
no_token = None
|
||||
for t in tokens:
|
||||
if t.get("outcome") == "Yes":
|
||||
yes_token = t["token_id"]
|
||||
else:
|
||||
no_token = t["token_id"]
|
||||
if yes_token and no_token:
|
||||
results.append({
|
||||
"question": m["question"],
|
||||
"yes_token": yes_token,
|
||||
"no_token": no_token,
|
||||
"slug": m.get("market_slug", "")
|
||||
})
|
||||
return results
|
||||
|
||||
|
||||
# ============ 主流程 ============
|
||||
def main():
|
||||
print("📡 正在发现 NYC 天气市场...\n")
|
||||
markets = discover_weather_markets("New York")
|
||||
if not markets:
|
||||
print("❌ 未找到天气市场,检查 Gamma API")
|
||||
return
|
||||
|
||||
print(f"✅ 找到 {len(markets)} 个市场\n")
|
||||
print("=" * 55)
|
||||
|
||||
for m in markets[:10]:
|
||||
q = m["question"]
|
||||
yes_id = m["yes_token"]
|
||||
no_id = m["no_token"]
|
||||
|
||||
# 获取真实 Ask 价格
|
||||
yes_ask = get_price(yes_id, "buy")
|
||||
no_ask = get_price(no_id, "buy")
|
||||
|
||||
# 获取 Bid
|
||||
yes_bid = get_price(yes_id, "sell")
|
||||
|
||||
spread = yes_ask - yes_bid if yes_ask and yes_bid else None
|
||||
|
||||
# 获取盘口深度
|
||||
book = get_orderbook(yes_id)
|
||||
ask_depth = sum(float(o.get("size", 0)) for o in book.get("asks", [])[:3])
|
||||
bid_depth = sum(float(o.get("size", 0)) for o in book.get("bids", [])[:3])
|
||||
|
||||
# 流动性判断
|
||||
if ask_depth < 50:
|
||||
liq = "🔴枯竭"
|
||||
elif ask_depth < 500:
|
||||
liq = "🟡正常"
|
||||
else:
|
||||
liq = "🟢充裕"
|
||||
|
||||
print(f"\n📊 {q}")
|
||||
print(f" YES Ask: {yes_ask*100:.1f}¢ | NO Ask: {no_ask*100:.1f}¢")
|
||||
print(f" YES Bid: {yes_bid*100:.1f}¢ | Spread: {spread*100:.1f}¢" if spread else " YES Bid: --")
|
||||
print(f" 深度: Ask ${ask_depth:.0f} | Bid ${bid_depth:.0f} | {liq}")
|
||||
print(f" 🔗 https://polymarket.com/event/{m['slug']}")
|
||||
|
||||
print("\n" + "=" * 55)
|
||||
print("💡 价格单位: Ask = 你买入要付的价格")
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -1,65 +0,0 @@
|
||||
|
||||
import os
|
||||
import json
|
||||
from src.utils.config_loader import load_config
|
||||
from src.utils.notifier import TelegramNotifier
|
||||
|
||||
def send_test_template():
|
||||
config_data = load_config()
|
||||
notifier = TelegramNotifier(config_data["telegram"])
|
||||
|
||||
city = "Nyc"
|
||||
target_date = "2026-02-07"
|
||||
|
||||
# 模拟异动信号数据
|
||||
alerts = [
|
||||
{
|
||||
"market": target_date,
|
||||
"msg": (
|
||||
"🟢 <b>26°F+</b>\n"
|
||||
"执行动作: <b>BUY YES</b> ⬆️\n"
|
||||
"Ask: 80¢ | Bid: -- | Mid: 79.5¢\n"
|
||||
"Spread: 1.2¢ | 深度: $1,847\n"
|
||||
"流动性: ✅ 充裕 | 可交易: ✅\n"
|
||||
"📐 预测偏差: -3.0°F (预测 23.0°F)"
|
||||
),
|
||||
"bought": True,
|
||||
"amount": 7.0,
|
||||
"confidence": "⭐中置信"
|
||||
},
|
||||
{
|
||||
"market": target_date,
|
||||
"msg": (
|
||||
"🔴 <b>18-19°F</b>\n"
|
||||
"执行动作: <b>SELL YES</b> ⬇️\n"
|
||||
"Ask: -- | Bid: 5.0¢ | Mid: 4.5¢\n"
|
||||
"Spread: 0.5¢ | 深度: $312\n"
|
||||
"流动性: ✅ 正常 | 可交易: ✅\n"
|
||||
"📐 预测偏差: -4.0°F (预测 23.0°F)"
|
||||
),
|
||||
"bought": False,
|
||||
"amount": 0.0,
|
||||
"confidence": ""
|
||||
}
|
||||
]
|
||||
|
||||
strategy_tips = [
|
||||
"预测温度 23.0°F 落在 22-23°F 区间,市场与模型一致",
|
||||
"26°F+ 区间出现主力大额买入,建议跟随",
|
||||
"18-19°F 流动性正常但偏差过大,已执行调仓"
|
||||
]
|
||||
|
||||
print("🚀 正在发送测试模板到 Telegram...")
|
||||
notifier.send_combined_alert(
|
||||
city=city,
|
||||
alerts=alerts,
|
||||
local_time="10:56 EST",
|
||||
forecast_temp="23.0°F",
|
||||
total_volume=40113,
|
||||
brackets_count=7,
|
||||
strategy_tips=strategy_tips
|
||||
)
|
||||
print("✅ 发送成功!请检查手机。")
|
||||
|
||||
if __name__ == "__main__":
|
||||
send_test_template()
|
||||
Reference in New Issue
Block a user