{"templateId":"markdown","sharedDataIds":{},"props":{"metadata":{"markdoc":{"tagList":["admonition"]},"type":"markdown"},"seo":{"title":"Gap Fill","description":"Write your own software to control Collective2","keywords":"documentation,api,automated trading,Collective2 API Documentation","lang":"en-US","siteUrl":"https://c2-api.redocly.app/","meta":[{"name":"charset","content":"utf-8"}],"llmstxt":{"hide":false,"sections":[{"title":"Table of contents","includeFiles":["**/*"],"excludeFiles":[]}],"excludeFiles":[]}},"dynamicMarkdocComponents":[],"compilationErrors":[],"ast":{"$$mdtype":"Tag","name":"article","attributes":{},"children":[{"$$mdtype":"Tag","name":"Heading","attributes":{"level":1,"id":"gap-fill","__idx":0},"children":["Gap Fill"]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["When your client disconnects and reconnects, signals may have been published while you"," ","were offline. The WebSocket API provides a built-in mechanism to replay those missed"," ","signals."]},{"$$mdtype":"Tag","name":"Heading","attributes":{"level":2,"id":"track-the-last-seq_id","__idx":1},"children":["Track the Last seq_id"]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["Every ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["strategy.signal"]}," message includes a ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["seq_id"]}," — a monotonically increasing integer"," ","that uniquely identifies each signal in the stream. Always store the ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["seq_id"]}," of the last"," ","signal you successfully processed:"]},{"$$mdtype":"Tag","name":"CodeBlock","attributes":{"data-language":"python","header":{"controls":{"copy":{}}},"source":"last_seq_id = 0\n\nasync for raw in ws:\n    msg = json.loads(raw)\n    if msg[\"msg_type\"] == \"strategy.signal\":\n        process_signal(msg[\"payload\"])\n        last_seq_id = msg[\"seq_id\"]  # persist this value\n","lang":"python"},"children":[]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["Persist ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["last_seq_id"]}," to durable storage (a database or file) so it survives process"," ","restarts."]},{"$$mdtype":"Tag","name":"Heading","attributes":{"level":2,"id":"resuming-after-reconnection","__idx":2},"children":["Resuming After Reconnection"]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["When you resubscribe after a disconnection, pass your last ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["seq_id"]}," as ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["resume_from"]},":"]},{"$$mdtype":"Tag","name":"CodeBlock","attributes":{"data-language":"json","header":{"controls":{"copy":{}}},"source":"{\n  \"msg_type\": \"subscription.request\",\n  \"timestamp\": \"2024-01-01T12:00:00Z\",\n  \"payload\": {\n    \"action\": \"subscribe\",\n    \"channel\": \"strategy.signal\",\n    \"strategy_id\": 12345678,\n    \"resume_from\": 98765\n  }\n}\n","lang":"json"},"children":[]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["The server will:"]},{"$$mdtype":"Tag","name":"ol","attributes":{},"children":[{"$$mdtype":"Tag","name":"li","attributes":{},"children":["Send a ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["subscription.ack"]}]},{"$$mdtype":"Tag","name":"li","attributes":{},"children":["Replay all signals with ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["seq_id > resume_from"]}," in order (up to 1000 messages)"]},{"$$mdtype":"Tag","name":"li","attributes":{},"children":["Begin live delivery once replay is complete"]}]},{"$$mdtype":"Tag","name":"Admonition","attributes":{"type":"info"},"children":[{"$$mdtype":"Tag","name":"p","attributes":{},"children":["Replayed messages are indistinguishable from live messages — same envelope, same"," ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["seq_id"]},". If your processing is idempotent on ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["seq_id"]},", no deduplication is needed."]}]},{"$$mdtype":"Tag","name":"Heading","attributes":{"level":2,"id":"the-gap-message","__idx":3},"children":["The Gap Message"]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["If more than 1000 signals were published while you were disconnected, the server cannot"," ","replay them all. After sending the maximum number of replayed messages, it sends a ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["gap"]}," ","message:"]},{"$$mdtype":"Tag","name":"CodeBlock","attributes":{"data-language":"json","header":{"controls":{"copy":{}}},"source":"{\n  \"msg_type\": \"gap\",\n  \"id\": \"msg_a1b2c3d4e5f6a1b2c3d4e5f6a7b8\",\n  \"timestamp\": \"2024-01-01T12:00:00Z\",\n  \"v\": 4,\n  \"payload\": {\n    \"strategy_id\": null,\n    \"channel\": \"strategy.signal:12345678\",\n    \"from\": \"98765\",\n    \"message\": \"Replay limit of 1000 messages exceeded\"\n  }\n}\n","lang":"json"},"children":[]},{"$$mdtype":"Tag","name":"div","attributes":{"className":"md-table-wrapper"},"children":[{"$$mdtype":"Tag","name":"table","attributes":{"className":"md"},"children":[{"$$mdtype":"Tag","name":"thead","attributes":{},"children":[{"$$mdtype":"Tag","name":"tr","attributes":{},"children":[{"$$mdtype":"Tag","name":"th","attributes":{"data-label":"Field"},"children":["Field"]},{"$$mdtype":"Tag","name":"th","attributes":{"data-label":"Description"},"children":["Description"]}]}]},{"$$mdtype":"Tag","name":"tbody","attributes":{},"children":[{"$$mdtype":"Tag","name":"tr","attributes":{},"children":[{"$$mdtype":"Tag","name":"td","attributes":{},"children":["channel"]},{"$$mdtype":"Tag","name":"td","attributes":{},"children":["The channel key — you can parse the strategy ID from here (e.g. ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["\"strategy.signal:12345678\""]}," → strategy ID ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["12345678"]},")"]}]},{"$$mdtype":"Tag","name":"tr","attributes":{},"children":[{"$$mdtype":"Tag","name":"td","attributes":{},"children":["from"]},{"$$mdtype":"Tag","name":"td","attributes":{},"children":["The last replayed ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["seq_id"]}," as a string"]}]},{"$$mdtype":"Tag","name":"tr","attributes":{},"children":[{"$$mdtype":"Tag","name":"td","attributes":{},"children":["strategy_id"]},{"$$mdtype":"Tag","name":"td","attributes":{},"children":["Always ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["null"]}," in the current implementation — use ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["channel"]}," to identify the strategy"]}]},{"$$mdtype":"Tag","name":"tr","attributes":{},"children":[{"$$mdtype":"Tag","name":"td","attributes":{},"children":["message"]},{"$$mdtype":"Tag","name":"td","attributes":{},"children":["Human-readable description"]}]}]}]}]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["When you receive a ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["gap"]}," message, the missed signals cannot be replayed individually via"," ","REST — REST endpoints are time-based, not seq_id-based. Instead, ",{"$$mdtype":"Tag","name":"strong","attributes":{},"children":["snapshot the current"," ","state"]}," of the strategy using the REST API and reconcile it against your local state:"]},{"$$mdtype":"Tag","name":"ul","attributes":{},"children":[{"$$mdtype":"Tag","name":"li","attributes":{},"children":[{"$$mdtype":"Tag","name":"code","attributes":{},"children":["GET /Strategies/GetStrategyOpenPositions?StrategyIds=12345678"]}," — returns all currently"," ","open positions. Reconcile your local positions against this to find any that were opened"," ","or closed during the gap."]},{"$$mdtype":"Tag","name":"li","attributes":{},"children":[{"$$mdtype":"Tag","name":"code","attributes":{},"children":["GET /Strategies/GetStrategyActiveOrders?StrategyIds=12345678"]}," — returns all currently"," ","working orders. Reconcile your local orders against this to find any that were placed"," ","or canceled during the gap."]}]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["After reconciling, continue receiving live signals from the WebSocket as normal. The"," ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["from"]}," field in the ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["gap"]}," payload is the last seq_id the server replayed — use it as"," ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["resume_from"]}," if you reconnect again."]},{"$$mdtype":"Tag","name":"Heading","attributes":{"level":2,"id":"full-reconnect-pattern","__idx":4},"children":["Full Reconnect Pattern"]},{"$$mdtype":"Tag","name":"CodeBlock","attributes":{"data-language":"python","header":{"controls":{"copy":{}}},"source":"import asyncio\nimport json\nimport websockets\nfrom datetime import datetime, timezone\n\nAPI_KEY = \"YOUR_API_KEY\"\nWS_URL = \"wss://api4-general.collective2.com/ws\"\nSTRATEGY_ID = 12345678\n\nasync def run(last_seq_id: int = 0):\n    headers = {\"Authorization\": f\"Bearer {API_KEY}\"}\n\n    while True:\n        try:\n            async with websockets.connect(WS_URL, additional_headers=headers) as ws:\n                # Wait for connection.connected\n                await ws.recv()\n\n                # Subscribe, resuming from last known position\n                request = {\n                    \"msg_type\": \"subscription.request\",\n                    \"timestamp\": datetime.now(timezone.utc).isoformat(),\n                    \"payload\": {\n                        \"action\": \"subscribe\",\n                        \"channel\": \"strategy.signal\",\n                        \"strategy_id\": STRATEGY_ID,\n                        \"resume_from\": last_seq_id\n                    }\n                }\n                await ws.send(json.dumps(request))\n\n                async for raw in ws:\n                    msg = json.loads(raw)\n\n                    if msg[\"msg_type\"] == \"heartbeat\":\n                        ack = {\n                            \"msg_type\": \"heartbeat.ack\",\n                            \"timestamp\": datetime.now(timezone.utc).isoformat()\n                        }\n                        await ws.send(json.dumps(ack))\n\n                    elif msg[\"msg_type\"] == \"strategy.signal\":\n                        process_signal(msg[\"payload\"])\n                        last_seq_id = msg[\"seq_id\"]\n\n                    elif msg[\"msg_type\"] == \"gap\":\n                        cursor = msg[\"payload\"][\"from\"]\n                        # Fetch remaining signals via REST and advance last_seq_id\n                        last_seq_id = fetch_historical_signals(STRATEGY_ID, cursor)\n\n        except websockets.ConnectionClosed:\n            print(\"Disconnected, reconnecting in 5 seconds...\")\n            await asyncio.sleep(5)\n\ndef process_signal(payload):\n    print(f\"Signal: {payload}\")\n\ndef fetch_historical_signals(strategy_id: int, from_cursor: str) -> int:\n    # REST endpoints are time-based, not seq_id-based — you cannot replay by seq_id.\n    # Instead, snapshot current state and reconcile:\n    #   GET /Strategies/GetStrategyOpenPositions?StrategyIds={strategy_id}\n    #   GET /Strategies/GetStrategyActiveOrders?StrategyIds={strategy_id}\n    # After reconciling, return from_cursor so the caller knows where the gap ended.\n    return int(from_cursor)\n\nasyncio.run(run())\n","lang":"python"},"children":[]}]},"headings":[{"value":"Gap Fill","id":"gap-fill","depth":1},{"value":"Track the Last seq_id","id":"track-the-last-seq_id","depth":2},{"value":"Resuming After Reconnection","id":"resuming-after-reconnection","depth":2},{"value":"The Gap Message","id":"the-gap-message","depth":2},{"value":"Full Reconnect Pattern","id":"full-reconnect-pattern","depth":2}],"frontmatter":{"title":"Gap Fill","description":"Catching up on missed signals after a disconnection","seo":{"title":"Gap Fill"}},"lastModified":"2026-03-25T16:16:43.000Z","pagePropGetterError":{"message":"","name":""}},"slug":"/guides/websocket-gap-fill","userData":{"isAuthenticated":false,"teams":["anonymous"]},"isPublic":true}