Type/to search

社媒KOL信号跟踪_币安TradFi股票合约

Common strategy
Created: 2026-06-05 18:02:40
Last modified: a month ago
2
Follow
496
Followers

社媒 KOL 信号跟踪策略(币安 TradFi 股票合约)

策略简介

本策略是一套基于 X/Twitter KOL 推文的事件驱动交易系统。策略通过 RSSHub 实时获取指定博主的新推文,再调用大语言模型解析推文中是否包含明确的股票交易信号,最后将信号映射到币安 TradFi 股票永续合约并执行交易。

策略最初围绕 X 上的 AI/半导体供应链分析博主 Serenity 设计。该博主常通过“瓶颈公司”“供应链错配”“AI 上游核心环节”等逻辑分析美股标的,因此策略重点不是捕捉普通喊单,而是尝试把这类社媒研究观点转化为结构化交易信号。

核心逻辑

  1. 实时获取 KOL 推文

    策略通过 RSSHub 订阅指定 X 用户的推文源,定时拉取最新内容。
    每条推文会被解析为正文、发布时间、链接和唯一 ID,并通过本地记录避免重复处理。

  2. 使用 LLM 解析自然语言信号

    新推文会被发送给大语言模型,由模型判断其中是否包含明确交易含义。

    输出结果包括:

    字段说明
    tickers推文明确涉及的股票代码
    direction方向,支持 longshortneutral
    confidence信号置信度,范围 0-100
    reason解析理由

    策略会特别识别 Serenity 这类博主的表达习惯,例如供应链瓶颈、低估、结构性机会、thesis validated 等偏多信号;同时过滤提问、卖关子、互动引流和未点名标的的宏观描述。

  3. 动态匹配币安 TradFi 股票合约

    策略不会写死可交易标的,而是定时扫描交易所市场信息,筛选:

    • USDT 永续合约;
    • underlyingSubType 包含 TradFi
    • underlyingTypeEQUITY

    这样当币安上线新的股票类合约时,策略可以自动纳入可交易池。

  4. 置信度过滤

    只有当推文方向明确、标的存在于可交易合约池中,并且 LLM 给出的置信度不低于设定阈值时,策略才会生成交易信号。

    默认过滤逻辑:

    推文类型处理方式
    明确看多/看空且有逻辑支撑允许交易
    只是提到某个股票跳过
    提问式、卖关子、互动类内容视为中性
    宏观叙事但没有具体标的跳过
    置信度不足跳过
  5. 通知模式与实盘模式

    策略提供两种运行模式:

    模式说明
    notify仅记录和推送信号,不实际下单
    trade信号满足条件后自动下单

    默认建议先使用通知模式运行一段时间,人工检查 LLM 对推文的理解是否稳定,再切换到实盘模式。

  6. 自动交易与仓位控制

    当满足交易条件后,策略会按账户权益的一定比例开仓。
    默认单笔仓位较小,并限制最大同时持仓数量,避免单个 KOL 或单条推文对账户造成过大影响。

  7. 止损与回撤止盈

    策略内置两类风控:

    风控方式说明
    硬止损入场后亏损达到设定比例立即平仓
    回撤止盈浮盈达到启动阈值后,按峰值利润回撤幅度触发离场

    这种设计适合 Serenity 一类中期逻辑型信号:亏损快速截断,盈利则尽量让趋势继续发展。

交易流程

text
RSSHub 获取 KOL 最新推文 ↓ 过滤重复推文 ↓ LLM 解析 ticker / direction / confidence / reason ↓ 匹配币安 TradFi 股票永续合约池 ↓ 过滤中性、低置信度、不可交易标的 ↓ 通知模式:只输出信号 实盘模式:按资金比例开仓 ↓ 硬止损 + 回撤止盈管理持仓

主要参数

参数说明
RSSHUB_URLRSSHub 推文订阅地址
LLM_API_KEY大语言模型 API Key
LLM_MODEL用于解析推文的模型
LLM_BASE_URLLLM 接口地址
CHECK_INTERVAL_S推文检查间隔
MARKET_REFRESH_STradFi 合约池刷新间隔
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 建议保持较低,避免单条推文造成过大风险暴露。
  • 先使用通知模式观察一段时间,再根据实际信号质量决定是否开启实盘。
  • 对引流式、提问式、未点名标的的推文,应保持严格过滤。

相关文章

Source
Python
# ════════════════════════════════════════════════════════════════════════════
#  推文驱动交易系统(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("&amp;", "&").replace("&lt;", "<").replace("&gt;", ">")
        text = text.replace("&quot;", '"').replace("&#39;", "'")
        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)
Comment
All comments (0)
No data
No data
  • 1
Forums
PINE Language
Get the app
iPhone Download
© 2015 - ∞ INVENTOR PTE LTD (SG)