社媒KOL信号跟踪_币安TradFi股票合约
社媒 KOL 信号跟踪策略(币安 TradFi 股票合约)
策略简介
本策略是一套基于 X/Twitter KOL 推文的事件驱动交易系统。策略通过 RSSHub 实时获取指定博主的新推文,再调用大语言模型解析推文中是否包含明确的股票交易信号,最后将信号映射到币安 TradFi 股票永续合约并执行交易。
策略最初围绕 X 上的 AI/半导体供应链分析博主 Serenity 设计。该博主常通过“瓶颈公司”“供应链错配”“AI 上游核心环节”等逻辑分析美股标的,因此策略重点不是捕捉普通喊单,而是尝试把这类社媒研究观点转化为结构化交易信号。
核心逻辑
-
实时获取 KOL 推文
策略通过 RSSHub 订阅指定 X 用户的推文源,定时拉取最新内容。
每条推文会被解析为正文、发布时间、链接和唯一 ID,并通过本地记录避免重复处理。 -
使用 LLM 解析自然语言信号
新推文会被发送给大语言模型,由模型判断其中是否包含明确交易含义。
输出结果包括:
字段 说明 tickers推文明确涉及的股票代码 direction方向,支持 long、short、neutralconfidence信号置信度,范围 0-100 reason解析理由 策略会特别识别 Serenity 这类博主的表达习惯,例如供应链瓶颈、低估、结构性机会、thesis validated 等偏多信号;同时过滤提问、卖关子、互动引流和未点名标的的宏观描述。
-
动态匹配币安 TradFi 股票合约
策略不会写死可交易标的,而是定时扫描交易所市场信息,筛选:
- USDT 永续合约;
underlyingSubType包含TradFi;underlyingType为EQUITY。
这样当币安上线新的股票类合约时,策略可以自动纳入可交易池。
-
置信度过滤
只有当推文方向明确、标的存在于可交易合约池中,并且 LLM 给出的置信度不低于设定阈值时,策略才会生成交易信号。
默认过滤逻辑:
推文类型 处理方式 明确看多/看空且有逻辑支撑 允许交易 只是提到某个股票 跳过 提问式、卖关子、互动类内容 视为中性 宏观叙事但没有具体标的 跳过 置信度不足 跳过 -
通知模式与实盘模式
策略提供两种运行模式:
模式 说明 notify仅记录和推送信号,不实际下单 trade信号满足条件后自动下单 默认建议先使用通知模式运行一段时间,人工检查 LLM 对推文的理解是否稳定,再切换到实盘模式。
-
自动交易与仓位控制
当满足交易条件后,策略会按账户权益的一定比例开仓。
默认单笔仓位较小,并限制最大同时持仓数量,避免单个 KOL 或单条推文对账户造成过大影响。 -
止损与回撤止盈
策略内置两类风控:
风控方式 说明 硬止损 入场后亏损达到设定比例立即平仓 回撤止盈 浮盈达到启动阈值后,按峰值利润回撤幅度触发离场 这种设计适合 Serenity 一类中期逻辑型信号:亏损快速截断,盈利则尽量让趋势继续发展。
交易流程
text
RSSHub 获取 KOL 最新推文
↓
过滤重复推文
↓
LLM 解析 ticker / direction / confidence / reason
↓
匹配币安 TradFi 股票永续合约池
↓
过滤中性、低置信度、不可交易标的
↓
通知模式:只输出信号
实盘模式:按资金比例开仓
↓
硬止损 + 回撤止盈管理持仓
主要参数
| 参数 | 说明 |
|---|---|
RSSHUB_URL | RSSHub 推文订阅地址 |
LLM_API_KEY | 大语言模型 API Key |
LLM_MODEL | 用于解析推文的模型 |
LLM_BASE_URL | LLM 接口地址 |
CHECK_INTERVAL_S | 推文检查间隔 |
MARKET_REFRESH_S | TradFi 合约池刷新间隔 |
SINGLE_POS_PCT | 单笔持仓占账户权益比例 |
MAX_POSITIONS | 最大同时持仓数量 |
LEVERAGE | 杠杆倍数 |
STOP_LOSS_PCT | 硬止损比例 |
TRAIL_ACTIVATE_PCT | 回撤止盈启动浮盈阈值 |
TRAIL_GIVEBACK_BASE | 基础回撤止盈阈值 |
MIN_CONFIDENCE | 最低交易置信度 |
MAX_TWEET_HISTORY | 本地保留的推文历史数量 |
策略特点
- 通过 RSSHub 免费获取指定 X 用户推文。
- 使用 LLM 将自然语言推文转化为结构化交易信号。
- 专门适配 AI、半导体、科技股类 KOL 的表达方式。
- 自动识别币安 TradFi 股票类永续合约,不需要手动维护合约表。
- 支持仅通知模式,便于先验证信号质量。
- 支持实盘自动下单、最大持仓数限制和单笔仓位控制。
- 使用硬止损和回撤止盈组合管理风险。
- 仪表板展示系统状态、推文统计、持仓明细和推文历史。
适用场景
本策略适合希望研究“社媒信息流驱动交易”的用户,尤其适合跟踪具备明确专业领域、长期输出稳定观点、并且对市场有一定影响力的 KOL。
以 Serenity 为例,其内容主要围绕 AI、半导体供应链、上游瓶颈公司和被忽视的小市值标的展开。策略尝试捕捉这类信息源中可能提前反映产业变化的交易线索,并借助币安 TradFi 股票永续合约进行表达。
本策略更适合作为信息触发器或半自动研究工具,而不是完全独立的成熟盈利系统。
风险提示
- KOL 推文不等于真实交易信号,观点可能错误、滞后或有主观偏见。
- LLM 可能误读语气、反讽、上下文或行业黑话。
- RSSHub 可能出现延迟、断流或抓取失败。
- 热门推文发布后,价格可能已经快速反应,实际成交存在追高风险。
- 币安 TradFi 股票合约是衍生品,并不等同于直接持有美股,没有分红或股东权益。
- 股票类永续合约可能存在资金费率、流动性、溢价折价和交易规则变化风险。
- 不建议高杠杆运行,单条社媒信号应控制仓位。
- 实盘前建议长期运行通知模式,重点检查 LLM 误判案例、信号命中率和滑点影响。
使用建议
- 优先选择长期观点稳定、专业领域清晰、市场影响力较强的 KOL。
- 不建议把多个风格差异很大的账号混在同一套 prompt 中。
MIN_CONFIDENCE可适当调高,以减少噪声交易。SINGLE_POS_PCT建议保持较低,避免单条推文造成过大风险暴露。- 先使用通知模式观察一段时间,再根据实际信号质量决定是否开启实盘。
- 对引流式、提问式、未点名标的的推文,应保持严格过滤。
相关文章
-
策略源码:社媒 KOL 信号跟踪_币安 TradFi 股票合约
包含完整 Python 策略代码,可查看 RSSHub 推文抓取、LLM 信号解析、TradFi 合约池匹配、通知/实盘模式、自动开仓、硬止损和回撤止盈逻辑。 -
策略文章:从一个爆火的 X 博主出发,我做了一个实时追踪社媒信号的自动交易系统
文章以 Serenity 这个 X 博主为例,介绍了如何用 RSSHub 获取推文、如何让 LLM 理解特定 KOL 的表达习惯,以及如何把社媒信号接入币安 TradFi 股票合约交易系统。
# ════════════════════════════════════════════════════════════════════════════
# 推文驱动交易系统(Python)
# 流程:RSSHub → LLM 解析推文 → 动态匹配合约 → 下单 + 回撤止盈
# ════════════════════════════════════════════════════════════════════════════
import time, json, re
import urllib.request
# ╔══════════════════════════════════════════════════════════════════════════╗
# ║ ★ 参数输入区(只需改这两行) ║
# ╚══════════════════════════════════════════════════════════════════════════╝
RSSHUB_URL = "http://localhost:1200/twitter/user/aleabitoreddit"
LLM_API_KEY = ""
# ══════════════════════════════════════════════════════════════════════════
LLM_MODEL = "deepseek-chat"
LLM_BASE_URL = "https://api.deepseek.com/chat/completions"
CHECK_INTERVAL_S = 5 * 60
MARKET_REFRESH_S = 30 * 60
SINGLE_POS_PCT = 0.05
MAX_POSITIONS = 5
LEVERAGE = 1
STOP_LOSS_PCT = 5
TRAIL_ACTIVATE_PCT = 8
TRAIL_GIVEBACK_BASE = 30
MIN_CONFIDENCE = 70
MAX_TWEET_HISTORY = 50
# ════════════════════════════════════════════════════════════════════════════
# 主入口
# ════════════════════════════════════════════════════════════════════════════
def main():
Log("════ 推文驱动交易系统启动 ════")
Log("RSS: " + RSSHUB_URL)
equity_contracts = {}
market_info_cache = {}
last_market_refresh = 0
last_seen_guid = _G("last_guid") or ""
trade_mode = _G("tradeMode") or "notify"
stats = _G("run_stats") or {
"total_analyzed": 0,
"total_signals": 0,
"total_skipped": 0,
"total_trades": 0,
"start_time": int(time.time()),
}
while True:
try:
trade_mode, equity_contracts, market_info_cache, last_market_refresh = \
handle_command(trade_mode, equity_contracts, market_info_cache, last_market_refresh)
if time.time() - last_market_refresh > MARKET_REFRESH_S:
equity_contracts, market_info_cache = refresh_equity_contracts()
last_market_refresh = time.time()
tweets = fetch_latest_tweets()
new_tweets = []
for tw in tweets:
if tw["guid"] == last_seen_guid:
break
new_tweets.append(tw)
if not new_tweets:
Log("💤 暂无新推文,等待下次检查({}分钟后)".format(CHECK_INTERVAL_S // 60))
else:
Log("📨 发现 {} 条新推文,开始分析...".format(len(new_tweets)))
for tw in reversed(new_tweets):
result = process_tweet(tw, trade_mode, equity_contracts)
stats["total_analyzed"] += 1
if result == "signal":
stats["total_signals"] += 1
elif result == "trade":
stats["total_signals"] += 1
stats["total_trades"] += 1
else:
stats["total_skipped"] += 1
Sleep(2000)
last_seen_guid = new_tweets[0]["guid"]
_G("last_guid", last_seen_guid)
_G("run_stats", stats)
if trade_mode == "trade":
monitor_positions(equity_contracts, market_info_cache)
show_dashboard(trade_mode, equity_contracts, market_info_cache, stats)
except Exception as e:
Log("⚠️ 主循环异常: " + str(e))
Sleep(CHECK_INTERVAL_S * 1000)
# ════════════════════════════════════════════════════════════════════════════
# 推文历史持久化
# ════════════════════════════════════════════════════════════════════════════
def save_tweet_history(tweet, result, analysis=None):
history = _G("tweet_history") or []
tickers = analysis.get("tickers", []) if analysis else []
direction = analysis.get("direction", "") if analysis else ""
confidence= analysis.get("confidence", 0) if analysis else 0
if direction == "neutral" or confidence < 50:
signal_type = "🎭 引流/提问"
elif confidence >= 80:
signal_type = "💎 强信号"
elif confidence >= 70:
signal_type = "📊 普通信号"
else:
signal_type = "⚠️ 弱信号"
history.insert(0, {
"date": tweet["date"],
"text": tweet["text"][:120],
"guid": tweet["guid"],
"result": result,
"tickers": tickers,
"direction": direction,
"confidence": confidence,
"signal_type": signal_type,
})
history = history[:MAX_TWEET_HISTORY]
_G("tweet_history", history)
# ════════════════════════════════════════════════════════════════════════════
# 命令处理
# ════════════════════════════════════════════════════════════════════════════
def handle_command(trade_mode, equity_contracts, market_info_cache, last_market_refresh):
cmd = GetCommand()
if not cmd:
return trade_mode, equity_contracts, market_info_cache, last_market_refresh
if cmd == "mode:notify":
trade_mode = "notify"
_G("tradeMode", "notify")
Log("📢 已切换为【仅通知】模式")
elif cmd == "mode:trade":
trade_mode = "trade"
_G("tradeMode", "trade")
Log("⚡ 已切换为【实际交易】模式")
elif cmd == "refresh_markets":
equity_contracts, market_info_cache = refresh_equity_contracts()
last_market_refresh = time.time()
elif cmd == "clear_history":
_G("tweet_history", [])
Log("🗑️ 推文历史已清空")
elif cmd.startswith("close:"):
ticker = cmd.replace("close:", "").strip()
sym = equity_contracts.get(ticker)
if not sym:
Log("❌ 合约不存在: " + ticker)
else:
pos = get_position(sym)
if pos:
side = "closebuy" if pos["Type"] == PD_LONG else "closesell"
exchange.CreateOrder(sym, side, -1, pos["Amount"])
clear_pos_state(ticker)
Log("✅ 手动平仓: " + ticker)
return trade_mode, equity_contracts, market_info_cache, last_market_refresh
# ════════════════════════════════════════════════════════════════════════════
# 动态刷新 TradFi EQUITY 合约表
# ════════════════════════════════════════════════════════════════════════════
def refresh_equity_contracts():
Log("🔄 刷新 TradFi EQUITY 合约表...")
try:
ms = exchange.GetMarkets()
except Exception as e:
Log("❌ GetMarkets 失败: " + str(e))
return {}, {}
new_map, cache = {}, {}
for key, market in ms.items():
info = market.get("Info", {}) or {}
sub_type = info.get("underlyingSubType", [])
if (
".swap" in key
and "TradFi" in sub_type
and info.get("underlyingType") == "EQUITY"
):
ticker = key.replace("_USDT.swap", "")
new_map[ticker] = key
cache[key] = {
"amountPrecision": market.get("AmountPrecision", 0),
"pricePrecision": market.get("PricePrecision", 2),
"ctVal": market.get("CtVal", 1) or 1,
"minQty": market.get("MinQty", 0) or 0,
}
Log("✅ 合约表刷新完成,共 {} 个 TradFi EQUITY 合约".format(len(new_map)))
Log("可交易标的: " + ", ".join(new_map.keys()))
return new_map, cache
# ════════════════════════════════════════════════════════════════════════════
# 拉取 RSS 推文
# ════════════════════════════════════════════════════════════════════════════
def fetch_latest_tweets():
try:
req = urllib.request.urlopen(RSSHUB_URL, timeout=10)
resp = req.read().decode("utf-8", errors="ignore")
except Exception as e:
Log("❌ RSS 请求失败: " + str(e))
return []
tweets = []
items = re.findall(r"<item>([\s\S]*?)</item>", resp)
for item in items:
title = _xml(item, "title")
desc = _xml(item, "description")
guid = _xml(item, "guid")
date = _xml(item, "pubDate")
desc = re.sub(r"<br\s*/?>", "\n", desc, flags=re.IGNORECASE)
desc = re.sub(r"<[^>]+>", " ", desc)
text = title + "\n" + desc
text = text.replace("&", "&").replace("<", "<").replace(">", ">")
text = text.replace(""", '"').replace("'", "'")
text = re.sub(r"\s{2,}", " ", text).strip()
if "[Subscribers Only]" in text:
continue
tweets.append({"guid": guid, "date": date, "text": text})
return tweets
def _xml(s, tag):
m = re.search(r"<" + tag + r"[^>]*>([\s\S]*?)</" + tag + r">", s)
return m.group(1).strip() if m else ""
# ════════════════════════════════════════════════════════════════════════════
# 处理单条推文
# ════════════════════════════════════════════════════════════════════════════
def process_tweet(tweet, trade_mode, equity_contracts):
Log("━" * 40)
Log("📅 时间: " + tweet["date"])
preview = tweet["text"][:300] + ("..." if len(tweet["text"]) > 300 else "")
Log("📝 推文: " + preview)
Log("🔗 链接: " + tweet["guid"])
analysis = analyze_with_llm(tweet["text"], equity_contracts)
if not analysis:
Log("❌ LLM 分析失败,跳过")
save_tweet_history(tweet, "skipped", None)
return "skipped"
direction_cn = {"long": "📈 看多", "short": "📉 看空", "neutral": "➖ 中性"}.get(
analysis["direction"], analysis["direction"])
conf_bar = "█" * (analysis["confidence"] // 10) + "░" * (10 - analysis["confidence"] // 10)
Log("🤖 LLM 解析结果:")
Log(" 方向: {} | 置信度: {}% [{}]".format(
direction_cn, analysis["confidence"], conf_bar))
Log(" 标的: {}".format(", ".join(analysis["tickers"]) if analysis["tickers"] else "无"))
Log(" 理由: " + analysis["reason"])
if analysis["direction"] == "neutral":
Log("⏭️ 方向中性,不产生信号")
save_tweet_history(tweet, "neutral", analysis)
return "neutral"
if analysis["confidence"] < MIN_CONFIDENCE:
Log("⏭️ 置信度 {}% 低于阈值 {}%,跳过".format(analysis["confidence"], MIN_CONFIDENCE))
save_tweet_history(tweet, "low_conf", analysis)
return "low_conf"
if not analysis.get("tickers"):
Log("⏭️ 无可交易标的,跳过")
save_tweet_history(tweet, "neutral", analysis)
return "neutral"
any_signal = False
any_trade = False
for ticker in analysis["tickers"]:
ticker = ticker.upper()
sym = equity_contracts.get(ticker)
if not sym:
Log(" ⚠️ {} 不在当前合约表中,跳过".format(ticker))
continue
t = exchange.GetTicker(sym)
price = t["Last"] if t else 0
direction_label = "📈 做多" if analysis["direction"] == "long" else "📉 做空"
if trade_mode == "notify":
Log("📢 信号通知 | {} @{} | 现价: {} | 置信度: {}%".format(
direction_label, ticker, price, analysis["confidence"]))
Log(" 💡 理由: " + analysis["reason"])
Log(" ⚠️ 当前【仅通知】模式,未实际下单")
any_signal = True
continue
if get_open_position_count(equity_contracts) >= MAX_POSITIONS:
Log(" ⚠️ 已达最大持仓数 {},跳过 {}".format(MAX_POSITIONS, ticker))
break
if get_position(sym):
Log(" ⚠️ {} 已有持仓,跳过重复开仓".format(ticker))
continue
ok = open_position(sym, ticker, analysis["direction"],
analysis["confidence"], analysis["reason"])
if ok:
any_trade = True
Sleep(1000)
if any_trade:
save_tweet_history(tweet, "trade", analysis)
return "trade"
if any_signal:
save_tweet_history(tweet, "signal", analysis)
return "signal"
save_tweet_history(tweet, "skipped", analysis)
return "skipped"
# ════════════════════════════════════════════════════════════════════════════
# LLM 分析推文(针对 Serenity 表达风格优化)
# ════════════════════════════════════════════════════════════════════════════
def analyze_with_llm(tweet_text, equity_contracts):
tradable = ", ".join(equity_contracts.keys())
system_prompt = (
"你是一个专门解读Twitter用户「Serenity」推文的交易信号提取器。"
"该用户是AI与半导体供应链分析师,你需要理解他特有的表达习惯:\n"
"1. 他很少直接说「买入」,而是通过描述公司供应链地位、壁垒、供需关系来暗示看多\n"
"2. 强烈看多的关键词:「I personally think」「undervalued」「going much higher」"
"「chokepoint」「structural」「thesis validated」「go brrr」「bullish」\n"
"3. 看空的关键词:「avoid」「overvalued」「nuking」「ban」「bearish」\n"
"4. 提问式推文(「Can anyone guess?」「Does anyone know?」「Who is it?」)"
"是在卖关子引流,本身不构成交易信号,direction 应为 neutral\n"
"5. 宏观趋势描述(如「Goldman上调资本开支」)若未点名具体标的态度,不构成信号\n"
"6. 付费内容(Subscribers Only)信息不完整,不构成信号\n"
"只输出合法 JSON,不输出任何其他内容,不加 markdown 代码块。"
)
user_prompt = "\n".join([
"分析以下推文,提取交易信号。",
"",
"当前可交易的美股合约(tickers 只能从这里选):",
tradable,
"",
"推文内容:",
tweet_text,
"",
"严格按如下 JSON 格式返回,所有字段不能缺少:",
'{"tickers":["NVDA"],"direction":"long","confidence":85,"reason":"简短中文理由"}',
"",
"字段说明:",
"· direction: long=明确看多 / short=明确看空 / neutral=中性或不明确",
"· confidence: 0-100,综合信号强度",
" - 作者明确表态(含关键词)+ 有具体逻辑:80-95",
" - 描述正面事实但未明确表态:55-75",
" - 提问/卖关子/互动/引流类:10-40(此时 direction 必须为 neutral)",
" - 宏观描述无具体标的态度:30-50(tickers 返回 [])",
" - 矛盾/中性/观望:< 50",
"· tickers: 只含作者明确表达态度的股票,必须在可交易列表中",
" - 仅被提及但无明确态度 → 不纳入",
" - 付费/纯宏观/无具体标的 → 返回 []",
"· reason: 引用推文中的关键词或句子说明判断依据,中文,15字以内",
"",
"注意:",
"1. 提问式推文本身不是信号,direction 必须返回 neutral",
"2. 同一推文提及多个标的,只对作者明确表态的纳入 tickers",
"3. 描述宏观趋势但未点名具体看多哪只股票,tickers 返回 []",
"4. 「Valuations do seem compelling」类含糊表态,confidence 不超过 65",
])
body = json.dumps({
"model": LLM_MODEL,
"max_tokens": 512,
"messages": [
{"role": "system", "content": system_prompt},
{"role": "user", "content": user_prompt},
]
}).encode("utf-8")
try:
req = urllib.request.Request(
LLM_BASE_URL, data=body,
headers={"Content-Type": "application/json",
"Authorization": "Bearer " + LLM_API_KEY},
method="POST"
)
with urllib.request.urlopen(req, timeout=30) as resp:
data = json.loads(resp.read().decode("utf-8"))
except Exception as e:
Log("❌ LLM API 调用失败: " + str(e))
return None
if "error" in data:
Log("❌ LLM 返回错误: " + str(data["error"].get("message", data["error"])))
return None
try:
text = data["choices"][0]["message"]["content"].strip()
if not text:
Log("⚠️ LLM 返回空内容,重试(换 deepseek-chat)...")
body2 = json.dumps({
"model": "deepseek-chat",
"max_tokens": 512,
"messages": [
{"role": "system", "content": system_prompt},
{"role": "user", "content": user_prompt},
]
}).encode("utf-8")
req2 = urllib.request.Request(
LLM_BASE_URL, data=body2,
headers={"Content-Type": "application/json",
"Authorization": "Bearer " + LLM_API_KEY},
method="POST"
)
with urllib.request.urlopen(req2, timeout=30) as resp2:
data = json.loads(resp2.read().decode("utf-8"))
text = data["choices"][0]["message"]["content"].strip()
if not text:
Log("❌ LLM 两次均返回空内容")
return None
text = re.sub(r"<think>[\s\S]*?</think>", "", text).strip()
text = re.sub(r"```json\s*|```\s*", "", text).strip()
result = json.loads(text)
if not isinstance(result.get("tickers"), list) or \
"direction" not in result or "confidence" not in result:
Log("⚠️ LLM 返回格式不完整: " + text)
return None
return result
except Exception:
Log("⚠️ LLM 返回解析失败: " + str(data)[:200])
return None
# ════════════════════════════════════════════════════════════════════════════
# 开仓
# ════════════════════════════════════════════════════════════════════════════
def open_position(sym, ticker, direction, confidence, reason):
acc = exchange.GetAccount()
if not acc:
Log("❌ 获取账户失败")
return False
equity = acc.get("Equity") or acc.get("Balance", 0)
t = exchange.GetTicker(sym)
if not t:
Log("❌ 获取行情失败: " + sym)
return False
info = get_market_info(sym)
qty = _N(equity * SINGLE_POS_PCT * LEVERAGE / t["Last"] / info["ctVal"],
info["amountPrecision"])
if qty <= 0 or qty < info["minQty"]:
Log("⚠️ {} 下单量 {} 低于最小值 {},跳过".format(ticker, qty, info["minQty"]))
return False
side = "buy" if direction == "long" else "sell"
order_id = exchange.CreateOrder(sym, side, -1, qty)
if not order_id:
Log("❌ {} 下单失败".format(ticker))
return False
Sleep(2000)
pos = get_position(sym)
if not pos:
Log("⚠️ {} 下单后未找到持仓".format(ticker))
return False
stop_price = (t["Last"] * (1 - STOP_LOSS_PCT / 100) if direction == "long"
else t["Last"] * (1 + STOP_LOSS_PCT / 100))
_G("pos_stop_" + ticker, stop_price)
_G("pos_peak_" + ticker, 0)
_G("pos_trail_" + ticker, False)
_G("pos_reason_" + ticker, reason)
_G("pos_conf_" + ticker, confidence)
_G("pos_open_time_" + ticker, int(time.time()))
pp = info["pricePrecision"]
Log("✅ 开仓成功 | {} {} | 价: {:.{}f} | 数量: {} | 止损: {:.{}f} | 置信度: {}%".format(
"📈 做多" if direction == "long" else "📉 做空",
ticker, t["Last"], pp, qty, stop_price, pp, confidence))
return True
# ════════════════════════════════════════════════════════════════════════════
# 持仓监控:回撤止盈 + 硬止损
# ════════════════════════════════════════════════════════════════════════════
def monitor_positions(equity_contracts, market_info_cache):
positions = get_equity_positions(equity_contracts)
for pos in positions:
sym = pos["Symbol"]
ticker = sym.replace("_USDT.swap", "")
is_long = pos["Type"] == PD_LONG
t = exchange.GetTicker(sym)
if not t:
continue
pnl_pct = (t["Last"] - pos["Price"]) / pos["Price"] * 100 * (1 if is_long else -1)
peak = _G("pos_peak_" + ticker) or 0
if pnl_pct > peak:
peak = pnl_pct
_G("pos_peak_" + ticker, peak)
trail = _G("pos_trail_" + ticker) or False
if not trail and peak >= TRAIL_ACTIVATE_PCT:
trail = True
_G("pos_trail_" + ticker, True)
Log("🎯 {} 浮盈达 {:.1f}%,启动回撤止盈追踪".format(ticker, peak))
exit_reason = None
stop_price = _G("pos_stop_" + ticker)
if stop_price:
if (is_long and t["Last"] <= stop_price) or \
(not is_long and t["Last"] >= stop_price):
exit_reason = "🛑 硬止损触发 | 浮盈: {:.2f}%".format(pnl_pct)
if not exit_reason and trail:
giveback_pct = max(TRAIL_GIVEBACK_BASE, peak * 0.35)
drawdown = peak - pnl_pct
if drawdown >= giveback_pct:
exit_reason = "🎯 回撤止盈 | 峰值+{:.1f}% 回撤{:.1f}% ≥ 阈值{:.1f}%".format(
peak, drawdown, giveback_pct)
if exit_reason:
close_side = "closebuy" if is_long else "closesell"
exchange.CreateOrder(sym, close_side, -1, pos["Amount"])
Sleep(1500)
clear_pos_state(ticker)
Log("📤 平仓 | {} | {}".format(ticker, exit_reason))
# ════════════════════════════════════════════════════════════════════════════
# 仪表板(4张表)
# ════════════════════════════════════════════════════════════════════════════
def show_dashboard(trade_mode, equity_contracts, market_info_cache, stats):
acc = exchange.GetAccount()
equity = (acc.get("Equity") or acc.get("Balance", 0)) if acc else 0
positions = get_equity_positions(equity_contracts) if trade_mode == "trade" else []
mode_label = "📢 仅通知" if trade_mode == "notify" else "⚡ 实际交易"
elapsed = int(time.time()) - stats.get("start_time", int(time.time()))
hours, rem = divmod(elapsed, 3600)
uptime = "{}h {}m".format(hours, rem // 60)
# ── 表1:系统总览 ─────────────────────────────────────────────────────
t1 = {
"type": "table",
"title": "📡 推文驱动交易系统 · 运行 " + uptime,
"cols": ["💰 账户权益", "📊 当前模式", "📋 持仓数",
"⚡ 单仓%", "🛑 硬止损", "🔄 合约池", "操作"],
"rows": [[
"${:.2f}".format(equity),
mode_label,
"{}/{}".format(len(positions), MAX_POSITIONS),
"{}%".format(int(SINGLE_POS_PCT * 100)),
"{}%".format(STOP_LOSS_PCT),
"{} 个".format(len(equity_contracts)),
[
{"type": "button", "cmd": "mode:notify", "name": "📢 仅通知"},
{"type": "button", "cmd": "mode:trade", "name": "⚡ 实际交易"},
{"type": "button", "cmd": "refresh_markets", "name": "🔄 刷新合约"},
{"type": "button", "cmd": "clear_history", "name": "🗑️ 清空历史"},
]
]]
}
# ── 表2:推文处理统计 ─────────────────────────────────────────────────
history = _G("tweet_history") or []
strong = sum(1 for h in history if h.get("confidence", 0) >= 80
and h.get("direction") != "neutral")
normal = sum(1 for h in history if 70 <= h.get("confidence", 0) < 80
and h.get("direction") != "neutral")
total = max(stats.get("total_analyzed", 0), 1)
signal_rate = stats.get("total_signals", 0) / total * 100
start_str = time.strftime("%m/%d %H:%M", time.localtime(stats.get("start_time", int(time.time()))))
now_str = time.strftime("%m/%d %H:%M", time.localtime())
t2 = {
"type": "table",
"title": "📊 推文处理统计 · 区间: {} → {}".format(start_str, now_str),
"cols": ["📨 总分析", "💎 强信号(≥80%)", "📊 普通信号(70-79%)",
"⏭️ 跳过/中性", "⚡ 实际下单", "📈 信号率", "⏱️ 运行时长"],
"rows": [[
str(stats.get("total_analyzed", 0)),
str(strong),
str(normal),
str(stats.get("total_skipped", 0)),
str(stats.get("total_trades", 0)),
"{:.1f}%".format(signal_rate),
uptime,
]]
}
# ── 表3:持仓明细 ─────────────────────────────────────────────────────
t3 = {
"type": "table",
"title": "📋 当前持仓(回撤止盈 · 无固定止盈)",
"cols": ["🪙 标的", "📍 方向", "🏷️ 入场价", "💲 现价",
"📊 浮盈%", "🏆 峰值%", "🎯 止盈状态", "⏱️ 持仓时长", "置信度", "操作"],
"rows": []
}
if trade_mode == "notify":
t3["rows"].append(["📢 【仅通知】模式,切换「实际交易」后开始建仓",
"-", "-", "-", "-", "-", "-", "-", "-", "-"])
elif not positions:
t3["rows"].append(["😴 暂无持仓,等待置信度≥{}%的推文信号".format(MIN_CONFIDENCE),
"-", "-", "-", "-", "-", "-", "-", "-", "-"])
else:
for pos in positions:
sym = pos["Symbol"]
ticker = sym.replace("_USDT.swap", "")
is_long = pos["Type"] == PD_LONG
tk = exchange.GetTicker(sym)
if not tk:
continue
info = get_market_info(sym, market_info_cache)
pp = info["pricePrecision"]
pnl_pct = (tk["Last"] - pos["Price"]) / pos["Price"] * 100 * (1 if is_long else -1)
peak = _G("pos_peak_" + ticker) or 0
trail = _G("pos_trail_" + ticker) or False
conf = _G("pos_conf_" + ticker) or "-"
gb = max(TRAIL_GIVEBACK_BASE, peak * 0.35)
open_t = _G("pos_open_time_" + ticker) or int(time.time())
held_h, held_rem = divmod(int(time.time()) - open_t, 3600)
held_str = "{}h{}m".format(held_h, held_rem // 60)
t3["rows"].append([
"🪙 " + ticker,
"🟢 做多" if is_long else "🔴 做空",
"{:.{}f}".format(pos["Price"], pp),
"{:.{}f}".format(tk["Last"], pp),
"{}{:.2f}%".format("🟢 +" if pnl_pct >= 0 else "🔴 ", pnl_pct),
"+{:.1f}%".format(peak),
"🎯 追踪 回撤>{:.0f}%离场".format(gb) if trail
else "⏸ 浮盈>{:.0f}%激活".format(TRAIL_ACTIVATE_PCT),
held_str,
"{}%".format(conf),
[{"type": "button", "cmd": "close:" + ticker, "name": "平仓"}]
])
# ── 表4:推文历史 ─────────────────────────────────────────────────────
result_label = {
"signal": "📢 通知",
"trade": "⚡ 下单",
"neutral": "➖ 中性",
"low_conf": "⏭️ 低置信",
"skipped": "❌ 跳过",
}
dir_label = {"long": "📈 多", "short": "📉 空", "neutral": "➖", "": "─"}
t4 = {
"type": "table",
"title": "📜 推文历史(显示 {} / 共 {} 条)".format(min(len(history), 20), len(history)),
"cols": ["📅 时间", "🪙 标的", "📍 方向", "🎯 置信度", "💬 信号类型", "📝 推文摘要", "🏷️ 结果"],
"rows": []
}
if not history:
t4["rows"].append(["─", "─", "─", "─", "─", "暂无历史记录", "─"])
else:
for h in history[:20]:
tickers_str = " ".join(h.get("tickers", [])) or "─"
conf = h.get("confidence", 0)
conf_str = "{}% {}".format(conf, "█" * (conf // 20) + "░" * (5 - conf // 20)) if conf else "─"
t4["rows"].append([
h.get("date", "")[:16],
tickers_str,
dir_label.get(h.get("direction", ""), "─"),
conf_str,
h.get("signal_type", "─"),
h.get("text", "")[:60] + "...",
result_label.get(h.get("result", ""), h.get("result", "")),
])
LogStatus(
"`" + json.dumps(t1, ensure_ascii=False) + "`\n\n" +
"`" + json.dumps(t2, ensure_ascii=False) + "`\n\n" +
"`" + json.dumps(t3, ensure_ascii=False) + "`\n\n" +
"`" + json.dumps(t4, ensure_ascii=False) + "`"
)
# ════════════════════════════════════════════════════════════════════════════
# 工具函数
# ════════════════════════════════════════════════════════════════════════════
def get_equity_positions(equity_contracts):
try:
all_pos = exchange.GetPositions()
except Exception:
return []
if not all_pos:
return []
return [p for p in all_pos
if p.get("Symbol", "").endswith("_USDT.swap")
and p["Symbol"].replace("_USDT.swap", "") in equity_contracts
and abs(p.get("Amount", 0)) > 0]
def get_open_position_count(equity_contracts):
return len(get_equity_positions(equity_contracts))
def get_position(sym):
try:
all_pos = exchange.GetPositions(sym)
except Exception:
return None
if not all_pos:
return None
for p in all_pos:
if abs(p.get("Amount", 0)) > 0:
return p
return None
def get_market_info(sym, cache=None):
if cache and sym in cache:
return cache[sym]
return {"amountPrecision": 0, "pricePrecision": 2, "ctVal": 1, "minQty": 0}
def clear_pos_state(ticker):
for k in ("pos_stop_", "pos_peak_", "pos_trail_",
"pos_reason_", "pos_conf_", "pos_open_time_"):
_G(k + ticker, None)- 1