diff --git a/bot_listener.py b/bot_listener.py index c649b3db..eab877d3 100644 --- a/bot_listener.py +++ b/bot_listener.py @@ -45,7 +45,7 @@ def start_bot(): @bot.message_handler(commands=["signal"]) def get_signals(message): - bot.send_message(message.chat.id, "šŸ” ę­£åœØę£€ē“¢å½“å‰ęœ€å€¼å¾—å…³ę³Øēš„å¤©ę°”äæ”å·...") + bot.send_message(message.chat.id, "šŸ” ę­£åœØę£€ē“¢ęœ€ę—©ē»“ē®—ēš„åø‚åœŗäæ”å·...") try: if not os.path.exists("data/active_signals.json"): @@ -63,26 +63,83 @@ def start_bot(): ) return - # ęŒ‰åˆ†ę•°ęŽ’åŗå¹¶å–å‰ 3 äøŖ - sorted_signals = sorted( - signals.values(), key=lambda x: x.get("score", 0), reverse=True - )[:3] + # čæ‡ę»¤ęŽ‰å·²ē»“ęŸēš„åø‚åœŗļ¼ˆä»·ę ¼ęŽ„čæ‘0ꈖ100ļ¼‰å’Œę— ę—„ęœŸēš„ + active_signals = [] + for s in signals.values(): + 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 - for s in sorted_signals: - notifier.send_signal( - market_name=s["city"], - full_title=s["full_title"], - option=s["option"], - score=round(s.get("score", 0) * 5, 1), - prediction=s["prediction"], - 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"], - target_date=s["target_date"], + # ęŒ‰ę—„ęœŸęŽ’åŗļ¼Œä¼˜å…ˆęœ€ę—©ē»“ē®—ēš„ + 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] + + # ꌉ"ęœŗä¼šä»·å€¼"ęŽ’åŗļ¼šęŽ„čæ‘é”å®šåŒŗé—“ļ¼ˆ85-95Ā¢ļ¼‰ēš„ä¼˜å…ˆ + def opportunity_score(s): + price = s.get("price", 50) + buy_yes = s.get("buy_yes", price) + buy_no = s.get("buy_no", 100 - price) + # č®”ē®—č·ē¦»é”å®šåŒŗé—“ēš„č·ē¦» + max_price = max(buy_yes, buy_no) + if 85 <= max_price <= 95: + return 100 + max_price # å·²åœØé”å®šåŒŗé—“ļ¼Œęœ€é«˜ä¼˜å…ˆ + elif max_price > 70: + return max_price # ęŽ„čæ‘é”å®š + else: + return max_price / 2 # čæœē¦»é”å®š + + earliest_markets.sort(key=opportunity_score, reverse=True) + top_markets = earliest_markets[:3] + + # ęž„å»ŗę¶ˆęÆ + msg_lines = [ + f"šŸŽÆ å³å°†ē»“ē®—åø‚åœŗ ({earliest_date})\n", + f"å…±å‘ēŽ° {len(earliest_markets)} äøŖę“»č·ƒé€‰é”¹ļ¼Œä»„äø‹äøŗęœ€å€¼å¾—å…³ę³Øēš„ļ¼š\n" + ] + + for i, s in enumerate(top_markets, 1): + city = s.get("city", "Unknown") + option = s.get("option", "Unknown") + prediction = s.get("prediction", "N/A") + buy_yes = s.get("buy_yes", s.get("price", 50)) + buy_no = s.get("buy_no", 100 - s.get("price", 50)) + + # åˆ¤ę–­ęœ€ä½³ę–¹å‘ + if buy_no >= 85: + direction = f"šŸ“ˆ Buy No {buy_no}Ā¢ (ęŽ„čæ‘é”å®š)" + confidence = "šŸ”„" if buy_no >= 90 else "⭐" + elif buy_yes >= 85: + direction = f"šŸ“ˆ Buy Yes {buy_yes}Ā¢ (ęŽ„čæ‘é”å®š)" + confidence = "šŸ”„" if buy_yes >= 90 else "⭐" + elif buy_no >= 70: + direction = f"šŸ‘€ Buy No {buy_no}Ā¢ (观ꜛ)" + confidence = "šŸ’”" + elif buy_yes >= 70: + direction = f"šŸ‘€ Buy Yes {buy_yes}Ā¢ (观ꜛ)" + confidence = "šŸ’”" + else: + direction = f"āš–ļø å‡č””ē›˜ Yes:{buy_yes}Ā¢ No:{buy_no}Ā¢" + confidence = "šŸ“Š" + + msg_lines.append( + f"{confidence} {i}. {city} {option}\n" + f" 预测: {prediction}\n" + f" {direction}\n" ) - time.sleep(0.5) + + local_time = top_markets[0].get("local_time", "N/A") if top_markets else "N/A" + msg_lines.append(f"\nšŸ•’ å½“åœ°ę—¶é—“: {local_time}") + + bot.send_message(message.chat.id, "\n".join(msg_lines), parse_mode="HTML") except Exception as e: bot.send_message(message.chat.id, f"āŒ čŽ·å–äæ”å·ę—¶å‡ŗé”™: {e}") @@ -110,77 +167,166 @@ def start_bot(): ) return - msg_lines = ["šŸ“Š ęØ”ę‹Ÿäŗ¤ę˜“ęŠ„å‘Š\n" + "═" * 20] + # å¦‚ęžœęŒä»“č¶…čæ‡20äøŖļ¼Œē”Ÿęˆ HTML ꖇ件 + if len(positions) > 20: + 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}" + ) + return + + # ē²¾ē®€ē‰ˆę¶ˆęÆ + msg_lines = ["šŸ“Š ęØ”ę‹Ÿäŗ¤ę˜“ęŠ„å‘Š"] - # 1. ę“»č·ƒęŒä»“ - ęŒ‰ē›®ę ‡ę—„ęœŸåˆ†ē»„ if positions: - # ęŒ‰ē›®ę ‡ę—„ęœŸåˆ†ē»„ positions_by_date = {} for pid, pos in positions.items(): - target_date = pos.get("target_date") or "ęœŖēŸ„ę—„ęœŸ" + 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) + 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šŸ“Œ ęŒä»“ę¦‚č§ˆ (共{len(positions)}äøŖ)") 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}$" - ) + 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}$") total_pnl = sum(p.get("pnl_usd", 0) for p in positions.values()) - msg_lines.append(f"\nšŸ’° ęŒä»“ę€»č®”: {total_pnl:+.2f}$") + total_cost = sum(p.get("cost_usd", 0) for p in positions.values()) + msg_lines.append(f"šŸ’° 合讔: ${total_cost:.0f}ęŠ•å…„ {total_pnl:+.2f}$") + + msg_lines.append("\nšŸ“‹ ęœ€ę–°ęŒä»“:") + recent_positions = list(positions.values())[-5:] + for pos in reversed(recent_positions): + pnl = pos.get("pnl_usd", 0) + 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}$") - # 2. ęœ€čæ‘äŗ¤ę˜“č®°å½• (꜀ꖰ 5 笔) trades = data.get("trades", []) if trades: msg_lines.append("\nšŸ“ ęœ€čæ‘ę“ä½œ:") - # å–ęœ«å°¾ 5 ē¬”äŗ¤ę˜“å¹¶å±•ē¤ŗ - recent_trades = trades[-5:] - for t in reversed(recent_trades): + 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.get("time", "") - msg_lines.append( - f"• {t_time} {t_type} {t['city']} {t['option']} ({t['price']}Ā¢)" - ) + 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']}") - # 3. åŽ†å²ę±‡ę€»ē»Ÿč®” 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}笔 | čƒœēŽ‡: {win_rate:.1f}%") - msg_lines.append(f"å·²ęŠ•å…„: ${total_cost:.2f} | ē›ˆäŗ: {total_profit:+.2f}$ ({roi:+.1f}%)") + msg_lines.append(f"\nšŸ“ˆ 历史: {total_trades}笔 čƒœēŽ‡{win_rate:.0f}% ē›ˆäŗ{total_profit:+.2f}$") - footer = "\n" + "═" * 20 + "\n" + f"šŸ’³ č“¦ęˆ·ä½™é¢: ${balance:.2f}" - msg_lines.append(footer) + msg_lines.append(f"\nšŸ’³ 余额: ${balance:.2f}") bot.reply_to(message, "\n".join(msg_lines), parse_mode="HTML") 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(): + 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) + + 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""" + + + + PolyWeather ęŒä»“ęŠ„å‘Š + + + +

šŸ“Š PolyWeather ęŒä»“ęŠ„å‘Š

+
+
šŸ’³ 余额: ${balance:.2f}
+
šŸ“¦ ęŒä»“: {len(positions)} äøŖ
+
šŸ’° ęŠ•å…„: ${total_cost:.2f}
+
šŸ“ˆ ęµ®ē›ˆ: {total_pnl:+.2f}$
+
+""" + + 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""" +
+ šŸ“… {target_date} | {len(date_positions)}笔 | ęŠ•å…„${date_cost:.0f} | + {date_pnl:+.2f}$ +
+ + +""" + for pos in date_positions: + pnl = pos.get("pnl_usd", 0) + pnl_class = "positive" if pnl >= 0 else "negative" + pred = pos.get("predicted_temp", "-") + html += f""" + + + + + + + + +""" + html += "
åŸŽåø‚é€‰é”¹ę–¹å‘å…„åœŗå½“å‰é¢„ęµ‹ē›ˆäŗ
{pos.get('city', '-')}{pos.get('option', '-')}{pos.get('side', '-')}{pos.get('entry_price', 0)}Ā¢{pos.get('current_price', 0)}Ā¢{pred}{pnl:+.2f}$
\n" + + html += f""" + + +""" + + html_path = "data/portfolio_report.html" + with open(html_path, "w", encoding="utf-8") as f: + f.write(html) + + return html_path + @bot.message_handler(commands=["status"]) def get_status(message): bot.reply_to(