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" - "💡 直接输入城市名称 (如: SeattleLondon) 即可查询该城市当天的最高温市场报价。" + "/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"