diff --git a/README.md b/README.md index 4c90b60..9fdb310 100644 --- a/README.md +++ b/README.md @@ -1,166 +1,176 @@ -# WebSocket API Documentation | IME Live Market Data +# IME Live Data WebSocket Documentation -## Real-Time Market Streams (IME) +## Introduction -### WebSocket • High-Frequency • Low-Latency +This WebSocket service provides real-time market data for Iran Mercantile Exchange (IME) contracts. + +The service streams live contract updates from Redis Pub/Sub channels and delivers them to connected WebSocket clients. + +Two WebSocket endpoints are available: + +1. WebSocket By Contract Name +2. WebSocket By Contract ID + +Both endpoints provide the exact same payload structure. +The only difference is how contracts are subscribed. --- -## 1. Overview +# WebSocket Endpoints -This documentation describes the **HT Data Engine WebSocket API** for subscribing to real-time market data for **IME (Iran Mercantile Exchange)** contracts. +# 1. WebSocket By Contract Name -The API provides **live market overview streams** including: -- Best limit order book (top 3 levels) -- Aggregate trade data (OHLC, volume, trade count) -- Allowed price range (min/max) -- Contract information (open interest & changes) +```text +wss://YOUR_DOMAIN/live/data/websocket/contract/name +``` -All WebSocket endpoints require **JWT token-based authentication** and support multiple contract names or IDs in a single connection. +## Query Parameters + +| Parameter | Type | Required | Description | +|---|---|---|---| +| contract_names | list[string] | Yes | List of contract names | --- -## 2. Authentication +## Example Connection -To access protected WebSocket endpoints, a valid **JWT token** is required. +```text +wss://YOUR_DOMAIN/live/data/websocket/contract/name?contract_names=ContractA&contract_names=ContractB +``` -### How to get the token +--- -Send a `POST` request to: -https://core.hedgetech.ir/auth/user/token/issue +## Python Example -text +```python +import asyncio +import websockets +import json -**Headers:** -Content-Type: application/x-www-form-urlencoded +async def main(): -text + uri = ( + "ws://127.0.0.1:8000/live/data/websocket/contract/name" + "?contract_names=ContractA" + "&contract_names=ContractB" + ) -**Body:** -username=your_username&password=your_password + async with websockets.connect(uri) as websocket: -text + while True: -### Response Example + message = await websocket.recv() + + data = json.loads(message) + + print(json.dumps(data, indent=4)) + +asyncio.run(main()) +``` + +--- + +# 2. WebSocket By Contract ID + +```text +wss://YOUR_DOMAIN/live/data/websocket/contract/id +``` + +## Query Parameters + +| Parameter | Type | Required | Description | +|---|---|---|---| +| contract_id | list[string] | Yes | List of contract IDs | + +--- + +## Example Connection + +```text +wss://YOUR_DOMAIN/live/data/websocket/contract/id?contract_id=12345&contract_id=67890 +``` + +--- + +## Python Example + +```python +import asyncio +import websockets +import json + +async def main(): + + uri = ( + "ws://127.0.0.1:8000/live/data/websocket/contract/id" + "?contract_id=12345" + "&contract_id=67890" + ) + + async with websockets.connect(uri) as websocket: + + while True: + + message = await websocket.recv() + + data = json.loads(message) + + print(json.dumps(data, indent=4)) + +asyncio.run(main()) +``` + +--- + +# Authentication + +All WebSocket endpoints require authentication. + +If authentication fails, the connection will be closed with: + +```text +1008 POLICY VIOLATION +``` + +--- + +# Message Structure + +## Response Structure (Contract Name) ```json { - "Authorization": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..." + "channel": "IME Stream", + "contractName": "ContractA", + "timestamp": "2026-05-23T15:10:00.000000", + "data": {} } -Usage -Include the token in the WebSocket connection headers: +``` -text -Authorization: -Important Notes: +--- -Tokens are bound to your IP and browser fingerprint. A change invalidates the token. +## Response Structure (Contract ID) -Ensure your account is registered and approved by an admin. - -Unauthorized connections are closed with WS code 1008. - -3. WebSocket Endpoints -Endpoint Description -wss://core.hedgetech.ir/data-engine/ime/live/data/websocket/contract/name Subscribe using contract names -wss://core.hedgetech.ir/data-engine/ime/live/data/websocket/contract/id Subscribe using contract IDs -Note: The output payload structure is identical for both endpoints, except the identifier field: - -contractName for /contract/name - -ContractId for /contract/id - -🔶 Important Clarification: Contract Name vs Contract ID WebSocket Endpoints -The data engine provides two separate WebSocket endpoints: - -Subscribe using Contract Names - -text -wss://core.hedgetech.ir/data-engine/ime/live/data/websocket/contract/name -Subscribe using Contract IDs - -text -wss://core.hedgetech.ir/data-engine/ime/live/data/websocket/contract/id -Both endpoints deliver identical payload structures, including the same channel and the same data schema. The only difference is the identifier field inside each message: - -Endpoint Identifier Field in Payload -/contract/name "contractName": "" -/contract/id "ContractId": "" -Example for Contract Name endpoint: - -json -{ - "channel": "IME Stream", - "contractName": "IMEFutures_Sample", - "timestamp": "2025-11-14T12:00:00.000000", - "data": { ... } -} -Example for Contract ID endpoint: - -json -{ - "channel": "IME Stream", - "ContractId": "IME123456789", - "timestamp": "2025-11-14T12:00:00.000000", - "data": { ... } -} -No other structural difference exists between these two WebSocket services. - -Why this clarification matters: - -Consumers might assume that subscribing to the contract-name endpoint returns a different schema — it does not. - -Client implementations should be prepared to handle either identifier field (contractName or ContractId) depending on which endpoint they connect to. - -This avoids confusing bugs (for example: looking for ContractId in messages coming from the /contract/name endpoint). - -4. Connection Flow -Establish WebSocket connection with the proper Authorization header. - -Include query parameters in the URL. Each contract name/ID is repeated as a separate query parameter: - -For Contract Name endpoint: - -text -wss://core.hedgetech.ir/data-engine/ime/live/data/websocket/contract/name?contract_names=&contract_names= -For Contract ID endpoint: - -text -wss://core.hedgetech.ir/data-engine/ime/live/data/websocket/contract/id?contract_id=&contract_id= -If verification passes, the WebSocket connection is accepted. - -Real-time messages are streamed continuously until the connection is closed. - -Important: Unauthorized connections are closed immediately with code 1008. - -5. Query Parameters -Parameter Type Description -contract_names list of strings List of contract names to subscribe (for /contract/name) -contract_id list of strings List of contract IDs to subscribe (for /contract/id) -Example URL (Contract Name): - -text -wss://core.hedgetech.ir/data-engine/ime/live/data/websocket/contract/name?contract_names=IMEFutures_Sample&contract_names=IMEOption_Sample -Example URL (Contract ID): - -text -wss://core.hedgetech.ir/data-engine/ime/live/data/websocket/contract/id?contract_id=IME123456789&contract_id=IME987654321 -6. Channel & Payload Schema -All messages are delivered in the following JSON structure: - -json +```json { "channel": "IME Stream", - "contractName": "IMEFutures_Sample", - "timestamp": "2025-11-14T12:00:00.000000", - "data": { ... } + "ContractId": "12345", + "timestamp": "2026-05-23T15:10:00.000000", + "data": {} } -NOTE: For the /contract/id endpoint, the contractName field above is replaced by ContractId. Everything else remains the same. +``` -Complete Data Payload Structure -The data field contains a comprehensive market overview with four subsections: +--- -json +# Data Payload Structure + +The `data` field contains the complete live contract market data. + +--- + +## Full Payload Example + +```json { "BestLimit": { "1": { @@ -204,136 +214,159 @@ json "open_interest_changes": 0 } } -6.1 Field Descriptions -BestLimit (Top 3 Order Book Levels) -Field Type Description -buy_quantity int Total buy quantity at this level -buy_price int Buy price at this level -sell_quantity int Total sell quantity at this level -sell_price int Sell price at this level -Aggregate (Trading Statistics) -Field Type Description -date str Trading date (YYYY-MM-DD) -time str Time of last trade update (HH:MM:SS) -trade_count int Number of trades -total_volume int Total volume traded -total_value int Total value of trades -closing_price float Closing price of the contract -last_price float Last traded price -low_price float Lowest traded price -high_price float Highest traded price -open_price float Opening price -previous_close float Previous day's closing price -AllowedPriceRange (Price Limits) -Field Type Description -minAllowedPrice float Minimum allowed price for the contract -maxAllowedPrice float Maximum allowed price for the contract -ContractInfo (Position Information) -Field Type Description -open_interest int Open interest for the contract -open_interest_changes int Change in open interest compared to previous period -7. Error Handling -Code Description -1008 Policy violation (invalid JWT, invalid contract names/IDs) -Connection closed Occurs if Redis stream fails or server error -8. Examples -8.1 Python (WebSocket Client) -python +``` + +--- + +# Payload Fields Description + +# BestLimit + +Represents the top buy and sell order book levels. + +## Levels + +| Level | Description | +|---|---| +| 1 | Best market level | +| 2 | Second order book level | +| 3 | Third order book level | + +## Fields + +| Field | Description | +|---|---| +| buy_quantity | Buy order quantity | +| buy_price | Buy order price | +| sell_quantity | Sell order quantity | +| sell_price | Sell order price | + +--- + +# Aggregate + +Aggregated market statistics for the contract. + +| Field | Description | +|---|---| +| date | Trading date | +| time | Last update time | +| trade_count | Number of trades | +| total_volume | Total traded volume | +| total_value | Total traded value | +| closing_price | Closing price | +| last_price | Last traded price | +| low_price | Lowest traded price | +| high_price | Highest traded price | +| open_price | Opening price | +| previous_close | Previous closing price | + +--- + +# AllowedPriceRange + +Allowed trading price range. + +| Field | Description | +|---|---| +| minAllowedPrice | Minimum allowed price | +| maxAllowedPrice | Maximum allowed price | + +--- + +# ContractInfo + +Additional contract information. + +| Field | Description | +|---|---| +| open_interest | Open interest | +| open_interest_changes | Open interest changes | + +--- + +# Error Handling + +## Invalid Authentication + +```text +WebSocket closed with code 1008 +``` + +--- + +## Invalid Contract + +If an invalid contract name or contract ID is provided, the connection will be closed. + +```text +WebSocket closed with code 1008 +``` + +--- + +# Notes + +- The WebSocket streams real-time market data updates. +- Each contract is subscribed through a dedicated Redis Pub/Sub stream. +- Updates are only sent when market data changes. +- The WebSocket connection remains active until disconnected by the client. +- Multiple contracts can be subscribed simultaneously. +- Timestamps are provided in ISO 8601 format. + +--- + +# Recommended Client Strategy + +For production usage, it is recommended to: + +- Implement automatic reconnect logic +- Enable heartbeat / ping interval +- Process messages asynchronously +- Use internal message queues +- Configure WebSocket timeout handling + +--- + +# Recommended Python Reconnect Example + +```python import asyncio -import websockets import json +import websockets -async def subscribe_ime(url: str, token: str): - headers = {"Authorization": token} - async with websockets.connect(url, extra_headers=headers) as ws: - async for message in ws: - data = json.loads(message) - identifier = data.get("contractName") or data.get("ContractId") - payload = data.get("data") - - print(f"{data['timestamp']} | {identifier}") - print(f" Last Price: {payload.get('Aggregate', {}).get('last_price')}") - print(f" Best Buy: {payload.get('BestLimit', {}).get('1', {}).get('buy_price')}") - -# Usage -url = "wss://core.hedgetech.ir/data-engine/ime/live/data/websocket/contract/name?contract_names=IMEFutures_Sample" -token = "" -asyncio.run(subscribe_ime(url, token)) -8.2 JavaScript (WebSocket Client) -javascript -const WebSocket = require('ws'); - -const url = 'wss://core.hedgetech.ir/data-engine/ime/live/data/websocket/contract/name?contract_names=IMEFutures_Sample'; -const token = ''; - -const ws = new WebSocket(url, { headers: { Authorization: token } }); - -ws.on('open', () => console.log('Connected')); -ws.on('message', (data) => { - const msg = JSON.parse(data); - const identifier = msg.contractName || msg.ContractId; - console.log(`${msg.timestamp} | ${identifier}`); - console.log(' Last Price:', msg.data.Aggregate.last_price); -}); -ws.on('close', () => console.log('Disconnected')); -8.3 Go (WebSocket Client) -go -package main - -import ( - "encoding/json" - "fmt" - "github.com/gorilla/websocket" - "log" +URI = ( + "ws://127.0.0.1:8000/live/data/websocket/contract/id" + "?contract_id=12345" ) -func main() { - url := "wss://core.hedgetech.ir/data-engine/ime/live/data/websocket/contract/name?contract_names=IMEFutures_Sample" - header := map[string][]string{"Authorization": {""}} - - c, _, err := websocket.DefaultDialer.Dial(url, header) - if err != nil { - log.Fatal(err) - } - defer c.Close() - - for { - _, message, _ := c.ReadMessage() - var msg map[string]interface{} - json.Unmarshal(message, &msg) - - identifier := msg["contractName"] - if identifier == nil { - identifier = msg["ContractId"] - } - - data := msg["data"].(map[string]interface{}) - agg := data["Aggregate"].(map[string]interface{}) - - fmt.Printf("%s | %v | Last Price: %v\n", - msg["timestamp"], identifier, agg["last_price"]) - } -} -9. Best Practices -Reconnect with exponential backoff in case of disconnects. +async def connect(): -Validate your JWT before subscribing. + while True: -Subscribe only to the contracts you need to reduce bandwidth. + try: -Handle both contractName and ContractId in your message parsing. + async with websockets.connect( + URI, + ping_interval=20, + ping_timeout=20 + ) as websocket: -The data payload is the same for both endpoints; reuse your parsing logic. + print("Connected") -Appendix: Quick Developer Checklist -✅ Use correct endpoint (/contract/name vs /contract/id) + while True: -✅ Provide Authorization header with valid token + message = await websocket.recv() -✅ Include contract_names or contract_id as repeated query params + data = json.loads(message) -✅ Handle both contractName and ContractId in message parsing + print(data) -✅ Monitor for WS close code 1008 for authorization errors + except Exception as e: -✅ BestLimit contains only top 3 levels (keys: "1", "2", "3") \ No newline at end of file + print("Disconnected:", e) + + await asyncio.sleep(5) + +asyncio.run(connect()) +```