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

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