实时市场数据分析产品选型|RisingWave x Databento

139 阅读6分钟

本文将分享我们是如何结合 Databento 和 RisingWave 这两个工具构建了一个稳定且功能强大的实时市场数据分析系统。

1. 背景介绍

在当今的金融市场中,实时市场分析对于成功至关重要。但是,构建一个能够处理大量、多样且快速变化的市场数据的系统非常困难。

  • 第一个难点在于获取和集成来自多个交易所的高质量实时和历史数据。
  • 第二个难点在于对来自多个数据源的大量实时数据进行低延迟处理。

本教程将介绍如何使用 Databento 和 RisingWave 创建一个强大的实时市场分析系统。

  • Databento 简化了数据源获取,通过统一的 API 提供来自各个交易所的标准化数据。
  • RisingWave 负责实时处理的繁重工作,使你能够以最小的延迟获得 VWAP 等重要指标。

结合使用这两个工具,可以帮助你更快、更明智地做出决策。

2. 平台介绍

databento 介绍

RisingWave 介绍

3. 详细步骤

开始之前需要准备以下内容:

  1. Databento 账户,并具备实时数据的许可。许可过程相对简单,非专业用户也可以轻松获得,详情请参阅 Databento 的许可指南
  2. 正在运行的 RisingWave 实例。最简单的方法是创建一个免费的 RisingWave Cloud 账户,几分钟内即可完成,也可以安装开源版本并在本地启动,详情请参见快速入门指南
  3. Python 3.7 及以上版本。

接下来,安装所需的 Python 包:

pip3 install databento
pip3 install risingwave-py

3.1 从 Databento 获取市场数据

E-mini S&P 500 期货数据是最具流动性的期货合约之一,颇具分析价值。以此为例,获取实时数据的方法:

import databento as db
   
client = db.Live()
client.subscribe(
    dataset="GLBX.MDP3",
    schema="trades",
    stype_in="parent",
    symbols="ES.FUT",
)

# 打印接收到的数据进行测试
for record in client:
    print(record)

3.2 设置 RisingWave

接下来连接到 RisingWave。以下参数用于连接本地 RisingWave 实例。如果是云用户,可以导航到集群并点击集群卡片上的 Connect 来获取 Connection string。

from risingwave import RisingWave, RisingWaveConnOptions

rw = RisingWave(
    RisingWaveConnOptions.from_connection_info(
        host="localhost", port=4566, user="root", password="root", database="dev"
    )
)

为了存储来自 Databento 的数据需要在 RisingWave 中创建一个具有正确 Schema 的表。

with rw.getconn() as conn:
    conn.execute("""
        CREATE TABLE IF NOT EXISTS es_futures_live (
            timestamp TIMESTAMPTZ,
            symbol VARCHAR,
            price NUMERIC,
            size BIGINT
        );
    """)

我们可以为每个需要实时结果的指标定义物化视图。RisingWave 中的物化视图将持续处理数据,随着新数据记录的到来,实时更新。

with rw.getconn() as conn:
    conn.execute("""
        CREATE MATERIALIZED VIEW vwap_analysis_live AS
        SELECT
            window_start,
            SUM(price * size) / SUM(size) as vwap,
            AVG(price) as simple_avg_price,
            SUM(size) as total_volume,
            COUNT(*) as trade_count
        FROM TUMBLE(es_futures_live, timestamp, INTERVAL '5 SECONDS')
        GROUP BY window_start;
    """)

这个物化视图每 5 秒计算一次 VWAP(加权平均成交价格)——这对于衡量交易执行质量至关重要。机构交易员通常会尝试匹配或超过 VWAP,以证明他们的交易价格较好。

3.3 处理数据记录

下一步,定义函数,在数据到达时立即格式化数据记录并将其插入到 RisingWave 中。

async def handle_trade(record):
    with rw.getconn() as conn:
        timestamp = datetime.fromtimestamp(record.ts_event / 1e9)
        params = {
            "timestamp": timestamp,
            "symbol": record.symbol,
            "price": float(record.price),
            "size": int(record.size)
        }
        conn.execute("""
            INSERT INTO es_futures_live
            (timestamp, symbol, price, size)
            VALUES (:timestamp, :symbol, :price, :size)
        """, params)

3.4 订阅数据变化

物化视图包含基于时间窗口的值,也许有用户希望订阅这一变化,让应用程序能够响应事件。为此,我们需要先定义一个变更事件处理器 (Event handler),然后使用该处理器来订阅物化视图的变化。

# 物化视图变化的事件处理程序
def handle_vwap_changes(event_df: pd.DataFrame) -> None:
    # 仅包括更新操作
    event_df = event_df[event_df["op"].isin(["Insert", "UpdateInsert"])]
    if event_df.empty:
        return

    # 格式化数据框以供打印
    event_df = event_df.rename(
        {
            "window_start": "Timestamp",
            "symbol": "Symbol",
            "vwap": "VWAP",
            "simple_avg_price": "Avg Price",
            "total_volume": "Volume",
            "trade_count": "Trades",
        },
        axis=1,
    )
    event_df = event_df.drop(["op", "rw_timestamp"], axis=1)
    event_df = event_df.set_index(["Timestamp", "Symbol"])

    print()
    print("VWAP分析更新:")
    print(event_df)
    
    # 订阅物化视图的变化
threading.Thread(
    target=lambda: rw.on_change(
        subscribe_from="vwap_analysis_live",
        handler=handle_vwap_changes,
        output_format=OutputFormat.DATAFRAME,
        persist_progress=False,
        max_batch_size=10
    )
).start()

3.5 查看最终结果

现在让我们将所有内容组合起来,看看最终结果。

from risingwave import RisingWave, RisingWaveConnOptions, OutputFormat
import databento as db
import pandas as pd
import threading

# 设置数据库函数
def setup_database(rw: RisingWave) -> None:
    with rw.getconn() as conn:
        conn.execute(
            """
            CREATE TABLE IF NOT EXISTS es_futures_live (
                timestamp TIMESTAMP,
                symbol VARCHAR,
                price DOUBLE PRECISION,
                size BIGINT
            )"""
        )

        conn.execute(
            """
            CREATE MATERIALIZED VIEW IF NOT EXISTS vwap_analysis_live AS
            SELECT
                window_start,
                symbol,
                SUM(price * size) / SUM(size) as vwap,
                AVG(price) as simple_avg_price,
                SUM(size) as total_volume,
                COUNT(*) as trade_count
            FROM TUMBLE(es_futures_live, timestamp, INTERVAL '5 SECONDS')
            GROUP BY window_start, symbol;"""
        )

# 物化视图变化的事件处理程序
def handle_vwap_changes(event_df: pd.DataFrame) -> None:
    # 仅包括更新操作
    event_df = event_df[event_df["op"].isin(["Insert", "UpdateInsert"])]
    if event_df.empty:
        return

    # 格式化数据框以供打印
    event_df = event_df.rename(
        {
            "window_start": "Timestamp",
            "symbol": "Symbol",
            "vwap": "VWAP",
            "simple_avg_price": "Avg Price",
            "total_volume": "Volume",
            "trade_count": "Trades",
        },
        axis=1,
    )
    event_df = event_df.drop(["op", "rw_timestamp"], axis=1)
    event_df = event_df.set_index(["Timestamp", "Symbol"])

    print()
    print("VWAP分析更新:")
    print(event_df)

def main() -> None:
    # 初始化RisingWave连接
    rw = RisingWave(
        RisingWaveConnOptions.from_connection_info(
            host="localhost", port=4566, user="root", password="root", database="dev"
        )
    )
    setup_database(rw)

    # 订阅物化视图的变化
    threading.Thread(
        target=lambda: rw.on_change(
            subscribe_from="vwap_analysis_live",
            handler=handle_vwap_changes,
            output_format=OutputFormat.DATAFRAME,
            persist_progress=False,
            max_batch_size=10,
        )
    ).start()

    # 通过 Databento 订阅CME 数据
    db.enable_logging()
    client = db.Live()
    client.subscribe(
        dataset="GLBX.MDP3",
        schema="trades",
        stype_in="parent",
        symbols="ES.FUT",
    )

    # 将交易数据发送到 RisingWave
    with rw.getconn() as conn:
        for record in client:
            # 只处理交易记录
            if not isinstance(record, db.TradeMsg):
                continue

            # 获取可读的符号名称
            symbol = client.symbology_map.get(record.instrument_id)
            if symbol is None:
                continue

            params = {
                "timestamp": record.pretty_ts_recv,
                "symbol": symbol,
                "price": record.pretty_price,
                "size": record.size,
            }
            conn.execute(
                """
                INSERT INTO es_futures_live
                (timestamp, symbol, price, size)
                VALUES (:timestamp, :symbol, :price, :size)""",
                params,
            )

if __name__ == "__main__":
    main()

3.6 动态输出示例

以下是来自实时 VWAP 分析的动态输出示例,随着新市场数据的到来,数据会持续更新。

TimestampSymbolVWAPAvg PriceVolumeTrades
2024-12-13 20:03:30ESZ4-ESH569.325969.325029744
2024-12-13 20:03:35ESZ46057.8756057.8752222
2024-12-13 20:03:35ESZ4-ESH569.331369.3200126555
2024-12-13 20:03:40ESH56127.5876127.5881199407
2024-12-13 20:03:40ESZ46058.0766058.03438391045

4. 延伸拓展

现在你的系统中已经有了实时市场数据,欢迎大家结合实际情况进一步自主探索。

  • 添加更复杂的分析,比如订单流失衡;
  • 基于 VWAP 交叉点创建交易信号;
  • 构建一个仪表盘来可视化你的指标。

5. 关于 RisingWave

RisingWave 是一款开源的分布式流处理数据库,旨在帮助用户降低实时应用的开发成本。RisingWave 采用存算分离架构,提供 Postgres-style 使用体验,具备比 Flink 高出 10 倍的性能以及更低的成本。

👨‍🔬加入 RW 社区,欢迎关注公众号:RisingWave中文开源社区

🧑‍💻想要了解和探索 RisingWave,欢迎浏览我们的官网:risingwave.com/

🔧快速上手 RisingWave,欢迎体验入门教程:github.com/risingwave

💻深入理解使用 RisingWave,欢迎阅读用户文档:zh-cn.risingwave.com/docs