diff --git a/PAPER_TRADING_GUIDE.md b/PAPER_TRADING_GUIDE.md
new file mode 100644
index 00000000..1034f395
--- /dev/null
+++ b/PAPER_TRADING_GUIDE.md
@@ -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) 实验室**
diff --git a/README.md b/README.md
index baa49636..f75d8d3b 100644
--- a/README.md
+++ b/README.md
@@ -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。
---
diff --git a/README_EN.md b/README_EN.md
index 29d2316c..06465207 100644
--- a/README_EN.md
+++ b/README_EN.md
@@ -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)
diff --git a/bot_listener.py b/bot_listener.py
index 5f85ac71..35f27530 100644
--- a/bot_listener.py
+++ b/bot_listener.py
@@ -28,9 +28,9 @@ def start_bot():
"🌡️ PolyWeather 监控机器人\n\n"
"可用指令:\n"
"/signal - 获取当前高置信度交易信号\n"
+ "/portfolio - 查看当前模拟交易报告\n"
"/status - 检查监控系统状态\n"
- "/id - 获取当前聊天的 Chat ID\n\n"
- "💡 直接输入城市名称 (如: Seattle 或 London) 即可查询该城市当天的最高温市场报价。"
+ "/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可用余额: ${balance:.2f}",
+ parse_mode="HTML",
+ )
+ return
+
+ msg_lines = ["📊 模拟交易报告 (北京时间)\n" + "═" * 15]
+
+ # 1. 活跃持仓
+ if positions:
+ msg_lines.append("📌 当前持仓:")
+ 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"持仓小计: {total_pnl:+.2f}$\n")
+
+ # 2. 最近交易记录 (最新 5 笔)
+ trades = data.get("trades", [])
+ if trades:
+ msg_lines.append("\n📝 最近操作:")
+ # 取末尾 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📈 历史战绩:")
+ msg_lines.append(f"累计成交: {total_trades} 笔")
+ msg_lines.append(f"综合胜率: {win_rate:.1f}%")
+
+ footer = "\n" + "═" * 15 + "\n" + f"💳 虚拟账户余额: ${balance:.2f}"
+ 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(
diff --git a/main.py b/main.py
index f918d474..f4f297ad 100644
--- a/main.py
+++ b/main.py
@@ -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"📊 每日模拟仓结算总结 ({now_bj.strftime('%Y-%m-%d')})\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"💳 可用余额: ${data.get('balance', 0):.2f}"
+ )
+ report.append(
+ f"💰 今日累计投入: ${total_cost:.2f}"
+ )
+ report.append(
+ f"📈 累计浮动盈亏: {total_pnl:+.2f}$"
+ )
+ 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)
diff --git a/src/data_collection/polymarket_api.py b/src/data_collection/polymarket_api.py
index 99c4e6f2..827b54f5 100644
--- a/src/data_collection/polymarket_api.py
+++ b/src/data_collection/polymarket_api.py
@@ -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
diff --git a/src/trading/paper_trader.py b/src/trading/paper_trader.py
new file mode 100644
index 00000000..53989317
--- /dev/null
+++ b/src/trading/paper_trader.py
@@ -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
diff --git a/src/utils/logger.py b/src/utils/logger.py
index a1122e0c..ee315620 100644
--- a/src/utils/logger.py
+++ b/src/utils/logger.py
@@ -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="{time:YYYY-MM-DD HH:mm:ss} | {level: <8} | {message}",
- 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"
)
diff --git a/src/utils/notifier.py b/src/utils/notifier.py
index 5090bd74..a855596f 100644
--- a/src/utils/notifier.py
+++ b/src/utils/notifier.py
@@ -132,7 +132,8 @@ class TelegramNotifier:
items_text = ""
for a in alerts:
type_icon = "⚡" if a["type"] == "price" else "🐋"
- items_text += f"{type_icon} {a['market']}: {a['msg']}\n"
+ buy_tag = " [🛒 模拟仓已买入]" if a.get("bought") else ""
+ items_text += f"{type_icon} {a['market']}: {a['msg']}{buy_tag}\n"
text = (
f"🔔 城市监控报告 #{self._escape_html(city)}\n\n"