期货API数据与脉动数据行情对接指南

8 阅读10分钟

在量化交易和金融应用开发领域,稳定、实时的行情数据是系统运行的基石。脉动数据作为专业的行情服务提供商,为开发者提供了完整的WebSocket和HTTP接口解决方案。本文将详细介绍脉动数据API的特性,并通过实战代码演示如何快速对接全球期货市场数据。

一、脉动数据平台概述

脉动数据专注于提供全球金融市场的实时行情数据服务,涵盖外汇、国际期货、国内期货、数字货币、国际股指期货等多个品种。其API接口具有以下核心特性:

  • 数据时效性:WebSocket实时推送,行情更新即推送
  • 接入方式:同时支持WebSocket实时推送和HTTP REST接口
  • 数据格式:统一的JSON格式,便于解析和处理
  • 授权机制:服务器IP白名单授权,确保数据安全

1.1 接入前准备

  1. 官网:http://39.107.99.235:1008/market

二、API接口体系

脉动数据提供五大类接口,满足不同的数据需求:

接口类型功能描述适用场景
WebSocket实时推送实时分笔明细数据推送高频交易、实时监控
获取实时数据接口HTTP方式获取实时行情中低频查询、应用集成
获取K线图接口历史K线数据查询技术分析、回测
查询产品分类接口获取产品分类ID数据浏览、分类查询
查询产品订阅代码获取产品代码列表订阅准备、代码映射

三、WebSocket实时数据推送实战

WebSocket接口适合需要实时监控行情的高频交易场景

3.1 WebSocket连接与心跳维持

import asyncio
import websockets
import json
import time

class PulseDataWebSocket:
    def __init__(self, uri="ws://39.107.99.235/ws"):
        self.uri = uri
        self.websocket = None
        self.subscribed_symbols = []
        self.running = False
    
    async def connect(self):
        """建立WebSocket连接"""
        try:
            self.websocket = await websockets.connect(self.uri)
            self.running = True
            print(f"WebSocket连接成功: {self.uri}")
            
            # 启动心跳任务
            asyncio.create_task(self.heartbeat())
            
            # 如果有之前订阅的代码,重新订阅
            if self.subscribed_symbols:
                await self.subscribe(self.subscribed_symbols)
                
            # 开始接收消息
            await self.receive_messages()
            
        except Exception as e:
            print(f"连接失败: {e}")
            await self.reconnect()
    
    async def heartbeat(self):
        """每10秒发送心跳"""
        while self.running:
            if self.websocket:
                try:
                    ping_msg = {"ping": int(time.time())}
                    await self.websocket.send(json.dumps(ping_msg))
                    print(f"发送心跳: {ping_msg}")
                    await asyncio.sleep(10)
                except Exception as e:
                    print(f"心跳发送失败: {e}")
                    break
    
    async def subscribe(self, symbols):
        """订阅产品代码"""
        self.subscribed_symbols = symbols
        if self.websocket:
            subscribe_msg = {"Key": ",".join(symbols)}
            await self.websocket.send(json.dumps(subscribe_msg))
            print(f"订阅产品: {subscribe_msg}")
    
    async def receive_messages(self):
        """接收服务器推送的消息"""
        while self.running:
            try:
                message = await self.websocket.recv()
                data = json.loads(message)
                
                # 处理pong响应
                if "pong" in data:
                    print(f"收到心跳响应: {data}")
                else:
                    # 处理行情数据
                    self.process_market_data(data)
                    
            except websockets.exceptions.ConnectionClosed:
                print("连接关闭,尝试重连...")
                await self.reconnect()
                break
            except Exception as e:
                print(f"接收消息错误: {e}")
    
    def process_market_data(self, data):
        """处理行情数据"""
        if "body" in data:
            body = data["body"]
            print(f"""
            产品代码: {body.get('StockCode')}
            最新价: {body.get('Price')}
            开盘价: {body.get('Open')}
            最高价: {body.get('High')}
            最低价: {body.get('Low')}
            时间: {body.get('Time')}
            """)
            
            # 处理深度数据
            if "Depth" in body:
                depth = body["Depth"]
                if "Buy" in depth and depth["Buy"]:
                    print(f"买一价: {depth['Buy'][0].get('BP1')}, 买一量: {depth['Buy'][0].get('BV1')}")
                if "Sell" in depth and depth["Sell"]:
                    print(f"卖一价: {depth['Sell'][0].get('SP1')}, 卖一量: {depth['Sell'][0].get('SV1')}")
    
    async def reconnect(self):
        """断线重连机制"""
        self.running = False
        if self.websocket:
            await self.websocket.close()
        
        # 指数退避重连
        retry_count = 0
        while True:
            retry_count += 1
            wait_time = min(30, 2 ** retry_count)
            print(f"{wait_time}秒后尝试第{retry_count}次重连...")
            await asyncio.sleep(wait_time)
            
            try:
                await self.connect()
                print("重连成功")
                break
            except Exception as e:
                print(f"重连失败: {e}")
    
    async def close(self):
        """关闭连接"""
        self.running = False
        if self.websocket:
            await self.websocket.close()

# 使用示例
async def main():
    client = PulseDataWebSocket()
    
    # 连接并订阅产品
    await client.connect()
    await client.subscribe(["btcusdt", "ethusdt"])
    
    # 运行60秒后关闭
    await asyncio.sleep(60)
    await client.close()

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

四、HTTP实时数据接口实战

对于不需要实时推送的应用场景,可以使用HTTP接口获取实时行情。需要注意请求频率限制:每个产品每秒最大支持请求3次。

4.1 获取实时行情

import requests
import gzip
import json
from io import BytesIO
import time
from functools import lru_cache

class PulseDataHTTP:
    def __init__(self, base_url="http://39.107.99.235:1008"):
        self.base_url = base_url
        self.session = requests.Session()
        # 启用gzip压缩
        self.session.headers.update({
            'Accept-Encoding': 'gzip',
            'User-Agent': 'Mozilla/5.0 (compatible; PulseDataClient/1.0)'
        })
    
    def _handle_response(self, response):
        """处理gzip压缩的响应"""
        if response.headers.get('Content-Encoding') == 'gzip':
            buf = BytesIO(response.content)
            with gzip.GzipFile(fileobj=buf) as f:
                return json.loads(f.read().decode('utf-8'))
        return response.json()
    
    def get_quote(self, code):
        """
        获取实时行情
        :param code: 产品代码,如 btcusdt
        :return: 行情数据
        """
        url = f"{self.base_url}/getQuote.php"
        params = {'code': code}
        
        try:
            response = self.session.get(url, params=params, timeout=10)
            response.raise_for_status()
            
            data = self._handle_response(response)
            
            if data.get('code') == 200:
                return data.get('data', {})
            else:
                print(f"API错误: {data.get('msg')}")
                return None
                
        except requests.exceptions.RequestException as e:
            print(f"请求失败: {e}")
            return None
    
    @lru_cache(maxsize=128)
    def get_cached_quote(self, code, cache_seconds=1):
        """
        带缓存的行情查询,减少API调用
        :param code: 产品代码
        :param cache_seconds: 缓存时间(秒)
        """
        # 通过lru_cache实现简单缓存,实际应用建议使用Redis等
        return self.get_quote(code)
    
    def parse_quote_data(self, data):
        """解析行情数据"""
        if not data or 'body' not in data:
            return None
        
        body = data['body']
        
        quote_info = {
            'code': body.get('StockCode'),
            'price': body.get('Price'),
            'open': body.get('Open'),
            'high': body.get('High'),
            'low': body.get('Low'),
            'last_close': body.get('LastClose'),
            'time': body.get('Time'),
            'timestamp': body.get('LastTime'),
            'volume': body.get('TotalVol'),
            'diff': data.get('Diff'),
            'diff_rate': data.get('DiffRate')
        }
        
        # 解析盘口数据
        if 'Depth' in body:
            depth = body['Depth']
            quote_info['bid'] = depth.get('Buy', [])
            quote_info['ask'] = depth.get('Sell', [])
        
        # 解析实时成交
        if 'BS' in body:
            quote_info['trades'] = body.get('BS', [])
        
        return quote_info

# 使用示例
def demo_http_api():
    client = PulseDataHTTP()
    
    # 查询BTCUSDT行情
    data = client.get_quote('btcusdt')
    if data:
        quote = client.parse_quote_data(data)
        if quote:
            print(f"""
            === {quote['code']} 实时行情 ===
            最新价: {quote['price']}
            开盘价: {quote['open']}
            最高价: {quote['high']}
            最低价: {quote['low']}
            成交量: {quote['volume']}
            涨跌额: {quote['diff']}
            涨跌幅: {quote['diff_rate']}%
            更新时间: {quote['time']}
            """)
            
            # 打印盘口
            if quote.get('bid'):
                print("\n买盘:")
                for i, bid in enumerate(quote['bid'][:3]):
                    print(f"买{i+1}: {bid.get(f'BP{i+1}')} @ {bid.get(f'BV{i+1}')}")
            
            if quote.get('ask'):
                print("\n卖盘:")
                for i, ask in enumerate(quote['ask'][:3]):
                    print(f"卖{i+1}: {ask.get(f'SP{i+1}')} @ {ask.get(f'SV{i+1}')}")

if __name__ == "__main__":
    demo_http_api()

五、K线数据获取与处理

K线数据是技术分析的基础,脉动数据支持多种时间周期的K线查询。

5.1 K线数据接口封装

import pandas as pd
import matplotlib.pyplot as plt
from datetime import datetime

class PulseDataKLine(PulseDataHTTP):
    """K线数据查询类,继承基础HTTP客户端"""
    
    # 时间周期映射
    TIME_FRAMES = {
        '1m': '1分钟',
        '5m': '5分钟',
        '15m': '15分钟',
        '30m': '30分钟',
        '1h': '1小时',
        '1d': '日线',
        '1M': '月线'
    }
    
    # 最大条数限制
    MAX_ROWS = {
        '1m': 600,
        '5m': 300,
        '15m': 300,
        '30m': 300,
        '1h': 300,
        '1d': 300,
        '1M': 100
    }
    
    def get_kline(self, code, timeframe='1m', rows=100):
        """
        获取K线数据
        :param code: 产品代码
        :param timeframe: 时间周期,支持 1m,5m,15m,30m,1h,1d,1M
        :param rows: 获取条数,不能超过对应周期的最大限制
        :return: K线数据列表
        """
        # 参数验证
        if timeframe not in self.TIME_FRAMES:
            raise ValueError(f"不支持的时间周期: {timeframe},支持: {list(self.TIME_FRAMES.keys())}")
        
        max_rows = self.MAX_ROWS.get(timeframe, 100)
        if rows > max_rows:
            print(f"警告: {timeframe}周期最大支持{max_rows}条数据,将使用{max_rows}")
            rows = max_rows
        
        url = f"{self.base_url}/redis.php"
        params = {
            'code': code,
            'time': timeframe,
            'rows': rows
        }
        
        try:
            response = self.session.get(url, params=params, timeout=10)
            response.raise_for_status()
            
            data = self._handle_response(response)
            
            if isinstance(data, list):
                return self._parse_kline_data(data)
            else:
                print(f"API返回异常: {data}")
                return None
                
        except Exception as e:
            print(f"K线查询失败: {e}")
            return None
    
    def _parse_kline_data(self, raw_data):
        """解析K线原始数据"""
        klines = []
        for item in raw_data:
            if len(item) >= 7:
                kline = {
                    'timestamp': item[0],  # 毫秒时间戳
                    'open': float(item[1]),
                    'high': float(item[2]),
                    'low': float(item[3]),
                    'close': float(item[4]),
                    'time_str': item[5],    # 格式化时间字符串
                    'volume': float(item[6]) if item[6] else 0  # 成交量
                }
                klines.append(kline)
        return klines
    
    def kline_to_dataframe(self, klines):
        """将K线数据转换为Pandas DataFrame"""
        if not klines:
            return None
        
        df = pd.DataFrame(klines)
        df['datetime'] = pd.to_datetime(df['timestamp'], unit='ms')
        df.set_index('datetime', inplace=True)
        
        # 计算常用技术指标
        df['ma5'] = df['close'].rolling(window=5).mean()
        df['ma10'] = df['close'].rolling(window=10).mean()
        df['ma20'] = df['close'].rolling(window=20).mean()
        
        return df
    
    def plot_kline(self, df, title=None):
        """绘制K线图(简化版,仅显示收盘价)"""
        if df is None or df.empty:
            return
        
        plt.figure(figsize=(12, 6))
        
        # 绘制收盘价
        plt.subplot(2, 1, 1)
        plt.plot(df.index, df['close'], label='Close', color='black')
        plt.plot(df.index, df['ma5'], label='MA5', color='blue', alpha=0.7)
        plt.plot(df.index, df['ma10'], label='MA10', color='orange', alpha=0.7)
        plt.plot(df.index, df['ma20'], label='MA20', color='red', alpha=0.7)
        plt.title(title or 'K线图')
        plt.legend()
        plt.grid(True, alpha=0.3)
        
        # 绘制成交量
        plt.subplot(2, 1, 2)
        plt.bar(df.index, df['volume'], color='gray', alpha=0.5)
        plt.title('成交量')
        plt.grid(True, alpha=0.3)
        
        plt.tight_layout()
        plt.show()

# 使用示例
def demo_kline():
    client = PulseDataKLine()
    
    # 获取BTCUSDT 15分钟K线,最近50条
    klines = client.get_kline('btcusdt', timeframe='15m', rows=50)
    
    if klines:
        # 转换为DataFrame
        df = client.kline_to_dataframe(klines)
        print(f"获取到 {len(df)} 条K线数据")
        print(df[['open', 'high', 'low', 'close', 'volume']].tail())
        
        # 绘制K线图
        client.plot_kline(df, title='BTCUSDT 15分钟K线')
        
        # 简单分析
        latest = df.iloc[-1]
        print(f"""
        最新K线:
        时间: {latest.name}
        开盘: {latest['open']}
        最高: {latest['high']}
        最低: {latest['low']}
        收盘: {latest['close']}
        成交量: {latest['volume']}
        涨跌幅: {(latest['close'] - df.iloc[-2]['close']) / df.iloc[-2]['close'] * 100:.2f}%
        """)

if __name__ == "__main__":
    demo_kline()

六、产品分类与代码查询

在实际应用中,需要先查询产品分类和订阅代码。

6.1 产品分类查询

class PulseDataSymbol(PulseDataHTTP):
    """产品代码查询类"""
    
    def get_categories(self):
        """获取产品分类"""
        url = f"{self.base_url}/getCategory.php"
        
        try:
            response = self.session.get(url, timeout=10)
            response.raise_for_status()
            
            data = self._handle_response(response)
            
            if data.get('code') == 200:
                return data.get('data', {}).get('list', [])
            else:
                print(f"获取分类失败: {data.get('msg')}")
                return []
                
        except Exception as e:
            print(f"分类查询异常: {e}")
            return []
    
    def get_symbols(self, category_id, page=1, page_size=10):
        """
        获取产品订阅代码
        :param category_id: 分类ID(从get_categories获取)
        :param page: 页码
        :param page_size: 每页条数
        :return: 产品列表
        """
        url = f"{self.base_url}/getSymbolList.php"
        params = {
            'category': category_id,
            'page': page,
            'pageSize': page_size
        }
        
        try:
            response = self.session.get(url, params=params, timeout=10)
            response.raise_for_status()
            
            data = self._handle_response(response)
            
            if data.get('code') == 200:
                return data.get('data', {})
            else:
                print(f"获取产品列表失败: {data.get('msg')}")
                return {}
                
        except Exception as e:
            print(f"产品查询异常: {e}")
            return {}
    
    def search_symbols_by_name(self, keyword):
        """根据名称搜索产品(遍历所有分类)"""
        results = []
        
        # 获取所有分类
        categories = self.get_categories()
        
        for category in categories:
            cat_id = category['id']
            cat_name = category['name']
            
            # 获取第一页数据
            data = self.get_symbols(cat_id, page=1, page_size=50)
            
            if data and 'list' in data:
                for symbol in data['list']:
                    if keyword.lower() in symbol['name'].lower() or keyword.lower() in symbol['code'].lower():
                        symbol['category'] = cat_name
                        results.append(symbol)
        
        return results

# 使用示例
def demo_symbol_query():
    client = PulseDataSymbol()
    
    # 1. 获取所有分类
    print("=== 产品分类 ===")
    categories = client.get_categories()
    for cat in categories:
        print(f"ID: {cat['id']}, 名称: {cat['name']}")
    
    # 2. 查询数字货币分类下的产品
    print("\n=== 数字货币产品(第1页)===")
    data = client.get_symbols('4', page=1, page_size=5)
    if data:
        print(f"总条数: {data.get('total')}")
        for symbol in data.get('list', []):
            print(f"代码: {symbol['code']}, 名称: {symbol['name']}")
    
    # 3. 搜索比特币相关产品
    print("\n=== 搜索 'BTC' 相关产品 ===")
    results = client.search_symbols_by_name('BTC')
    for symbol in results:
        print(f"[{symbol['category']}] {symbol['code']} - {symbol['name']}")

if __name__ == "__main__":
    demo_symbol_query()

七、总结

脉动数据API为开发者提供了完整的期货、数字货币等金融产品行情数据接入方案。通过本文的实战指南,您可以快速掌握:

  1. WebSocket实时数据推送:适用于高频交易和实时监控场景,需实现心跳维持和断线重连机制
  2. HTTP接口调用:适用于中低频查询,需注意请求频率限制,建议配合缓存使用
  3. K线数据获取:支持多种时间周期,可用于技术分析和策略回测
  4. 产品分类与代码查询:便于动态获取可订阅的产品列表
  5. 系统集成最佳实践:包括限流、缓存、错误处理等关键机制

脉动数据的主要优势包括:

  • 多市场覆盖:外汇、期货、数字货币等全球品种
  • 双通道接入:WebSocket实时推送 + HTTP查询
  • 数据丰富:不仅包含基础行情,还有深度盘口、实时成交等
  • 开发者友好:统一的JSON格式,清晰的字段说明

无论是构建量化交易系统、开发行情分析工具,还是创建风险管理平台,脉动数据API都能提供稳定可靠的数据支持。建议开发者根据实际需求选择合适的接入方式,并合理设计限流、缓存和错误处理机制,确保系统的稳定性和性能。

:期货和数字货币交易具有高风险,投资者应充分了解相关风险并谨慎决策。本文示例代码仅供参考,实际使用时请根据具体需求进行调整优化。