盘后 4 小时:你在刷剧,别人在计算明天的信号

凌晨 2 点,你被一条推送震醒——某家你持仓公司的 CEO 突然宣布辞职。睡意全无,盯着手机屏幕开始焦虑:明天开盘怎么办?挂跌停跑,还是等反弹?

而隔壁老王早就睡了。他的量化系统在后半夜已经完成了所有计算:

  • CEO 辞职消息发布时,对应板块期货下跌 1.2%,预计带动正股低开 3-5%
  • 恐慌指数跳升 4.5 个点,put/call 比从 0.8 飙到 1.3,期权市场定价了更大的波动区间
  • 同日财报历史数据显示,相似事件下前 15 分钟的低吸胜率约 62%,但需配合盘口流动性确认
  • 盘前 30 分钟关键价位已标记,系统告警阈值已调整完毕

差距不在信息,在于信号预计算的完成度。

本文从工程视角拆解:收盘后到次日开盘前这段"隐形窗口",可以系统性地计算哪些信号?每个信号的计算逻辑、优先级和实操代码怎么写?重点解决一个核心问题——让你在 9:15 集合竞价开始前,就已经知道今天开盘大概率怎么走,而不是临时看盘拍脑袋。


一、为什么盘前 4 小时是量化系统的"加时段"

1.1 三个被低估的数据事实

事实一:美股盘后成交量约占全天 3-5%,但信息密度极高

盘后交易(After-hours)并非可有可无的"垃圾时间"。重大事件(财报、并购、高管变动)发布后,超 40% 的价格发现发生在盘后交易段。这段时间的订单簿结构直接决定次日开盘价的基准。

事实二:隔夜风险敞口是可以量化的

如果你持有隔夜仓位,你承担的不只是价格风险,还包括:

风险类型 可量化程度 计算窗口
宏观事件(Fed 讲话、CPI) 高(期货领先指标) T-1 日盘后
个股公告(财报、并购) 高(盘后交易价格发现) T-1 日盘后
地缘/商品冲击 中(期货/数字货币领先) T-1 日盘后至 T 日盘前
隔夜流动性枯竭 高(VIX 结构分析) 每日盘后

事实三:盘前准备的质量决定开盘执行的胜率

很多交易者把开盘前 15 分钟用来盯盘——这是最低效的用法。正确的姿势是:把盯盘时间留给确认和微调,把分析工作留给盘后和盘前。 系统在盘前已经输出"今日开盘预期",你只需要在 9:15 对比实际盘口与预期,决定是否触发预设信号。

1.2 信号预计算的系统架构

T日收盘 (16:00 ET)
    │
    ├─[盘后阶段 16:00 - 20:00 ET]
    │   ├─ 事件扫描:新闻/API/公告解析
    │   ├─ 盘后交易分析:价格发现、成交量异动
    │   └─ 隔夜仓位风险评估
    │
    ├─[深夜阶段 20:00 - 04:00 ET]
    │   ├─ 宏观数据预加载(FedWatch、CPI 预期)
    │   ├─ 期货夜盘监控(S&P500 futures、原油、黄金)
    │   └─ 期权未平仓量(OI)结构分析
    │
    └─[盘前阶段 04:00 - 09:15 ET]
        ├─ 盘前流动性预估
        ├─ 集合竞价价格预测
        ├─ 关键价位标记与告警设置
        └─ 策略参数动态调整
            │
            ▼
        T日开盘 (09:30 ET) → 预设信号触发 → 执行

接下来的章节会逐一拆解每个阶段的核心信号及其计算逻辑,并给出可直接运行的生产级 Python 代码


二、盘后事件扫描:把"消息"转化为"可计算信号"

2.1 信号类型与优先级矩阵

盘后阶段(16:00-20:00 ET)是最宝贵的信息加工窗口。并非所有事件都需要同等对待,以下是按信息密度 × 信号衰减速度分类的信号矩阵:

信号类型 数据来源 典型衰减周期 预计算价值 优先级
财报/业绩预警 公司公告 API 30 分钟内定价完毕 高(预期差可量化) P0
并购/要约收购 交易所公告 2-4 小时 极高(价差收敛明确) P0
高管变动/诉讼 新闻流 不确定(市场解读分歧) 中(需配合盘前盘口确认) P1
分析师评级调整 券商报告 30 分钟至数小时 中(定价分歧可被机构消化) P1
Fed 官员讲话 央行官网 即时 高(利率预期实时变化) P0
大宗商品冲击 期货市场 取决于事件类型 高(期货 23 小时交易) P0

2.2 事件驱动的价格预期估算

对于 P0 级事件(财报、并购),我们可以在盘后计算一个关键指标:隐含开盘价区间

这不是预测,而是基于盘后交易价格发现的合理区间估算:

import requests
import os
import time
from datetime import datetime, date

class AfterMarketEventScanner:
    """
    盘后事件扫描器
    监控盘后公告,估算次日隐含开盘区间
    ⚠️ 此代码为生产级参考实现,需要根据实际数据源适配
    """

    def __init__(self, api_key: str):
        self.api_key = api_key
        self.headers = {"X-API-Key": api_key}

    def estimate_implied_open(self, symbol: str, event_price: float,
                               prev_close: float, confidence: float = 0.68):
        """
        估算隐含开盘价区间

        Args:
            symbol: 股票代码
            event_price: 盘后交易价格
            prev_close: 昨日收盘价
            confidence: 置信区间(0.68 ≈ 1σ,0.95 ≈ 2σ)

        Returns:
            dict: 含隐含涨跌幅和区间估算
        """
        implied_move = (event_price - prev_close) / prev_close
        # 基于历史波动率估算区间宽度
        # 简化模型:使用 ATR 的 0.5 倍作为日内波动基准
        # ⚠️ 生产环境应从 TickDB kline 历史数据计算真实 ATR
        symbol_info = self._get_symbol_info(symbol)
        atr_factor = 0.5  # 保守估计:实际应计算真实 ATR

        # 置信区间宽度与事件类型相关
        # 财报事件:区间宽;并购公告:区间窄
        interval_multiplier = 1.0 if "acquisition" not in str(symbol_info) else 0.3

        return {
            "symbol": symbol,
            "prev_close": prev_close,
            "event_price": event_price,
            "implied_move_pct": round(implied_move * 100, 2),
            "lower_bound_pct": round((implied_move - confidence * interval_multiplier) * 100, 2),
            "upper_bound_pct": round((implied_move + confidence * interval_multiplier) * 100, 2),
            "direction_bias": "bullish" if implied_move > 0.01 else "bearish" if implied_move < -0.01 else "neutral",
            "confidence": confidence,
            "timestamp": datetime.utcnow().isoformat()
        }

    def _get_symbol_info(self, symbol: str) -> dict:
        """查询交易品种基础信息"""
        # ⚠️ 生产环境:实际调用 TickDB symbols 接口
        # response = requests.get(
        #     f"https://api.tickdb.ai/v1/symbols/{symbol}",
        #     headers=self.headers,
        #     timeout=(3.05, 10)
        # )
        # handle_api_error(response, symbol)
        return {"symbol": symbol, "type": "stock"}

    def scan_and_rank(self, watchlist: list) -> list:
        """
        扫描观察列表,按隐含波动排序输出优先级

        Returns:
            按绝对预期波动率降序排列的事件列表
        """
        results = []
        for symbol in watchlist:
            try:
                # 实际场景中,这里需要接入新闻/API 数据源
                # 获取盘后交易价格和事件类型
                event_data = self._fetch_afterhours_data(symbol)

                if event_data:
                    estimate = self.estimate_implied_open(
                        symbol=symbol,
                        event_price=event_data["price"],
                        prev_close=event_data["prev_close"],
                        confidence=0.68
                    )
                    results.append(estimate)
            except Exception as e:
                # 记录错误但不中断整个扫描流程
                print(f"[WARN] {symbol} 扫描失败: {e}")
                continue

        # 按隐含波动率绝对值降序排列
        results.sort(key=lambda x: abs(x["implied_move_pct"]), reverse=True)
        return results

    def _fetch_afterhours_data(self, symbol: str) -> dict:
        """获取盘后交易数据"""
        # ⚠️ 占位实现。生产环境应接入实际数据源
        # 示例:从 TickDB 盘后交易频道获取(部分品种支持)
        # response = requests.get(
        #     f"https://api.tickdb.ai/v1/market/trades/latest",
        #     headers=self.headers,
        #     params={"symbol": symbol},
        #     timeout=(3.05, 10)
        # )
        raise NotImplementedError(
            "需要接入实际数据源(如新闻 API、财报 API)后替换此方法"
        )


# ========== 使用示例 ==========
if __name__ == "__main__":
    api_key = os.environ.get("TICKDB_API_KEY")
    scanner = AfterMarketEventScanner(api_key=api_key)

    watchlist = ["AAPL.US", "TSLA.US", "NVDA.US"]
    ranked_events = scanner.scan_and_rank(watchlist)

    print("=== 盘后事件扫描结果 ===")
    for i, event in enumerate(ranked_events, 1):
        print(f"\n#{i} {event['symbol']}")
        print(f"  隐含波动: {event['implied_move_pct']:+.2f}%")
        print(f"  区间: [{event['lower_bound_pct']:+.2f}%, {event['upper_bound_pct']:+.2f}%]")
        print(f"  方向偏好: {event['direction_bias']}")

工程预警

  • _fetch_afterhours_data 方法需要接入实际新闻/公告数据源(如 Alpha Vantage、Polygon、Finnhub),TickDB 的 trades 接口目前不支持美股,如需美股逐笔盘后交易数据需补充数据源
  • 隐含开盘区间估算使用简化模型(固定 ATR 倍数),生产环境应基于历史数据计算真实 ATR 和事件类型权重

三、期货夜盘:宏观信号的实时捕获

3.1 为什么期货是盘前最可靠的方向指标

S&P500 期货(ES)、纳斯达克期货(NQ)在美东时间周日至周四 18:00 ET 开盘,次日 17:00 ET 收盘。这意味着:

  • 在美股正式开盘前,期货市场已经交易了 15+ 小时
  • 宏观事件(隔夜亚洲市场、Fed 讲话、大宗商品)会率先在期货价格中反映
  • 期货的 pre-market gap 百分比是次日开盘价的强力先验指标

关键指标:

  • 期货隐含开盘(Fair Value):期货价格与理论价值的偏差
  • 隔夜净变化:期货收盘价 vs 当日收盘价的差异
  • 波动率锥:基于期货期权的隐含波动率期限结构

3.2 期货信号预计算代码

import asyncio
import websockets
import json
import os
import time
import random
from datetime import datetime
from typing import Optional

class FuturesSignalMonitor:
    """
    期货信号监控器(WebSocket 实时版)
    监控 S&P500 期货、纳斯达克期货、原油、黄金的盘前动态

    ⚠️ 生产环境高频场景建议使用 aiohttp/asyncio 架构
    """

    def __init__(self, api_key: str):
        self.api_key = api_key
        # TickDB 支持的期货品种示例(需确认实际 symbol)
        self.futures_symbols = {
            "ES": "ES.CUR",   # S&P500 E-mini 期货
            "NQ": "NQ.CUR",   # 纳斯达克 E-mini 期货
            "CL": "CL.CUR",   # WTI 原油
            "GC": "GC.CUR",   # 黄金
        }
        self._price_history = {s: [] for s in self.futures_symbols}
        self._last_heartbeat = time.time()

    async def connect_with_retry(self, uri: str, max_retries: int = 5):
        """带指数退避的 WebSocket 连接"""
        base_delay, max_delay = 1.0, 30.0

        for attempt in range(max_retries):
            try:
                # ⚠️ TickDB WebSocket 鉴权通过 URL 参数传递
                async with websockets.connect(
                    f"{uri}?api_key={self.api_key}"
                ) as ws:
                    await self._websocket_loop(ws, uri)
            except websockets.exceptions.ConnectionClosed as e:
                delay = min(base_delay * (2 ** attempt), max_delay)
                jitter = random.uniform(0, delay * 0.1)
                wait_time = delay + jitter
                print(f"[WARN] WebSocket 连接断开 (attempt {attempt + 1}): {e}")
                print(f"[INFO] {wait_time:.1f} 秒后重连...")
                time.sleep(wait_time)
            except Exception as e:
                print(f"[ERROR] 未知连接错误: {e}")
                break

    async def _websocket_loop(self, ws, uri: str):
        """WebSocket 主循环"""
        print(f"[INFO] WebSocket 连接成功: {uri}")
        ping_interval = 30  # TickDB 推荐心跳间隔

        while True:
            try:
                # 发送心跳保活(部分 WebSocket 服务需要)
                # await ws.ping()

                # 订阅期货行情(具体格式参考 TickDB WebSocket API 文档)
                subscribe_msg = {
                    "cmd": "subscribe",
                    "args": list(self.futures_symbols.values())
                }
                await ws.send(json.dumps(subscribe_msg))

                # 收取实时数据,超时自动重连
                while True:
                    try:
                        message = await asyncio.wait_for(
                            ws.recv(),
                            timeout=ping_interval + 5
                        )
                        data = json.loads(message)
                        self._process_futures_tick(data)
                    except asyncio.TimeoutError:
                        # 超时,重新发送订阅以保活
                        await ws.send(json.dumps(subscribe_msg))
                        self._last_heartbeat = time.time()

            except websockets.exceptions.ConnectionClosed:
                raise  # 触发外层重连逻辑

    def _process_futures_tick(self, data: dict):
        """处理期货行情推送"""
        # TickDB depth 频道数据格式:
        # {"symbol": "ES.CUR", "type": "depth", "bids": [...], "asks": [...]}
        symbol = data.get("symbol")
        if symbol not in self.futures_symbols.values():
            return

        tick_type = data.get("type")

        if tick_type == "depth":
            # 深度数据用于流动性分析和盘前价差预估
            bids = data.get("bids", [])
            asks = data.get("asks", [])
            if bids and asks:
                spread = asks[0][0] - bids[0][0]
                mid_price = (asks[0][0] + bids[0][0]) / 2
                best_bid_vol = bids[0][1]
                best_ask_vol = asks[0][1]
                liquidity_ratio = best_ask_vol / best_bid_vol if best_bid_vol > 0 else 0

                self._price_history[symbol].append({
                    "time": datetime.utcnow().isoformat(),
                    "mid": mid_price,
                    "spread": spread,
                    "liquidity_ratio": liquidity_ratio
                })

                # 保留最近 100 条记录防止内存膨胀
                if len(self._price_history[symbol]) > 100:
                    self._price_history[symbol] = self._price_history[symbol][-100:]

                # 实时告警:流动性失衡
                if abs(1 - liquidity_ratio) > 0.5:
                    print(f"[ALERT] {symbol}: 流动性失衡! "
                          f"买卖压力比 = {liquidity_ratio:.2f}, "
                          f"当前价差 = {spread:.2f}")

    def calculate_premarket_bias(self, symbol_key: str) -> dict:
        """
        基于盘前期货数据计算方向偏差

        Returns:
            含均值、波动率、趋势方向的统计摘要
        """
        history = self._price_history[self.futures_symbols.get(symbol_key, symbol_key)]
        if len(history) < 5:
            return {"status": "insufficient_data", "samples": len(history)}

        mid_prices = [h["mid"] for h in history]
        spreads = [h["spread"] for h in history]

        import statistics
        mean_price = statistics.mean(mid_prices)
        latest_price = mid_prices[-1]
        move_from_mean = (latest_price - mean_price) / mean_price * 100

        return {
            "symbol": symbol_key,
            "latest": round(latest_price, 2),
            "mean_premarket": round(mean_price, 2),
            "move_from_mean_pct": round(move_from_mean, 2),
            "avg_spread": round(statistics.mean(spreads), 4),
            "samples": len(history),
            "bias": "bullish" if move_from_mean > 0.5 else "bearish" if move_from_mean < -0.5 else "neutral",
            "timestamp": datetime.utcnow().isoformat()
        }

    def generate_direction_report(self) -> str:
        """生成宏观方向综合报告"""
        report_lines = [
            "\n========== 盘前期货方向报告 ==========",
            f"生成时间 (UTC): {datetime.utcnow().strftime('%Y-%m-%d %H:%M:%S')}",
            ""
        ]

        for key in self.futures_symbols:
            bias = self.calculate_premarket_bias(key)
            status = bias.get("status", "ok")

            if status == "insufficient_data":
                report_lines.append(f"{key}: 数据不足 ({bias['samples']} 条),跳过")
                continue

            emoji = {"bullish": "🟢", "bearish": "🔴", "neutral": "⚪️"}.get(bias["bias"], "")
            report_lines.append(
                f"{emoji} {key}: {bias['bias'].upper()} "
                f"(偏离均值 {bias['move_from_mean_pct']:+.2f}%, "
                f"样本 {bias['samples']})"
            )

        report_lines.append("=" * 40)
        return "\n".join(report_lines)


async def main():
    api_key = os.environ.get("TICKDB_API_KEY")
    monitor = FuturesSignalMonitor(api_key=api_key)

    # 后台持续监控
    ws_uri = "wss://api.tickdb.ai/v1/ws/market"  # ⚠️ 占位 URI,需参考实际 TickDB WebSocket 端点

    try:
        await asyncio.wait_for(
            monitor.connect_with_retry(ws_uri),
            timeout=60  # 演示用:60 秒后退出;生产环境移除此限制
        )
    except asyncio.TimeoutError:
        print("[INFO] 演示结束,打印报告:")
        print(monitor.generate_direction_report())


if __name__ == "__main__":
    asyncio.run(main())

工程预警

  • wss://api.tickdb.ai/v1/ws/market 为占位 URI,TickDB 实际 WebSocket 端点格式请查阅官方文档
  • 期货 symbol 格式需以 TickDB 返回的品种列表为准,ES.CUR 等仅为示例
  • 生产环境建议使用 asyncio 全异步架构,配合独立的消息队列(如 Redis)做数据缓冲

四、订单簿深度分析:集合竞价的微观结构预测

4.1 集合竞价的本质是一个迷你拍卖市场

09:15-09:28 ET 的预开盘(Pre-opening)阶段,所有买卖指令被收集但不成交。撮合引擎以最大化成交量的价格作为开盘价。这个机制意味着:

  • 价格优先:高于开盘价的买盘和低于开盘价的卖盘会在开盘瞬间被成交
  • 数量均衡点:订单簿中买卖力量的相对分布决定最终撮合位置
  • 流动性不对称:若买盘远大于卖盘,缺口(gap)向上概率极高

因此,盘前订单簿的买卖压力比(Bid-Ask Pressure Ratio)是预测开盘方向最直接的微观信号

买卖压力比 = Σ(买盘前N档挂单量) / Σ(卖盘前N档挂单量)
  • 压力比 > 1.5 → 买方力量主导,预计高开
  • 压力比 < 0.67 → 卖方力量主导,预计低开
  • 压力比在 0.67-1.5 → 多空僵持,开盘方向需等待前 5 分钟确认

4.2 盘前订单簿分析代码

import os
import time
import random
import requests
from collections import deque
from datetime import datetime
from typing import Optional

class PreMarketOrderBookAnalyzer:
    """
    盘前订单簿分析器
    使用 TickDB depth 频道实时分析盘前流动性,计算买卖压力比
    预测次日开盘方向和集合竞价撮合区间
    """

    def __init__(self, api_key: str):
        self.api_key = api_key
        self.headers = {"X-API-Key": api_key}
        self.base_url = "https://api.tickdb.ai/v1"
        # 滑动窗口:保留最近 N 个快照用于趋势分析
        self.window_size = 20
        self.pressure_history = deque(maxlen=self.window_size)

    def get_depth_snapshot(self, symbol: str, depth: int = 10) -> dict:
        """
        获取订单簿深度快照

        Args:
            symbol: 交易品种,如 "AAPL.US"
            depth: 档位数(美股 depth 频道通常为 1 档,港股/数字货币支持多档)

        Returns:
            含 bids/asks 的完整订单簿字典
        """
        response = requests.get(
            f"{self.base_url}/market/depth",
            headers=self.headers,
            params={"symbol": symbol, "depth": depth},
            timeout=(3.05, 10)
        )

        data = response.json()
        code = data.get("code", 0)

        if code == 3001:
            retry_after = int(response.headers.get("Retry-After", 5))
            print(f"[RATE] 请求频率超限,等待 {retry_after} 秒...")
            time.sleep(retry_after)
            return self.get_depth_snapshot(symbol, depth)  # 重试
        elif code != 0:
            raise RuntimeError(f"API 错误 {code}: {data.get('message')}")

        return data.get("data", {})

    def calculate_pressure_ratio(self, depth_data: dict,
                                   levels: int = 5) -> float:
        """
        计算买卖压力比

        Args:
            depth_data: get_depth_snapshot 返回的订单簿数据
            levels: 用于计算的档位数(前 N 档)

        Returns:
            买卖压力比(买盘量/卖盘量)
        """
        bids = depth_data.get("bids", [])
        asks = depth_data.get("asks", [])

        bid_volume = sum(float(b[1]) for b in bids[:levels])
        ask_volume = sum(float(a[1]) for a in asks[:levels])

        if ask_volume == 0:
            return float('inf')  # 极端情况:无卖盘

        return bid_volume / ask_volume

    def estimate_auction_price(self, depth_data: dict) -> dict:
        """
        估算集合竞价撮合价格区间

        原理:找到买卖盘交叉价格区间,即最大成交量对应的价格
        简化版本:使用盘口中间价 ± 价差作为参考区间
        """
        bids = depth_data.get("bids", [])
        asks = depth_data.get("asks", [])

        if not bids or not asks:
            return {"status": "no_data"}

        best_bid = float(bids[0][0])
        best_ask = float(asks[0][0])
        mid_price = (best_bid + best_ask) / 2
        spread = best_ask - best_bid

        # 估算撮合区间(简化模型)
        # 实际生产中需要遍历所有可能价格,计算每个价格下的成交量
        auction_lower = mid_price - spread * 0.5
        auction_upper = mid_price + spread * 0.5

        return {
            "best_bid": best_bid,
            "best_ask": best_ask,
            "mid_price": round(mid_price, 4),
            "spread": round(spread, 4),
            "spread_pct": round(spread / mid_price * 100, 4),
            "auction_lower": round(auction_lower, 4),
            "auction_upper": round(auction_upper, 4),
            "estimated_gap": self._estimate_gap_risk(spread, mid_price),
        }

    def _estimate_gap_risk(self, spread: float, mid_price: float) -> str:
        """基于盘口价差估算开盘缺口风险"""
        spread_pct = spread / mid_price * 100
        if spread_pct > 2.0:
            return "HIGH"  # 价差超过 2%,可能出现显著缺口
        elif spread_pct > 0.5:
            return "MEDIUM"
        else:
            return "LOW"

    def analyze_and_record(self, symbol: str) -> dict:
        """
        执行单次分析并记录到历史窗口
        """
        depth = self.get_depth_snapshot(symbol, depth=10)
        pressure = self.calculate_pressure_ratio(depth)
        auction = self.estimate_auction_price(depth)

        record = {
            "symbol": symbol,
            "timestamp": datetime.utcnow().isoformat(),
            "pressure_ratio": round(pressure, 3),
            "auction": auction,
            "direction": self._pressure_to_direction(pressure)
        }

        self.pressure_history.append(record)
        return record

    def _pressure_to_direction(self, pressure: float) -> str:
        if pressure > 1.5:
            return "bullish"
        elif pressure < 0.67:
            return "bearish"
        else:
            return "neutral"

    def get_trend_summary(self, symbol: str) -> dict:
        """
        基于滑动窗口趋势,给出方向信号摘要
        """
        if not self.pressure_history:
            return {"status": "no_history"}

        recent = [r for r in self.pressure_history if r["symbol"] == symbol]
        if len(recent) < 3:
            return {"status": "insufficient", "samples": len(recent)}

        ratios = [r["pressure_ratio"] for r in recent]
        directions = [r["direction"] for r in recent]

        import statistics
        mean_ratio = statistics.mean(ratios)

        # 趋势判断:最近 3 个样本的方向是否一致
        consistent = len(set(directions)) == 1
        latest = directions[-1]

        return {
            "symbol": symbol,
            "samples": len(recent),
            "mean_pressure_ratio": round(mean_ratio, 3),
            "latest_direction": latest,
            "is_consistent": consistent,
            "signal": "STRONG" if (consistent and abs(mean_ratio - 1.0) > 0.5) else "WEAK",
            "interpretation": self._interpret_signal(mean_ratio, consistent, latest)
        }

    def _interpret_signal(self, ratio: float, consistent: bool, direction: str) -> str:
        if ratio > 1.5 and consistent:
            return f"连续看多信号(压力比 {ratio:.2f}),预计高开,建议关注突破买入"
        elif ratio < 0.67 and consistent:
            return f"连续看空信号(压力比 {ratio:.2f}),预计低开,建议关注反弹卖出"
        elif abs(ratio - 1.0) < 0.33:
            return f"多空均衡(压力比 {ratio:.2f}),建议等待 09:30 后确认方向"
        else:
            return f"信号分歧,压力比 {ratio:.2f},建议轻仓观望"

    def run_premarket_monitoring(self, symbols: list, interval: int = 60,
                                  duration_minutes: int = 60):
        """
        盘前定时监控循环
        在 07:00-09:15 ET 期间持续采集数据,输出趋势报告
        ⚠️ 演示用:实际部署时应设置 cron 或 scheduler
        """
        print(f"[INFO] 启动盘前监控,目标品种: {symbols}")
        print(f"[INFO] 采集间隔 {interval}s,预计运行 {duration_minutes} 分钟\n")

        start_time = time.time()
        iterations = (duration_minutes * 60) // interval

        for i in range(iterations):
            elapsed = (time.time() - start_time) / 60
            print(f"--- 第 {i + 1}/{iterations} 次采集 ({elapsed:.1f} 分钟) ---")

            for symbol in symbols:
                try:
                    record = self.analyze_and_record(symbol)
                    trend = self.get_trend_summary(symbol)

                    if trend.get("status") == "ok":
                        print(f"  {symbol}: 压力比={record['pressure_ratio']:.2f}, "
                              f"方向={record['direction']}, "
                              f"信号强度={trend['signal']}")
                except Exception as e:
                    print(f"  [ERROR] {symbol}: {e}")

            time.sleep(interval)


# ========== 使用示例 ==========
if __name__ == "__main__":
    api_key = os.environ.get("TICKDB_API_KEY")
    analyzer = PreMarketOrderBookAnalyzer(api_key=api_key)

    # 盘前监控:关注 3 只关键品种,采集 30 分钟
    symbols_to_watch = ["AAPL.US", "TSLA.US", "NVDA.US"]

    # 方式一:单次快照分析
    print("=== 盘前订单簿快照分析 ===")
    for symbol in symbols_to_watch:
        try:
            record = analyzer.analyze_and_record(symbol)
            print(f"\n{symbol}:")
            print(f"  买卖压力比: {record['pressure_ratio']:.3f} ({record['direction']})")
            print(f"  撮合区间: [{record['auction']['auction_lower']:.2f}, "
                  f"{record['auction']['auction_upper']:.2f}]")
            print(f"  缺口风险: {record['auction']['estimated_gap']}")
        except Exception as e:
            print(f"  [ERROR] {symbol}: {e}")

    # 方式二:定时监控(演示:仅运行 2 次)
    analyzer.run_premarket_monitoring(
        symbols=symbols_to_watch,
        interval=5,  # ⚠️ 生产环境建议 60-300 秒,避免频率超限
        duration_minutes=1
    )

工程预警

  • 美股 depth 频道仅支持 1 档,港股/数字货币支持 10 档。此代码在美股品种上实际只能分析 bids[0]asks[0]levels 参数对美股品种无效
  • 买卖压力比在单档数据下参考价值有限,建议将 trades 频道(港股/数字货币可用)配合 depth 一起分析,以获得多档完整的压力结构
  • run_premarket_monitoring 中的 interval=5 仅为演示设置,TickDB 对 REST API 有频率限制(错误码 3001),生产环境应使用 WebSocket 实时推送并设置合理采集间隔

五、隔夜信号合成:预计算输出的完整框架

5.1 单一信号无效,信号矩阵才有效

上文拆解了四类信号源:事件扫描、期货夜盘、盘前流动性、集合竞价。但真正的盘前策略准备不是只看某一个信号,而是将多个信号合成为一个综合置信度评分

from dataclasses import dataclass
from typing import Optional
import statistics

@dataclass
class CompositeSignal:
    """合成信号容器"""
    symbol: str
    event_score: float       # 事件驱动评分 [-1, 1],正为利好
    futures_score: float     # 期货方向评分 [-1, 1]
    orderbook_score: float   # 盘前压力评分 [-1, 1]
    confidence: float        # 综合置信度 [0, 1]

    @property
    def weighted_signal(self) -> float:
        """
        加权综合信号

        权重分配逻辑:
        - 事件扫描(明确事件):权重最高,因为定价逻辑清晰
        - 期货夜盘(宏观方向):权重次之,作为市场情绪验证
        - 盘前订单簿(微观结构):权重灵活,作为入场时机参考
        """
        weights = {
            "event": 0.45,
            "futures": 0.30,
            "orderbook": 0.25
        }
        return (
            self.event_score * weights["event"] +
            self.futures_score * weights["futures"] +
            self.orderbook_score * weights["orderbook"]
        )

    @property
    def action_recommendation(self) -> str:
        """行动建议(仅供系统内部决策参考,不构成投资建议)"""
        ws = self.weighted_signal
        conf = self.confidence

        if conf < 0.3:
            return "信号不足,保持观望"
        elif ws > 0.4 and conf > 0.5:
            return "多头信号,建议关注突破买入"
        elif ws < -0.4 and conf > 0.5:
            return "空头信号,建议关注反弹做空"
        elif abs(ws) < 0.2:
            return "多空均衡,趋势不明"
        else:
            return "信号存在但不明确,轻仓尝试"


class OvernightSignalSynthesizer:
    """
    隔夜信号合成器
    将事件扫描、期货监控、订单簿分析的结果综合为单一信号输出
    输出可直接对接下单系统的触发条件
    """

    def __init__(self, event_scanner, futures_monitor, orderbook_analyzer):
        self.event_scanner = event_scanner
        self.futures_monitor = futures_monitor
        self.orderbook_analyzer = orderbook_analyzer

    def _normalize_event_score(self, event_estimate: dict) -> float:
        """将事件隐含波动映射为 [-1, 1] 评分"""
        move_pct = event_estimate.get("implied_move_pct", 0) / 100
        # 简单映射:±5% 波动映射到 ±1
        return max(-1.0, min(1.0, move_pct / 0.05))

    def _normalize_futures_score(self, futures_bias: dict) -> float:
        """将期货偏离均值映射为 [-1, 1] 评分"""
        move_pct = futures_bias.get("move_from_mean_pct", 0) / 100
        return max(-1.0, min(1.0, move_pct / 0.01))  # ±1% 偏离映射到 ±1

    def _normalize_orderbook_score(self, pressure_ratio: float) -> float:
        """将买卖压力比映射为 [-1, 1] 评分"""
        # 压力比 1.0 → 0,>2 → 1,<0.5 → -1
        if pressure_ratio >= 2.0:
            return 1.0
        elif pressure_ratio <= 0.5:
            return -1.0
        else:
            # 线性映射:[0.5, 2.0] → [-1, 1]
            return (pressure_ratio - 0.5) / 1.5 * 2 - 1

    def synthesize(self, symbol: str) -> CompositeSignal:
        """合成单一标的的隔夜信号"""
        # 1. 事件评分
        event_data = self._fetch_event_data(symbol)
        event_score = self._normalize_event_score(event_data)

        # 2. 期货评分
        futures_data = self.futures_monitor.calculate_premarket_bias("ES")
        futures_score = self._normalize_futures_score(futures_data)

        # 3. 盘前订单簿评分
        orderbook_record = self.orderbook_analyzer.analyze_and_record(symbol)
        pressure = orderbook_record.get("pressure_ratio", 1.0)
        orderbook_score = self._normalize_orderbook_score(pressure)

        # 4. 置信度评估:可用信号越多,置信度越高
        signal_count = sum([
            event_data.get("status") != "no_data",
            futures_data.get("samples", 0) >= 5,
            orderbook_record.get("pressure_ratio") is not None
        ])
        confidence = signal_count / 3

        return CompositeSignal(
            symbol=symbol,
            event_score=round(event_score, 3),
            futures_score=round(futures_score, 3),
            orderbook_score=round(orderbook_score, 3),
            confidence=round(confidence, 2)
        )

    def generate_morning_brief(self, watchlist: list) -> str:
        """生成每日盘前简报(可对接飞书/Slack 推送)"""
        lines = [
            "\n" + "=" * 50,
            f"📊 盘前信号简报 | {datetime.utcnow().strftime('%Y-%m-%d %H:%M UTC')}",
            "=" * 50
        ]

        signals = []
        for symbol in watchlist:
            try:
                sig = self.synthesize(symbol)
                signals.append(sig)

                ws = sig.weighted_signal
                emoji_map = {
                    "多头信号": "🟢",
                    "空头信号": "🔴",
                    "多空均衡": "⚪️",
                    "信号不足": "⚫"
                }
                emoji = emoji_map.get(sig.action_recommendation.split(",")[0], "⚪️")

                lines.append(
                    f"\n{emoji} {symbol} "
                    f"[置信度 {sig.confidence:.0%}]"
                )
                lines.append(
                    f"   综合信号: {ws:+.3f} → {sig.action_recommendation}"
                )
                lines.append(
                    f"   分项: 事件 {sig.event_score:+.2f} | "
                    f"期货 {sig.futures_score:+.2f} | "
                    f"盘口 {sig.orderbook_score:+.2f}"
                )
            except Exception as e:
                lines.append(f"\n⚠️ {symbol}: {e}")

        lines.append("\n" + "=" * 50)
        lines.append("⚠️ 本简报仅供系统决策参考,不构成任何投资建议。")
        lines.append("=" * 50)

        return "\n".join(lines)

    def _fetch_event_data(self, symbol: str) -> dict:
        """获取事件数据(占位实现)"""
        # 实际场景中调用 event_scanner
        return {"status": "no_data", "implied_move_pct": 0}

5.2 置信度决定了仓位管理

有了合成信号,下一步是根据置信度决定仓位:

置信度 信号强度 建议仓位
≥ 0.7 STRONG 正常仓位(如 10% 基准)
0.4-0.7 MODERATE 半仓
< 0.4 WEAK 观望或仅做日内迷你仓

关键原则:置信度不足时,不交易随便交易更好。盘前预计算的价值不仅在于"发现机会",更在于避免冲动交易


六、完整方案:盘前准备清单与部署建议

6.1 从信号计算到执行的分层架构

┌─────────────────────────────────────────┐
│           Layer 4: 执行层                │
│   预设条件单 + 触发阈值 + 仓位计算        │
├─────────────────────────────────────────┤
│           Layer 3: 信号合成层            │
│   CompositeSignal + 置信度 + 行动建议    │
├─────────────────────────────────────────┤
│           Layer 2: 数据采集层             │
│   事件扫描 + 期货监控 + 订单簿分析        │
├─────────────────────────────────────────┤
│           Layer 1: 数据源                │
│   TickDB (期货深度/港股/数字货币)         │
│   第三方 (美股新闻/财报/期权)            │
└─────────────────────────────────────────┘

6.2 分场景部署方案

场景 推荐配置 TickDB 能力覆盖
个人量化爱好者 REST API + WebSocket,Python 单进程 港股/数字货币 depth + 期货深度
小型团队(3-5 人) WebSocket + Redis 队列 + 飞书告警 同上,扩展多账号并发
机构级 多路数据源(TickDB + Bloomberg + 自研) 期货 + 数字货币主力合约

6.3 盘前 30 分钟行动清单

以下清单在 T日 09:00-09:30 ET 执行:

□ 09:00  运行 OvernightSignalSynthesizer,生成 watchlist 简报
□ 09:05  对比盘前订单簿快照与昨日收盘价,确认缺口方向
□ 09:10  检查宏观数据日历(今日有无 CPI / Fed 讲话 / 财报)
□ 09:15  集合竞价开始,盯住盘口压力比变化
□ 09:20  若压力比趋势与昨日预判一致 → 准备入场
□ 09:25  设置止损单,确认流动性充足(Bid/Ask 档位均有挂单)
□ 09:28  最终确认:成交量是否配合?避免低流动性时段追单
□ 09:30  开盘,执行预设计划,无论结果如何记录日志
□ 09:45  盘后复盘:预判 vs 实际偏差,更新信号权重模型

七、技术总结:盘前准备的本质是"不对称信息优势"

复盘全文的核心逻辑:

盘前预计算解决的不是"明天涨还是跌"的问题,而是:

  1. 信号冗余:单一信号(期货 or 盘前盘口)不足以决策,多信号交叉验证才能提升胜率
  2. 时间压缩:把需要 30 分钟分析的工作在盘前 15 分钟内完成,留足执行窗口
  3. 情绪隔离:预设计算在盘后完成,不受盘中情绪干扰;开盘时只需"确认 vs 预判一致"或"放弃"

工程上,本文给出了三个生产级模块:事件扫描器期货信号监控订单簿分析器。每个模块均可独立运行,也可通过 OvernightSignalSynthesizer 组合为完整的盘前信号流水线。


下一步行动

如果你是个人量化开发者,想亲手实现本文的盘前监控系统:

  1. 访问 tickdb.ai 注册(免费,无需信用卡)
  2. 在控制台生成 API Key
  3. 设置环境变量 TICKDB_API_KEY,参考本文代码即可运行

如果你的策略需要港股或数字货币的盘前深度数据,TickDB 的 depth 频道在盘前阶段(04:00-09:15 ET)有实时推送,结合本文的买卖压力比算法可提前预判开盘方向。

如果你习惯用 AI 辅助开发,在 ClawHub 安装 tickdb-market-data SKILL,可以直接用自然语言查询盘前流动性数据并生成监控代码。

如果你需要机构级的历史数据回测支持(包括港股/数字货币的历史 depth 数据用于策略验证),联系 [email protected] 了解专业版方案。


风险提示:本文不构成任何投资建议。量化策略存在模型风险、历史数据不代表未来收益。盘前信号基于有限信息和简化模型,实际开盘结果受多种不可预测因素影响。使用本文代码前请充分测试,注意数据源限制和 API 频率约束。市场有风险,投资需谨慎。