feat: Revamp signal retrieval to prioritize earliest settling markets by opportunity score and enhance portfolio reporting with HTML generation for large portfolios.
This commit is contained in:
+208
-62
@@ -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"🎯 <b>即将结算市场 ({earliest_date})</b>\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} <b>{i}. {city} {option}</b>\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 = ["📊 <b>模拟交易报告</b>\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 = ["📊 <b>模拟交易报告</b>"]
|
||||
|
||||
# 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📌 <b>持仓概览</b> (共{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} <b>【{target_date}】</b> 小计: {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💰 <b>持仓总计: {total_pnl:+.2f}$</b>")
|
||||
total_cost = sum(p.get("cost_usd", 0) for p in positions.values())
|
||||
msg_lines.append(f"<b>💰 合计: ${total_cost:.0f}投入 {total_pnl:+.2f}$</b>")
|
||||
|
||||
msg_lines.append("\n📋 <b>最新持仓:</b>")
|
||||
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📝 <b>最近操作:</b>")
|
||||
# 取末尾 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📈 <b>历史战绩:</b>")
|
||||
msg_lines.append(f"累计成交: {total_trades}笔 | 胜率: <b>{win_rate:.1f}%</b>")
|
||||
msg_lines.append(f"已投入: ${total_cost:.2f} | 盈亏: <b>{total_profit:+.2f}$</b> ({roi:+.1f}%)")
|
||||
msg_lines.append(f"\n📈 <b>历史:</b> {total_trades}笔 胜率{win_rate:.0f}% 盈亏{total_profit:+.2f}$")
|
||||
|
||||
footer = "\n" + "═" * 20 + "\n" + f"💳 账户余额: <b>${balance:.2f}</b>"
|
||||
msg_lines.append(footer)
|
||||
msg_lines.append(f"\n💳 余额: <b>${balance:.2f}</b>")
|
||||
|
||||
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"""<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>PolyWeather 持仓报告</title>
|
||||
<style>
|
||||
body {{ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; background: #1a1a2e; color: #eee; padding: 20px; }}
|
||||
h1 {{ color: #00d4ff; text-align: center; }}
|
||||
.summary {{ background: #16213e; padding: 15px; border-radius: 10px; margin-bottom: 20px; }}
|
||||
.summary-item {{ display: inline-block; margin-right: 30px; }}
|
||||
.positive {{ color: #00ff88; }}
|
||||
.negative {{ color: #ff4757; }}
|
||||
table {{ width: 100%; border-collapse: collapse; margin-top: 10px; }}
|
||||
th {{ background: #0f3460; padding: 10px; text-align: left; }}
|
||||
td {{ padding: 8px; border-bottom: 1px solid #333; }}
|
||||
.date-header {{ background: #0f3460; padding: 10px; margin-top: 20px; border-radius: 5px; }}
|
||||
.footer {{ text-align: center; margin-top: 30px; color: #666; }}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1>📊 PolyWeather 持仓报告</h1>
|
||||
<div class="summary">
|
||||
<div class="summary-item">💳 余额: <b>${balance:.2f}</b></div>
|
||||
<div class="summary-item">📦 持仓: <b>{len(positions)}</b> 个</div>
|
||||
<div class="summary-item">💰 投入: <b>${total_cost:.2f}</b></div>
|
||||
<div class="summary-item">📈 浮盈: <b class="{'positive' if total_pnl >= 0 else 'negative'}">{total_pnl:+.2f}$</b></div>
|
||||
</div>
|
||||
"""
|
||||
|
||||
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"""
|
||||
<div class="date-header">
|
||||
📅 <b>{target_date}</b> | {len(date_positions)}笔 | 投入${date_cost:.0f} |
|
||||
<span class="{'positive' if date_pnl >= 0 else 'negative'}">{date_pnl:+.2f}$</span>
|
||||
</div>
|
||||
<table>
|
||||
<tr><th>城市</th><th>选项</th><th>方向</th><th>入场</th><th>当前</th><th>预测</th><th>盈亏</th></tr>
|
||||
"""
|
||||
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""" <tr>
|
||||
<td>{pos.get('city', '-')}</td>
|
||||
<td>{pos.get('option', '-')}</td>
|
||||
<td>{pos.get('side', '-')}</td>
|
||||
<td>{pos.get('entry_price', 0)}¢</td>
|
||||
<td>{pos.get('current_price', 0)}¢</td>
|
||||
<td>{pred}</td>
|
||||
<td class="{pnl_class}">{pnl:+.2f}$</td>
|
||||
</tr>
|
||||
"""
|
||||
html += " </table>\n"
|
||||
|
||||
html += f"""
|
||||
<div class="footer">
|
||||
生成时间: {now_bj} (北京时间) | PolyWeather Monitor
|
||||
</div>
|
||||
</body>
|
||||
</html>"""
|
||||
|
||||
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(
|
||||
|
||||
Reference in New Issue
Block a user