> ## Documentation Index
> Fetch the complete documentation index at: https://docs.55-tech.com/llms.txt
> Use this file to discover all available pages before exploring further.

# MM WebSocket - 实时更新

> 连接 MM WebSocket 获取实时订单成交、对冲投注更新、账户余额变动、比分和紧急状态。基于服务端广播。

## 端点

```
wss://mmapi.55-tech.com/ws/subscribe
```

## 连接流程

### 1. 连接

打开WebSocket连接。连接时无需身份验证。

### 2. 订阅

在连接后 **30秒** 内发送订阅消息：

```json theme={null}
{
  "type": "subscribe",
  "apiKey": "your-uuid-api-key",
  "channels": ["orders", "bets", "accounts", "scores", "emergency"]
}
```

### 3. 确认

成功后，服务器响应：

```json theme={null}
{
  "type": "subscribed",
  "subscriptionId": 1,
  "channels": ["orders", "bets", "accounts", "scores", "emergency"]
}
```

### 4. 接收广播

数据更新以广播消息形式到达，包含当前和先前状态：

```json theme={null}
{
  "type": "broadcast",
  "channel": "orders",
  "event": "UPDATE",
  "payload": {
    "orderId": 123,
    "orderStatus": "FILLED",
    "matchedStake": 100.0,
    "matchedAt": "2026-02-15T10:30:05Z"
  },
  "old": {
    "orderStatus": "PLACED",
    "matchedStake": 0
  }
}
```

`old` 字段包含先前状态，便于检测变更。

### 5. 保持活跃

发送心跳保持连接：

```json theme={null}
{"type": "ping"}
```

服务器响应：

```json theme={null}
{"type": "pong"}
```

服务器也会每 **60秒** 发送心跳：

```json theme={null}
{"type": "heartbeat"}
```

### 6. 取消订阅

停止接收更新并清理订阅：

```json theme={null}
{"type": "unsubscribe"}
```

## 频道

### 客户端过滤频道

这些频道仅传递属于您客户端的数据：

| 频道         | 事件             | 描述              |
| ---------- | -------------- | --------------- |
| `orders`   | INSERT, UPDATE | 交易所订单下达、状态变更、成交 |
| `bets`     | INSERT, UPDATE | 对冲投注下达、确认、结算    |
| `accounts` | UPDATE         | 账户余额和状态变更       |

### 全局频道

所有订阅者都会收到：

| 频道          | 事件     | 描述             |
| ----------- | ------ | -------------- |
| `scores`    | UPDATE | 实时比分更新（进球、盘、节） |
| `emergency` | UPDATE | 紧急模式激活/停用      |

## 消息示例

### 订单成交

当交易所订单被匹配时：

```json theme={null}
{
  "type": "broadcast",
  "channel": "orders",
  "event": "UPDATE",
  "payload": {
    "orderId": 123,
    "fixtureId": "id1000000861624412",
    "outcomeId": 161,
    "exchange": "polymarket",
    "exchangeOrderId": "0x1a2b3c...",
    "orderStatus": "FILLED",
    "matchedStatus": "FULLY_MATCHED",
    "orderCents": 0.45,
    "orderStake": 100.0,
    "matchedStake": 100.0,
    "matchedAt": "2026-02-15T10:30:05Z"
  },
  "old": {
    "orderStatus": "PLACED",
    "matchedStatus": "NOT_MATCHED",
    "matchedStake": 0
  }
}
```

### 对冲投注下达

订单成交后，系统自动下对冲注：

```json theme={null}
{
  "type": "broadcast",
  "channel": "bets",
  "event": "INSERT",
  "payload": {
    "betId": 456,
    "orderId": 123,
    "client": "your-client",
    "bookmaker": "vertex",
    "placedPrice": 1.808,
    "placedStake": 100.0,
    "betStatus": "placed",
    "sentData": {
      "requestId": "a1b2c3d4",
      "eventId": 98765,
      "price": 1.808,
      "amount": 100.0,
      "side": "1"
    },
    "receivedData": {
      "betId": "789",
      "status": "U"
    },
    "placedAt": "2026-02-15T10:30:08Z"
  }
}
```

### 实时比分更新

```json theme={null}
{
  "type": "broadcast",
  "channel": "scores",
  "event": "UPDATE",
  "payload": {
    "fixtureId": "id1000000861624412",
    "live": true,
    "statusId": 1,
    "currentPeriod": "下半场",
    "currentMinute": 67,
    "scores": {
      "home": 2,
      "away": 1,
      "period1Home": 1,
      "period1Away": 0
    }
  }
}
```

### 紧急状态

```json theme={null}
{
  "type": "broadcast",
  "channel": "emergency",
  "event": "UPDATE",
  "payload": {
    "emergency": true,
    "reason": "手动触发",
    "triggeredAt": "2026-02-15T10:30:00Z"
  }
}
```

## 连接限制

| 设置          | 值       |
| ----------- | ------- |
| 每API密钥最大连接数 | 5       |
| 认证超时        | 30秒     |
| 服务器心跳间隔     | 60秒     |
| 客户端心跳间隔     | 30秒（推荐） |

## 错误消息

| 错误                          | 描述                  |
| --------------------------- | ------------------- |
| `apiKey required`           | 订阅消息中缺少 `apiKey` 字段 |
| `Invalid apiKey format`     | API密钥必须是有效的UUID     |
| `Invalid API key`           | API密钥未找到或客户端未激活     |
| `Connection limit exceeded` | 此API密钥已有5个活跃连接      |

## 示例：Python 客户端

```python theme={null}
import asyncio
import json
import websockets

async def connect():
    uri = "wss://mmapi.55-tech.com/ws/subscribe"
    async with websockets.connect(uri) as ws:
        # 订阅
        await ws.send(json.dumps({
            "type": "subscribe",
            "apiKey": "YOUR_API_KEY",
            "channels": ["orders", "bets", "scores"]
        }))

        # 等待确认
        sub_resp = json.loads(await ws.recv())
        print(f"已订阅 (id={sub_resp.get('subscriptionId')})")

        # 保活任务
        async def keep_alive():
            while True:
                await asyncio.sleep(30)
                await ws.send(json.dumps({"type": "ping"}))

        asyncio.create_task(keep_alive())

        # 监听更新
        async for message in ws:
            data = json.loads(message)
            if data["type"] == "broadcast":
                channel = data["channel"]
                event = data["event"]
                print(f"[{channel}:{event}] {data['payload']}")
                if "old" in data:
                    print(f"  变更自: {data['old']}")

asyncio.run(connect())
```

## 示例：JavaScript 客户端

```javascript theme={null}
const WebSocket = require('ws');

const ws = new WebSocket('wss://mmapi.55-tech.com/ws/subscribe');

ws.on('open', () => {
  ws.send(JSON.stringify({
    type: 'subscribe',
    apiKey: 'YOUR_API_KEY',
    channels: ['orders', 'bets', 'scores']
  }));
});

ws.on('message', (raw) => {
  const msg = JSON.parse(raw);

  if (msg.type === 'subscribed') {
    console.log(`已订阅 (id=${msg.subscriptionId})`);
  }

  if (msg.type === 'broadcast') {
    console.log(`[${msg.channel}:${msg.event}]`, msg.payload);
    if (msg.old) {
      console.log('  变更自:', msg.old);
    }
  }
});

// 保持活跃
setInterval(() => {
  if (ws.readyState === WebSocket.OPEN) {
    ws.send(JSON.stringify({ type: 'ping' }));
  }
}, 30000);
```
