From df2117afe822045ae79f3982eb04ef841d97a63f Mon Sep 17 00:00:00 2001 From: "2569718930@qq.com" <2569718930@qq.com> Date: Fri, 6 Feb 2026 20:11:01 +0800 Subject: [PATCH] feat: Introduce dynamic position sizing based on market conditions and enhance trading report with grouped positions and ROI. --- bot_listener.py | 58 ++++++++++++++++++++-------- main.py | 75 ++++++++++++++++++++++++++++++++++++- src/trading/paper_trader.py | 4 +- src/utils/notifier.py | 8 +++- 4 files changed, 125 insertions(+), 20 deletions(-) diff --git a/bot_listener.py b/bot_listener.py index 35f27530..c649b3db 100644 --- a/bot_listener.py +++ b/bot_listener.py @@ -110,20 +110,43 @@ def start_bot(): ) return - msg_lines = ["📊 模拟交易报告 (北京时间)\n" + "═" * 15] + msg_lines = ["📊 模拟交易报告\n" + "═" * 20] - # 1. 活跃持仓 + # 1. 活跃持仓 - 按目标日期分组 if positions: - msg_lines.append("📌 当前持仓:") - total_pnl = 0 + # 按目标日期分组 + positions_by_date = {} 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") + target_date = pos.get("target_date") or "未知日期" + if target_date not in positions_by_date: + positions_by_date[target_date] = [] + positions_by_date[target_date].append(pos) + + # 按日期排序显示 + 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_icon = "📈" if date_pnl >= 0 else "📉" + + msg_lines.append(f"\n{date_icon} 【{target_date}】 小计: {date_pnl:+.2f}$") + msg_lines.append("─" * 18) + + for pos in date_positions: + pnl_usd = pos.get("pnl_usd", 0) + icon = "🟢" if pnl_usd >= 0 else "🔴" + entry_price = pos.get("entry_price", 0) + current_price = pos.get("current_price", entry_price) + predicted_temp = pos.get("predicted_temp") + + # 格式:城市 选项 | 方向 入场→当前 | 预测温度 | 盈亏 + pred_text = f"预测:{predicted_temp}" if predicted_temp else "" + msg_lines.append( + f"{icon} {pos['city']} {pos['option']}\n" + f" {pos['side']} {entry_price}¢→{current_price}¢ {pred_text} | {pnl_usd:+.2f}$" + ) + + total_pnl = sum(p.get("pnl_usd", 0) for p in positions.values()) + msg_lines.append(f"\n💰 持仓总计: {total_pnl:+.2f}$") # 2. 最近交易记录 (最新 5 笔) trades = data.get("trades", []) @@ -132,8 +155,8 @@ def start_bot(): # 取末尾 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] # 仅显示时间 + t_type = "🛒" if t["type"] == "BUY" else "💰" + t_time = t.get("time", "").split(" ")[1] if " " in t.get("time", "") else t.get("time", "") msg_lines.append( f"• {t_time} {t_type} {t['city']} {t['option']} ({t['price']}¢)" ) @@ -142,12 +165,15 @@ def start_bot(): if history: total_trades = len(history) wins = sum(1 for p in history if p.get("pnl_usd", 0) > 0) + 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 + roi = (total_profit / total_cost * 100) if total_cost > 0 else 0 msg_lines.append("\n📈 历史战绩:") - msg_lines.append(f"累计成交: {total_trades} 笔") - msg_lines.append(f"综合胜率: {win_rate:.1f}%") + msg_lines.append(f"累计成交: {total_trades}笔 | 胜率: {win_rate:.1f}%") + msg_lines.append(f"已投入: ${total_cost:.2f} | 盈亏: {total_profit:+.2f}$ ({roi:+.1f}%)") - footer = "\n" + "═" * 15 + "\n" + f"💳 虚拟账户余额: ${balance:.2f}" + footer = "\n" + "═" * 20 + "\n" + f"💳 账户余额: ${balance:.2f}" msg_lines.append(footer) bot.reply_to(message, "\n".join(msg_lines), parse_mode="HTML") diff --git a/main.py b/main.py index f4f297ad..e0197fde 100644 --- a/main.py +++ b/main.py @@ -2,6 +2,7 @@ import sys import time import os import json +import re from datetime import datetime, timedelta from loguru import logger @@ -311,6 +312,67 @@ def main(): else int(buy_no_price * 100) ) + # --- 智能动态仓位计算 --- + # 1. 获取 Open-Meteo 对目标日期的最高温预测 + predicted_high = None + weather_supports = False + daily_data = weather_data.get("open-meteo", {}).get("daily", {}) + if daily_data and target_date: + dates = daily_data.get("time", []) + max_temps = daily_data.get("temperature_2m_max", []) + for idx, d_str in enumerate(dates): + if target_date == d_str and idx < len(max_temps): + predicted_high = max_temps[idx] + break + + # 2. 判断天气预测是否支持当前方向 + if predicted_high is not None: + # 解析选项的温度范围 (例如 "40-41°F" 或 "32°F or below") + temp_match = re.search(r'(\d+)(?:-(\d+))?°[FC]', question) + if temp_match: + low_bound = int(temp_match.group(1)) + high_bound = int(temp_match.group(2)) if temp_match.group(2) else low_bound + + # 如果买 NO,天气预测应该在这个区间之外 + if trigger_side == "Buy No": + weather_supports = (predicted_high < low_bound - 2) or (predicted_high > high_bound + 2) + else: # 买 YES + weather_supports = (low_bound - 2 <= predicted_high <= high_bound + 2) + + # 3. 获取成交量信息 + market_volume = market.get("volume", 0) + if isinstance(market_volume, str): + try: + market_volume = float(market_volume.replace("$", "").replace(",", "")) + except: + market_volume = 0 + high_volume = market_volume >= 5000 # $5000+ 算高成交量 + + # 4. 动态仓位决策 + # 条件: 价格锁定程度 + 天气支持 + 成交量 + if trigger_price >= 90 and weather_supports and high_volume: + # 三重确认:重注 + amount_usd = 10.0 + confidence_tag = "🔥高置信" + elif trigger_price >= 90 and weather_supports: + # 双重确认:中等仓位 + amount_usd = 7.0 + confidence_tag = "⭐中置信" + elif trigger_price >= 92: + # 价格接近锁定,即使其他条件不满足也小额参与 + amount_usd = 5.0 + confidence_tag = "📌价格锁定" + else: + # 普通信号:最小仓位 + amount_usd = 3.0 + confidence_tag = "💡试探" + + logger.info( + f"【仓位决策】{city} {question} | " + f"价格:{trigger_price}¢ | 预测:{predicted_high} | 天气支持:{weather_supports} | " + f"高量:{high_volume} | 仓位:${amount_usd} ({confidence_tag})" + ) + # --- 模拟交易触发逻辑 --- side = "YES" if trigger_side == "Buy Yes" else "NO" success = paper_trader.open_position( @@ -319,15 +381,24 @@ def main(): option=question, price=trigger_price, side=side, - amount_usd=5.0, + amount_usd=amount_usd, + target_date=target_date, + predicted_temp=predicted_high, ) + # 构建预测温度显示文本 + temp_unit = weather_data.get("open-meteo", {}).get("unit", "celsius") + temp_symbol = "°F" if temp_unit == "fahrenheit" else "°C" + forecast_text = f"预测:{predicted_high}{temp_symbol}" if predicted_high else "预测:N/A" + city_alerts.append( { "type": "price", "market": f"{question} ({target_date or '今日'})", - "msg": f"{trigger_side}进入锁定区间 {trigger_price}¢", + "msg": f"{trigger_side} {trigger_price}¢ | {forecast_text}", "bought": success, + "amount": amount_usd, + "confidence": confidence_tag, } ) pushed_signals[alert_key] = time.time() diff --git a/src/trading/paper_trader.py b/src/trading/paper_trader.py index 53989317..386bf6e4 100644 --- a/src/trading/paper_trader.py +++ b/src/trading/paper_trader.py @@ -43,7 +43,7 @@ class PaperTrader: indent=2, ) - def open_position(self, market_id: str, city: str, option: str, price: int, side: str, amount_usd: float = 5.0): + def open_position(self, market_id: str, city: str, option: str, price: int, side: str, amount_usd: float = 5.0, target_date: str = None, predicted_temp: float = None): """ 开仓进入模拟仓位 """ @@ -76,6 +76,8 @@ class PaperTrader: "pnl_usd": 0.0, "pnl_pct": 0.0, "status": "OPEN", + "target_date": target_date, + "predicted_temp": predicted_temp, "opened_at": (datetime.utcnow() + timedelta(hours=8)).strftime("%Y-%m-%d %H:%M:%S") } diff --git a/src/utils/notifier.py b/src/utils/notifier.py index a855596f..f215090a 100644 --- a/src/utils/notifier.py +++ b/src/utils/notifier.py @@ -132,7 +132,13 @@ class TelegramNotifier: items_text = "" for a in alerts: type_icon = "⚡" if a["type"] == "price" else "🐋" - buy_tag = " [🛒 模拟仓已买入]" if a.get("bought") else "" + # 买入标签:显示金额 + if a.get("bought"): + amount = a.get("amount", 5.0) + confidence = a.get("confidence", "") + buy_tag = f" [🛒 ${amount} {confidence}]" + else: + buy_tag = "" items_text += f"{type_icon} {a['market']}: {a['msg']}{buy_tag}\n" text = (