{"id":273,"date":"2026-09-08T11:50:40","date_gmt":"2026-09-08T03:50:40","guid":{"rendered":"https:\/\/blog.infoway.io\/en\/?p=273"},"modified":"2026-09-08T11:50:43","modified_gmt":"2026-09-08T03:50:43","slug":"how-to-stream-real-time-financial-news-with-infoways-websocket-api","status":"publish","type":"post","link":"https:\/\/blog.infoway.io\/en\/how-to-stream-real-time-financial-news-with-infoways-websocket-api\/","title":{"rendered":"How to Stream Real-Time Financial News with Infoway&#8217;s WebSocket API"},"content":{"rendered":"\n<p class=\"wp-block-paragraph\"><strong>Infoway&#8217;s real-time financial news API is live.<\/strong> With the same API key you already use for market data, you can now open a WebSocket and receive a structured stream of global financial news \u2014 every item tagged with the tickers it affects, an urgency level, and a cross-source deduplication key, in 11 languages.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">This guide is a hands-on walkthrough. By the end you&#8217;ll have a production-ready Python client that connects, subscribes, survives disconnects, deduplicates, and routes each headline to the instruments it concerns. If you just want to see a news push land in your terminal, jump to <a href=\"#elementor-toc__heading-anchor-3\">Quickstart<\/a>.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Everything below \u2014 endpoints, protocol codes, field names, and the latency figures \u2014 reflects the live production service.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Who this is for<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">The news feed is built for programs, not readers. It&#8217;s the right tool if you&#8217;re building:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li><strong>Event-driven strategies<\/strong> that need to act within seconds of a headline.<\/li>\n\n\n\n<li><strong>Portfolio and watchlist alerting<\/strong> \u2014 &#8220;tell me when something happens to a stock I hold.&#8221;<\/li>\n\n\n\n<li><strong>Per-instrument news panels<\/strong> inside a trading terminal or broker app.<\/li>\n\n\n\n<li><strong>Cross-market, multi-language monitoring dashboards.<\/strong><\/li>\n\n\n\n<li><strong>News-to-price research pipelines<\/strong> that line up a headline against what the market did next.<\/li>\n<\/ul>\n\n\n\n<p class=\"wp-block-paragraph\">If all you need is a list of headlines to render on a page, a plain REST &#8220;latest news&#8221; endpoint is simpler and it&#8217;s the better fit. The Infoway feed is aimed at the case where a machine consumes each item and decides what to do with it.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">1. How the feed is designed: events, not articles<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Most news APIs hand you an article: a title, a blurb, a source name, a link, maybe a thumbnail. That&#8217;s everything you need to <em>show<\/em> the news to a person, and nothing you need to <em>act<\/em> on it programmatically. To go from that payload to &#8220;<strong>which of my positions does this affect, and how urgent is it?<\/strong>&#8221; you&#8217;d build and maintain a named-entity-recognition pipeline, a company-name-to-ticker mapping, a cross-source dedup layer, and a priority model.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Infoway pushes the result of that work instead. Each message is a structured event:<\/p>\n\n\n\n<figure class=\"wp-block-table\"><table class=\"has-fixed-layout\"><thead><tr><th>You get<\/th><th>Field<\/th><th>Why it matters<\/th><\/tr><\/thead><tbody><tr><td>Associated tickers<\/td><td><code>symbols<\/code><\/td><td>Route the item to holdings\/instruments with a set intersection \u2014 no NER to build<\/td><\/tr><tr><td>Urgency level<\/td><td><code>urgency<\/code><\/td><td>Turn the stream into a priority queue; flash breaking items, batch the rest<\/td><\/tr><tr><td>Cross-source dedup key<\/td><td><code>dk<\/code><\/td><td>Reuters and a wire service report the same event \u2014 one <code>dk<\/code>, so you process it once<\/td><\/tr><tr><td>Full body + summary<\/td><td><code>content<\/code>, <code>sd<\/code><\/td><td>Run your own NLP\/LLM on the text, or show the summary directly<\/td><\/tr><tr><td>Machine timestamp<\/td><td><code>published<\/code><\/td><td>Unix seconds, ready for arithmetic and alignment against price bars<\/td><\/tr><\/tbody><\/table><\/figure>\n\n\n\n<p class=\"wp-block-paragraph\">The transport is a WebSocket, so there&#8217;s no polling interval sitting between an event happening and your code hearing about it. You subscribe once; the server pushes.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">2. Prerequisites<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>An API key with news access.<\/strong> <\/p>\n\n\n\n<p class=\"wp-block-paragraph\">The news feed is available on the <strong>Premium plan ($199\/month) and above<\/strong>, or an equivalent custom plan. It uses the same key and the same authentication as Infoway&#8217;s real-time market data \u2014 if you&#8217;re already streaming prices, you only need to make sure news is enabled on your plan. Get a key from the Infoway dashboard.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>One Python dependency:<\/strong><\/p>\n\n\n\n<div class=\"wp-block-kevinbatdorf-code-block-pro\" data-code-block-pro-font-family=\"Code-Pro-JetBrains-Mono\" style=\"font-size:.875rem;font-family:Code-Pro-JetBrains-Mono,ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;line-height:1.25rem;--cbp-tab-width:2;tab-size:var(--cbp-tab-width, 2)\"><span style=\"display:block;padding:16px 0 0 16px;margin-bottom:-1px;width:100%;text-align:left;background-color:#2e3440ff\"><svg xmlns=\"http:\/\/www.w3.org\/2000\/svg\" width=\"54\" height=\"14\" viewBox=\"0 0 54 14\"><g fill=\"none\" fill-rule=\"evenodd\" transform=\"translate(1 1)\"><circle cx=\"6\" cy=\"6\" r=\"6\" fill=\"#FF5F56\" stroke=\"#E0443E\" stroke-width=\".5\"><\/circle><circle cx=\"26\" cy=\"6\" r=\"6\" fill=\"#FFBD2E\" stroke=\"#DEA123\" stroke-width=\".5\"><\/circle><circle cx=\"46\" cy=\"6\" r=\"6\" fill=\"#27C93F\" stroke=\"#1AAB29\" stroke-width=\".5\"><\/circle><\/g><\/svg><\/span><span role=\"button\" tabindex=\"0\" style=\"color:#d8dee9ff;display:none\" aria-label=\"Copy\" class=\"code-block-pro-copy-button\"><pre class=\"code-block-pro-copy-button-pre\" aria-hidden=\"true\"><textarea class=\"code-block-pro-copy-button-textarea\" tabindex=\"-1\" aria-hidden=\"true\" readonly>pip install websockets<\/textarea><\/pre><svg xmlns=\"http:\/\/www.w3.org\/2000\/svg\" style=\"width:24px;height:24px\" fill=\"none\" viewBox=\"0 0 24 24\" stroke=\"currentColor\" stroke-width=\"2\"><path class=\"with-check\" stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2m-6 9l2 2 4-4\"><\/path><path class=\"without-check\" stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2\"><\/path><\/svg><\/span><pre class=\"shiki nord\" style=\"background-color: #2e3440ff\" tabindex=\"0\"><code><span class=\"line\"><span style=\"color: #88C0D0\">pip<\/span><span style=\"color: #D8DEE9FF\"> <\/span><span style=\"color: #A3BE8C\">install<\/span><span style=\"color: #D8DEE9FF\"> <\/span><span style=\"color: #A3BE8C\">websockets<\/span><\/span><\/code><\/pre><\/div>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Your key in an environment variable<\/strong> \u2014 never hard-code it, and never ship it in front-end code, since it travels in the URL:<\/p>\n\n\n\n<div class=\"wp-block-kevinbatdorf-code-block-pro\" data-code-block-pro-font-family=\"Code-Pro-JetBrains-Mono\" style=\"font-size:.875rem;font-family:Code-Pro-JetBrains-Mono,ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;line-height:1.25rem;--cbp-tab-width:2;tab-size:var(--cbp-tab-width, 2)\"><span style=\"display:block;padding:16px 0 0 16px;margin-bottom:-1px;width:100%;text-align:left;background-color:#2e3440ff\"><svg xmlns=\"http:\/\/www.w3.org\/2000\/svg\" width=\"54\" height=\"14\" viewBox=\"0 0 54 14\"><g fill=\"none\" fill-rule=\"evenodd\" transform=\"translate(1 1)\"><circle cx=\"6\" cy=\"6\" r=\"6\" fill=\"#FF5F56\" stroke=\"#E0443E\" stroke-width=\".5\"><\/circle><circle cx=\"26\" cy=\"6\" r=\"6\" fill=\"#FFBD2E\" stroke=\"#DEA123\" stroke-width=\".5\"><\/circle><circle cx=\"46\" cy=\"6\" r=\"6\" fill=\"#27C93F\" stroke=\"#1AAB29\" stroke-width=\".5\"><\/circle><\/g><\/svg><\/span><span role=\"button\" tabindex=\"0\" style=\"color:#d8dee9ff;display:none\" aria-label=\"Copy\" class=\"code-block-pro-copy-button\"><pre class=\"code-block-pro-copy-button-pre\" aria-hidden=\"true\"><textarea class=\"code-block-pro-copy-button-textarea\" tabindex=\"-1\" aria-hidden=\"true\" readonly>export INFOWAY_API_KEY=\"your_key_here\"<\/textarea><\/pre><svg xmlns=\"http:\/\/www.w3.org\/2000\/svg\" style=\"width:24px;height:24px\" fill=\"none\" viewBox=\"0 0 24 24\" stroke=\"currentColor\" stroke-width=\"2\"><path class=\"with-check\" stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2m-6 9l2 2 4-4\"><\/path><path class=\"without-check\" stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2\"><\/path><\/svg><\/span><pre class=\"shiki nord\" style=\"background-color: #2e3440ff\" tabindex=\"0\"><code><span class=\"line\"><span style=\"color: #81A1C1\">export<\/span><span style=\"color: #D8DEE9FF\"> <\/span><span style=\"color: #D8DEE9\">INFOWAY_API_KEY<\/span><span style=\"color: #81A1C1\">=<\/span><span style=\"color: #ECEFF4\">&quot;<\/span><span style=\"color: #A3BE8C\">your_key_here<\/span><span style=\"color: #ECEFF4\">&quot;<\/span><\/span><\/code><\/pre><\/div>\n\n\n\n<h2 class=\"wp-block-heading\">Quickstart<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Here is the smallest client that does something useful: connect, subscribe to the English channel, keep the connection alive, and print each headline as it arrives.<\/p>\n\n\n\n<div class=\"wp-block-kevinbatdorf-code-block-pro\" data-code-block-pro-font-family=\"Code-Pro-JetBrains-Mono\" style=\"font-size:.875rem;font-family:Code-Pro-JetBrains-Mono,ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;line-height:1.25rem;--cbp-tab-width:2;tab-size:var(--cbp-tab-width, 2)\"><span style=\"display:block;padding:16px 0 0 16px;margin-bottom:-1px;width:100%;text-align:left;background-color:#2e3440ff\"><svg xmlns=\"http:\/\/www.w3.org\/2000\/svg\" width=\"54\" height=\"14\" viewBox=\"0 0 54 14\"><g fill=\"none\" fill-rule=\"evenodd\" transform=\"translate(1 1)\"><circle cx=\"6\" cy=\"6\" r=\"6\" fill=\"#FF5F56\" stroke=\"#E0443E\" stroke-width=\".5\"><\/circle><circle cx=\"26\" cy=\"6\" r=\"6\" fill=\"#FFBD2E\" stroke=\"#DEA123\" stroke-width=\".5\"><\/circle><circle cx=\"46\" cy=\"6\" r=\"6\" fill=\"#27C93F\" stroke=\"#1AAB29\" stroke-width=\".5\"><\/circle><\/g><\/svg><\/span><span role=\"button\" tabindex=\"0\" style=\"color:#d8dee9ff;display:none\" aria-label=\"Copy\" class=\"code-block-pro-copy-button\"><pre class=\"code-block-pro-copy-button-pre\" aria-hidden=\"true\"><textarea class=\"code-block-pro-copy-button-textarea\" tabindex=\"-1\" aria-hidden=\"true\" readonly>import asyncio, json, os, uuid\nimport websockets\n\nWS_URL = f\"wss:\/\/data.infoway.io\/news?apikey={os.environ&#91;'INFOWAY_API_KEY'&#93;}\"\n\nasync def main():\n    async with websockets.connect(WS_URL) as ws:\n        # Subscribe to one language channel (code 10020)\n        await ws.send(json.dumps({\n            \"code\": 10020,\n            \"trace\": uuid.uuid4().hex,\n            \"data\": {\"lang\": \"en\"},\n        }))\n\n        async def heartbeat():\n            while True:\n                await asyncio.sleep(30)\n                await ws.send(json.dumps({\"code\": 10010, \"trace\": uuid.uuid4().hex}))\n\n        asyncio.create_task(heartbeat())\n\n        async for raw in ws:\n            msg = json.loads(raw)\n            if msg.get(\"code\") == 10022:                 # news push\n                n = msg&#91;\"data\"&#93;\n                syms = \", \".join(n.get(\"symbols\") or []) or \"\u2014\"\n                print(f\"[{n&#91;'urgency'&#93;}] {n&#91;'provider'&#93;:&lt;16} {n&#91;'title'&#93;}\")\n                print(f\"      symbols: {syms}\")\n\nasyncio.run(main())<\/textarea><\/pre><svg xmlns=\"http:\/\/www.w3.org\/2000\/svg\" style=\"width:24px;height:24px\" fill=\"none\" viewBox=\"0 0 24 24\" stroke=\"currentColor\" stroke-width=\"2\"><path class=\"with-check\" stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2m-6 9l2 2 4-4\"><\/path><path class=\"without-check\" stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2\"><\/path><\/svg><\/span><pre class=\"shiki nord\" style=\"background-color: #2e3440ff\" tabindex=\"0\"><code><span class=\"line\"><span style=\"color: #88C0D0\">import<\/span><span style=\"color: #D8DEE9FF\"> <\/span><span style=\"color: #A3BE8C\">asyncio,<\/span><span style=\"color: #D8DEE9FF\"> <\/span><span style=\"color: #A3BE8C\">json,<\/span><span style=\"color: #D8DEE9FF\"> <\/span><span style=\"color: #A3BE8C\">os,<\/span><span style=\"color: #D8DEE9FF\"> <\/span><span style=\"color: #A3BE8C\">uuid<\/span><\/span>\n<span class=\"line\"><span style=\"color: #88C0D0\">import<\/span><span style=\"color: #D8DEE9FF\"> <\/span><span style=\"color: #A3BE8C\">websockets<\/span><\/span>\n<span class=\"line\"><\/span>\n<span class=\"line\"><span style=\"color: #88C0D0\">WS_URL<\/span><span style=\"color: #D8DEE9FF\"> <\/span><span style=\"color: #A3BE8C\">=<\/span><span style=\"color: #D8DEE9FF\"> <\/span><span style=\"color: #A3BE8C\">f<\/span><span style=\"color: #ECEFF4\">&quot;<\/span><span style=\"color: #A3BE8C\">wss:\/\/data.infoway.io\/news?apikey={os.environ&#91;&#39;INFOWAY_API_KEY&#39;&#93;}<\/span><span style=\"color: #ECEFF4\">&quot;<\/span><\/span>\n<span class=\"line\"><\/span>\n<span class=\"line\"><span style=\"color: #88C0D0\">async<\/span><span style=\"color: #D8DEE9FF\"> <\/span><span style=\"color: #A3BE8C\">def<\/span><span style=\"color: #D8DEE9FF\"> <\/span><span style=\"color: #A3BE8C\">main<\/span><span style=\"color: #ECEFF4\">()<\/span><span style=\"color: #A3BE8C\">:<\/span><\/span>\n<span class=\"line\"><span style=\"color: #D8DEE9FF\">    <\/span><span style=\"color: #88C0D0\">async<\/span><span style=\"color: #D8DEE9FF\"> <\/span><span style=\"color: #A3BE8C\">with<\/span><span style=\"color: #D8DEE9FF\"> <\/span><span style=\"color: #A3BE8C\">websockets.connect<\/span><span style=\"color: #ECEFF4\">(<\/span><span style=\"color: #88C0D0\">WS_URL<\/span><span style=\"color: #ECEFF4\">)<\/span><span style=\"color: #D8DEE9FF\"> <\/span><span style=\"color: #A3BE8C\">as<\/span><span style=\"color: #D8DEE9FF\"> <\/span><span style=\"color: #A3BE8C\">ws:<\/span><\/span>\n<span class=\"line\"><span style=\"color: #D8DEE9FF\">        <\/span><span style=\"color: #616E88\"># Subscribe to one language channel (code 10020)<\/span><\/span>\n<span class=\"line\"><span style=\"color: #D8DEE9FF\">        <\/span><span style=\"color: #88C0D0\">await<\/span><span style=\"color: #D8DEE9FF\"> <\/span><span style=\"color: #A3BE8C\">ws.send<\/span><span style=\"color: #ECEFF4\">(<\/span><span style=\"color: #88C0D0\">json.dumps(<\/span><span style=\"color: #D8DEE9FF\">{<\/span><\/span>\n<span class=\"line\"><span style=\"color: #D8DEE9FF\">            <\/span><span style=\"color: #88C0D0\">&quot;code&quot;<\/span><span style=\"color: #88C0D0\">:<\/span><span style=\"color: #D8DEE9FF\"> <\/span><span style=\"color: #B48EAD\">10020<\/span><span style=\"color: #A3BE8C\">,<\/span><\/span>\n<span class=\"line\"><span style=\"color: #D8DEE9FF\">            <\/span><span style=\"color: #88C0D0\">&quot;trace&quot;<\/span><span style=\"color: #88C0D0\">:<\/span><span style=\"color: #D8DEE9FF\"> <\/span><span style=\"color: #A3BE8C\">uuid.uuid4<\/span><span style=\"color: #ECEFF4\">()<\/span><span style=\"color: #A3BE8C\">.hex,<\/span><\/span>\n<span class=\"line\"><span style=\"color: #D8DEE9FF\">            <\/span><span style=\"color: #88C0D0\">&quot;data&quot;<\/span><span style=\"color: #88C0D0\">:<\/span><span style=\"color: #D8DEE9FF\"> <\/span><span style=\"color: #A3BE8C\">{<\/span><span style=\"color: #ECEFF4\">&quot;<\/span><span style=\"color: #A3BE8C\">lang<\/span><span style=\"color: #ECEFF4\">&quot;<\/span><span style=\"color: #A3BE8C\">:<\/span><span style=\"color: #D8DEE9FF\"> <\/span><span style=\"color: #ECEFF4\">&quot;<\/span><span style=\"color: #A3BE8C\">en<\/span><span style=\"color: #ECEFF4\">&quot;<\/span><span style=\"color: #A3BE8C\">},<\/span><\/span>\n<span class=\"line\"><span style=\"color: #D8DEE9FF\">        }<\/span><span style=\"color: #ECEFF4\">)<\/span><span style=\"color: #D8DEE9FF\">)<\/span><\/span>\n<span class=\"line\"><\/span>\n<span class=\"line\"><span style=\"color: #D8DEE9FF\">        <\/span><span style=\"color: #88C0D0\">async<\/span><span style=\"color: #D8DEE9FF\"> <\/span><span style=\"color: #A3BE8C\">def<\/span><span style=\"color: #D8DEE9FF\"> <\/span><span style=\"color: #A3BE8C\">heartbeat<\/span><span style=\"color: #ECEFF4\">()<\/span><span style=\"color: #A3BE8C\">:<\/span><\/span>\n<span class=\"line\"><span style=\"color: #D8DEE9FF\">            <\/span><span style=\"color: #81A1C1\">while<\/span><span style=\"color: #D8DEE9FF\"> <\/span><span style=\"color: #88C0D0\">True:<\/span><\/span>\n<span class=\"line\"><span style=\"color: #D8DEE9FF\">                <\/span><span style=\"color: #88C0D0\">await<\/span><span style=\"color: #D8DEE9FF\"> <\/span><span style=\"color: #A3BE8C\">asyncio.sleep<\/span><span style=\"color: #ECEFF4\">(<\/span><span style=\"color: #88C0D0\">30<\/span><span style=\"color: #ECEFF4\">)<\/span><\/span>\n<span class=\"line\"><span style=\"color: #D8DEE9FF\">                <\/span><span style=\"color: #88C0D0\">await<\/span><span style=\"color: #D8DEE9FF\"> <\/span><span style=\"color: #A3BE8C\">ws.send<\/span><span style=\"color: #ECEFF4\">(<\/span><span style=\"color: #88C0D0\">json.dumps(<\/span><span style=\"color: #D8DEE9FF\">{&quot;<\/span><span style=\"color: #88C0D0\">code<\/span><span style=\"color: #88C0D0\">&quot;: 10010, &quot;<\/span><span style=\"color: #88C0D0\">trace<\/span><span style=\"color: #88C0D0\">&quot;: uuid.uuid4().hex}))<\/span><\/span>\n<span class=\"line\"><\/span>\n<span class=\"line\"><span style=\"color: #88C0D0\">        asyncio.create_task(heartbeat())<\/span><\/span>\n<span class=\"line\"><\/span>\n<span class=\"line\"><span style=\"color: #88C0D0\">        async for raw in ws:<\/span><\/span>\n<span class=\"line\"><span style=\"color: #88C0D0\">            msg = json.loads(raw)<\/span><\/span>\n<span class=\"line\"><span style=\"color: #88C0D0\">            if msg.get(&quot;<\/span><span style=\"color: #88C0D0\">code<\/span><span style=\"color: #88C0D0\">&quot;) == 10022:                 # news push<\/span><\/span>\n<span class=\"line\"><span style=\"color: #88C0D0\">                n = msg&#91;&quot;<\/span><span style=\"color: #88C0D0\">data<\/span><span style=\"color: #88C0D0\">&quot;&#93;<\/span><\/span>\n<span class=\"line\"><span style=\"color: #88C0D0\">                syms = &quot;<\/span><span style=\"color: #88C0D0\">,<\/span><span style=\"color: #D8DEE9FF\"> <\/span><span style=\"color: #ECEFF4\">&quot;<\/span><span style=\"color: #A3BE8C\">.join(n.get(<\/span><span style=\"color: #ECEFF4\">&quot;<\/span><span style=\"color: #A3BE8C\">symbols<\/span><span style=\"color: #ECEFF4\">&quot;<\/span><span style=\"color: #A3BE8C\">) or []) or <\/span><span style=\"color: #ECEFF4\">&quot;<\/span><span style=\"color: #A3BE8C\">\u2014<\/span><span style=\"color: #ECEFF4\">&quot;<\/span><\/span>\n<span class=\"line\"><span style=\"color: #A3BE8C\">                print(f<\/span><span style=\"color: #ECEFF4\">&quot;<\/span><span style=\"color: #A3BE8C\">[{n&#91;<\/span><span style=\"color: #ECEFF4\">&#39;<\/span><span style=\"color: #A3BE8C\">urgency<\/span><span style=\"color: #ECEFF4\">&#39;<\/span><span style=\"color: #A3BE8C\">&#93;}]<\/span><span style=\"color: #D8DEE9FF\"> <\/span><span style=\"color: #A3BE8C\">{n&#91;<\/span><span style=\"color: #ECEFF4\">&#39;<\/span><span style=\"color: #A3BE8C\">provider<\/span><span style=\"color: #ECEFF4\">&#39;<\/span><span style=\"color: #A3BE8C\">&#93;:<\/span><span style=\"color: #81A1C1\">&lt;<\/span><span style=\"color: #A3BE8C\">16}<\/span><span style=\"color: #D8DEE9FF\"> <\/span><span style=\"color: #A3BE8C\">{n&#91;<\/span><span style=\"color: #ECEFF4\">&#39;<\/span><span style=\"color: #A3BE8C\">title<\/span><span style=\"color: #ECEFF4\">&#39;<\/span><span style=\"color: #A3BE8C\">&#93;}<\/span><span style=\"color: #ECEFF4\">&quot;<\/span><span style=\"color: #A3BE8C\">)<\/span><\/span>\n<span class=\"line\"><span style=\"color: #A3BE8C\">                print(f<\/span><span style=\"color: #ECEFF4\">&quot;<\/span><span style=\"color: #D8DEE9FF\">      <\/span><span style=\"color: #A3BE8C\">symbols:<\/span><span style=\"color: #D8DEE9FF\"> <\/span><span style=\"color: #A3BE8C\">{syms}<\/span><span style=\"color: #ECEFF4\">&quot;<\/span><span style=\"color: #A3BE8C\">)<\/span><\/span>\n<span class=\"line\"><\/span>\n<span class=\"line\"><span style=\"color: #A3BE8C\">asyncio.run(main())<\/span><\/span><\/code><\/pre><\/div>\n\n\n\n<p class=\"wp-block-paragraph\">Run it during market hours and you&#8217;ll start seeing lines like:<\/p>\n\n\n\n<div class=\"wp-block-kevinbatdorf-code-block-pro\" data-code-block-pro-font-family=\"Code-Pro-JetBrains-Mono\" style=\"font-size:.875rem;font-family:Code-Pro-JetBrains-Mono,ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;line-height:1.25rem;--cbp-tab-width:2;tab-size:var(--cbp-tab-width, 2)\"><span style=\"display:block;padding:16px 0 0 16px;margin-bottom:-1px;width:100%;text-align:left;background-color:#2e3440ff\"><svg xmlns=\"http:\/\/www.w3.org\/2000\/svg\" width=\"54\" height=\"14\" viewBox=\"0 0 54 14\"><g fill=\"none\" fill-rule=\"evenodd\" transform=\"translate(1 1)\"><circle cx=\"6\" cy=\"6\" r=\"6\" fill=\"#FF5F56\" stroke=\"#E0443E\" stroke-width=\".5\"><\/circle><circle cx=\"26\" cy=\"6\" r=\"6\" fill=\"#FFBD2E\" stroke=\"#DEA123\" stroke-width=\".5\"><\/circle><circle cx=\"46\" cy=\"6\" r=\"6\" fill=\"#27C93F\" stroke=\"#1AAB29\" stroke-width=\".5\"><\/circle><\/g><\/svg><\/span><span role=\"button\" tabindex=\"0\" style=\"color:#d8dee9ff;display:none\" aria-label=\"Copy\" class=\"code-block-pro-copy-button\"><pre class=\"code-block-pro-copy-button-pre\" aria-hidden=\"true\"><textarea class=\"code-block-pro-copy-button-textarea\" tabindex=\"-1\" aria-hidden=\"true\" readonly>&#91;2&#93; dow-jones        Global Server Shipments to Hit Record 5 Million Units in 3Q\n      symbols: DELL.US, SMCI.US, HPE.US\n&#91;3&#93; reuters          Shein seeks $30-$40 billion valuation for August Hong Kong IPO\n      symbols: \u2014<\/textarea><\/pre><svg xmlns=\"http:\/\/www.w3.org\/2000\/svg\" style=\"width:24px;height:24px\" fill=\"none\" viewBox=\"0 0 24 24\" stroke=\"currentColor\" stroke-width=\"2\"><path class=\"with-check\" stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2m-6 9l2 2 4-4\"><\/path><path class=\"without-check\" stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2\"><\/path><\/svg><\/span><pre class=\"shiki nord\" style=\"background-color: #2e3440ff\" tabindex=\"0\"><code><span class=\"line\"><span style=\"color: #ECEFF4\">&#91;<\/span><span style=\"color: #B48EAD\">2<\/span><span style=\"color: #ECEFF4\">&#93;<\/span><span style=\"color: #D8DEE9FF\"> dow-jones        Global Server Shipments to Hit Record 5 Million Units <\/span><span style=\"color: #81A1C1\">in<\/span><span style=\"color: #D8DEE9FF\"> 3Q<\/span><\/span>\n<span class=\"line\"><span style=\"color: #D8DEE9FF\">      <\/span><span style=\"color: #88C0D0\">symbols:<\/span><span style=\"color: #D8DEE9FF\"> <\/span><span style=\"color: #A3BE8C\">DELL.US,<\/span><span style=\"color: #D8DEE9FF\"> <\/span><span style=\"color: #A3BE8C\">SMCI.US,<\/span><span style=\"color: #D8DEE9FF\"> <\/span><span style=\"color: #A3BE8C\">HPE.US<\/span><\/span>\n<span class=\"line\"><span style=\"color: #ECEFF4\">&#91;<\/span><span style=\"color: #B48EAD\">3<\/span><span style=\"color: #ECEFF4\">&#93;<\/span><span style=\"color: #D8DEE9FF\"> reuters          Shein seeks <\/span><span style=\"color: #D8DEE9\">$30<\/span><span style=\"color: #D8DEE9FF\">-<\/span><span style=\"color: #D8DEE9\">$40<\/span><span style=\"color: #D8DEE9FF\"> billion valuation <\/span><span style=\"color: #81A1C1\">for<\/span><span style=\"color: #D8DEE9FF\"> August Hong Kong IPO<\/span><\/span>\n<span class=\"line\"><span style=\"color: #D8DEE9FF\">      <\/span><span style=\"color: #88C0D0\">symbols:<\/span><span style=\"color: #D8DEE9FF\"> <\/span><span style=\"color: #A3BE8C\">\u2014<\/span><\/span><\/code><\/pre><\/div>\n\n\n\n<p class=\"wp-block-paragraph\">That&#8217;s the whole idea in 30 lines. The rest of this guide is about making it robust and putting the structured fields to work.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">3. The protocol, step by step<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">The news feed is a small, purpose-built protocol \u2014 five message types, one subscription dimension.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">3.1 Connection<\/h3>\n\n\n\n<div class=\"wp-block-kevinbatdorf-code-block-pro\" data-code-block-pro-font-family=\"Code-Pro-JetBrains-Mono\" style=\"font-size:.875rem;font-family:Code-Pro-JetBrains-Mono,ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;line-height:1.25rem;--cbp-tab-width:2;tab-size:var(--cbp-tab-width, 2)\"><span style=\"display:block;padding:16px 0 0 16px;margin-bottom:-1px;width:100%;text-align:left;background-color:#2e3440ff\"><svg xmlns=\"http:\/\/www.w3.org\/2000\/svg\" width=\"54\" height=\"14\" viewBox=\"0 0 54 14\"><g fill=\"none\" fill-rule=\"evenodd\" transform=\"translate(1 1)\"><circle cx=\"6\" cy=\"6\" r=\"6\" fill=\"#FF5F56\" stroke=\"#E0443E\" stroke-width=\".5\"><\/circle><circle cx=\"26\" cy=\"6\" r=\"6\" fill=\"#FFBD2E\" stroke=\"#DEA123\" stroke-width=\".5\"><\/circle><circle cx=\"46\" cy=\"6\" r=\"6\" fill=\"#27C93F\" stroke=\"#1AAB29\" stroke-width=\".5\"><\/circle><\/g><\/svg><\/span><span role=\"button\" tabindex=\"0\" style=\"color:#d8dee9ff;display:none\" aria-label=\"Copy\" class=\"code-block-pro-copy-button\"><pre class=\"code-block-pro-copy-button-pre\" aria-hidden=\"true\"><textarea class=\"code-block-pro-copy-button-textarea\" tabindex=\"-1\" aria-hidden=\"true\" readonly>wss:\/\/data.infoway.io\/news?apikey=YOUR_API_KEY<\/textarea><\/pre><svg xmlns=\"http:\/\/www.w3.org\/2000\/svg\" style=\"width:24px;height:24px\" fill=\"none\" viewBox=\"0 0 24 24\" stroke=\"currentColor\" stroke-width=\"2\"><path class=\"with-check\" stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2m-6 9l2 2 4-4\"><\/path><path class=\"without-check\" stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2\"><\/path><\/svg><\/span><pre class=\"shiki nord\" style=\"background-color: #2e3440ff\" tabindex=\"0\"><code><span class=\"line\"><span style=\"color: #88C0D0\">wss:\/\/data.infoway.io\/news?apikey<\/span><span style=\"color: #D8DEE9FF\">=YOUR_API_KEY<\/span><\/span><\/code><\/pre><\/div>\n\n\n\n<p class=\"wp-block-paragraph\">The path is <code>\/news<\/code> (not the <code>\/ws<\/code> path used for market data), and the key goes in the <code>apikey<\/code> query parameter. On a successful handshake the server sends:<\/p>\n\n\n\n<div class=\"wp-block-kevinbatdorf-code-block-pro\" data-code-block-pro-font-family=\"Code-Pro-JetBrains-Mono\" style=\"font-size:.875rem;font-family:Code-Pro-JetBrains-Mono,ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;line-height:1.25rem;--cbp-tab-width:2;tab-size:var(--cbp-tab-width, 2)\"><span style=\"display:block;padding:16px 0 0 16px;margin-bottom:-1px;width:100%;text-align:left;background-color:#2e3440ff\"><svg xmlns=\"http:\/\/www.w3.org\/2000\/svg\" width=\"54\" height=\"14\" viewBox=\"0 0 54 14\"><g fill=\"none\" fill-rule=\"evenodd\" transform=\"translate(1 1)\"><circle cx=\"6\" cy=\"6\" r=\"6\" fill=\"#FF5F56\" stroke=\"#E0443E\" stroke-width=\".5\"><\/circle><circle cx=\"26\" cy=\"6\" r=\"6\" fill=\"#FFBD2E\" stroke=\"#DEA123\" stroke-width=\".5\"><\/circle><circle cx=\"46\" cy=\"6\" r=\"6\" fill=\"#27C93F\" stroke=\"#1AAB29\" stroke-width=\".5\"><\/circle><\/g><\/svg><\/span><span role=\"button\" tabindex=\"0\" style=\"color:#d8dee9ff;display:none\" aria-label=\"Copy\" class=\"code-block-pro-copy-button\"><pre class=\"code-block-pro-copy-button-pre\" aria-hidden=\"true\"><textarea class=\"code-block-pro-copy-button-textarea\" tabindex=\"-1\" aria-hidden=\"true\" readonly>{ \"code\": 200, \"msg\": \"ws connect success\" }<\/textarea><\/pre><svg xmlns=\"http:\/\/www.w3.org\/2000\/svg\" style=\"width:24px;height:24px\" fill=\"none\" viewBox=\"0 0 24 24\" stroke=\"currentColor\" stroke-width=\"2\"><path class=\"with-check\" stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2m-6 9l2 2 4-4\"><\/path><path class=\"without-check\" stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2\"><\/path><\/svg><\/span><pre class=\"shiki nord\" style=\"background-color: #2e3440ff\" tabindex=\"0\"><code><span class=\"line\"><span style=\"color: #ECEFF4\">{<\/span><span style=\"color: #D8DEE9FF\"> <\/span><span style=\"color: #88C0D0\">&quot;code&quot;<\/span><span style=\"color: #88C0D0\">:<\/span><span style=\"color: #D8DEE9FF\"> <\/span><span style=\"color: #B48EAD\">200<\/span><span style=\"color: #A3BE8C\">,<\/span><span style=\"color: #D8DEE9FF\"> <\/span><span style=\"color: #ECEFF4\">&quot;<\/span><span style=\"color: #A3BE8C\">msg<\/span><span style=\"color: #ECEFF4\">&quot;<\/span><span style=\"color: #A3BE8C\">:<\/span><span style=\"color: #D8DEE9FF\"> <\/span><span style=\"color: #ECEFF4\">&quot;<\/span><span style=\"color: #A3BE8C\">ws connect success<\/span><span style=\"color: #ECEFF4\">&quot;<\/span><span style=\"color: #D8DEE9FF\"> <\/span><span style=\"color: #A3BE8C\">}<\/span><\/span><\/code><\/pre><\/div>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>One connection per API key.<\/strong> If a connection already exists for your key, the new one is rejected. To consume more than one language at once, use more than one key (see <a href=\"#elementor-toc__heading-anchor-6\">recipe 5.5<\/a>).<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">3.2 Subscribe (<code>10020<\/code>)<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Send one subscribe message after the connection opens:<\/p>\n\n\n\n<div class=\"wp-block-kevinbatdorf-code-block-pro\" data-code-block-pro-font-family=\"Code-Pro-JetBrains-Mono\" style=\"font-size:.875rem;font-family:Code-Pro-JetBrains-Mono,ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;line-height:1.25rem;--cbp-tab-width:2;tab-size:var(--cbp-tab-width, 2)\"><span style=\"display:block;padding:16px 0 0 16px;margin-bottom:-1px;width:100%;text-align:left;background-color:#2e3440ff\"><svg xmlns=\"http:\/\/www.w3.org\/2000\/svg\" width=\"54\" height=\"14\" viewBox=\"0 0 54 14\"><g fill=\"none\" fill-rule=\"evenodd\" transform=\"translate(1 1)\"><circle cx=\"6\" cy=\"6\" r=\"6\" fill=\"#FF5F56\" stroke=\"#E0443E\" stroke-width=\".5\"><\/circle><circle cx=\"26\" cy=\"6\" r=\"6\" fill=\"#FFBD2E\" stroke=\"#DEA123\" stroke-width=\".5\"><\/circle><circle cx=\"46\" cy=\"6\" r=\"6\" fill=\"#27C93F\" stroke=\"#1AAB29\" stroke-width=\".5\"><\/circle><\/g><\/svg><\/span><span role=\"button\" tabindex=\"0\" style=\"color:#d8dee9ff;display:none\" aria-label=\"Copy\" class=\"code-block-pro-copy-button\"><pre class=\"code-block-pro-copy-button-pre\" aria-hidden=\"true\"><textarea class=\"code-block-pro-copy-button-textarea\" tabindex=\"-1\" aria-hidden=\"true\" readonly>{\n    \"code\": 10020,\n    \"trace\": \"5dd7e89cdcc247d78e817b67269167e0\",\n    \"data\": { \"lang\": \"en\" }\n}<\/textarea><\/pre><svg xmlns=\"http:\/\/www.w3.org\/2000\/svg\" style=\"width:24px;height:24px\" fill=\"none\" viewBox=\"0 0 24 24\" stroke=\"currentColor\" stroke-width=\"2\"><path class=\"with-check\" stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2m-6 9l2 2 4-4\"><\/path><path class=\"without-check\" stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2\"><\/path><\/svg><\/span><pre class=\"shiki nord\" style=\"background-color: #2e3440ff\" tabindex=\"0\"><code><span class=\"line\"><span style=\"color: #ECEFF4\">{<\/span><\/span>\n<span class=\"line\"><span style=\"color: #D8DEE9FF\">    <\/span><span style=\"color: #88C0D0\">&quot;code&quot;<\/span><span style=\"color: #88C0D0\">:<\/span><span style=\"color: #D8DEE9FF\"> <\/span><span style=\"color: #B48EAD\">10020<\/span><span style=\"color: #A3BE8C\">,<\/span><\/span>\n<span class=\"line\"><span style=\"color: #D8DEE9FF\">    <\/span><span style=\"color: #88C0D0\">&quot;trace&quot;<\/span><span style=\"color: #88C0D0\">:<\/span><span style=\"color: #D8DEE9FF\"> <\/span><span style=\"color: #ECEFF4\">&quot;<\/span><span style=\"color: #A3BE8C\">5dd7e89cdcc247d78e817b67269167e0<\/span><span style=\"color: #ECEFF4\">&quot;<\/span><span style=\"color: #A3BE8C\">,<\/span><\/span>\n<span class=\"line\"><span style=\"color: #D8DEE9FF\">    <\/span><span style=\"color: #88C0D0\">&quot;data&quot;<\/span><span style=\"color: #88C0D0\">:<\/span><span style=\"color: #D8DEE9FF\"> <\/span><span style=\"color: #A3BE8C\">{<\/span><span style=\"color: #D8DEE9FF\"> <\/span><span style=\"color: #ECEFF4\">&quot;<\/span><span style=\"color: #A3BE8C\">lang<\/span><span style=\"color: #ECEFF4\">&quot;<\/span><span style=\"color: #A3BE8C\">:<\/span><span style=\"color: #D8DEE9FF\"> <\/span><span style=\"color: #ECEFF4\">&quot;<\/span><span style=\"color: #A3BE8C\">en<\/span><span style=\"color: #ECEFF4\">&quot;<\/span><span style=\"color: #D8DEE9FF\"> <\/span><span style=\"color: #A3BE8C\">}<\/span><\/span>\n<span class=\"line\"><span style=\"color: #ECEFF4\">}<\/span><\/span><\/code><\/pre><\/div>\n\n\n\n<figure class=\"wp-block-table\"><table class=\"has-fixed-layout\"><thead><tr><th>Field<\/th><th>Type<\/th><th>Notes<\/th><\/tr><\/thead><tbody><tr><td><code>code<\/code><\/td><td>Integer<\/td><td>Always <code>10020<\/code> for a news subscription<\/td><\/tr><tr><td><code>trace<\/code><\/td><td>String<\/td><td>Any random ID; echoed back so you can match request to response<\/td><\/tr><tr><td><code>data.lang<\/code><\/td><td>String<\/td><td>Language channel \u2014 <strong>required<\/strong>, case-insensitive (normalized to lowercase server-side)<\/td><\/tr><\/tbody><\/table><\/figure>\n\n\n\n<p class=\"wp-block-paragraph\">Supported <code>lang<\/code> values:<\/p>\n\n\n\n<figure class=\"wp-block-table\"><table class=\"has-fixed-layout\"><thead><tr><th>Code<\/th><th>Language<\/th><th>Code<\/th><th>Language<\/th><\/tr><\/thead><tbody><tr><td><code>en<\/code><\/td><td>English (global)<\/td><td><code>pt<\/code><\/td><td>Portuguese<\/td><\/tr><tr><td><code>zh-Hans<\/code><\/td><td>Simplified Chinese<\/td><td><code>ru<\/code><\/td><td>Russian<\/td><\/tr><tr><td><code>zh-Hant<\/code><\/td><td>Traditional Chinese<\/td><td><code>de<\/code><\/td><td>German<\/td><\/tr><tr><td><code>ja<\/code><\/td><td>Japanese<\/td><td><code>fr<\/code><\/td><td>French<\/td><\/tr><tr><td><code>ko<\/code><\/td><td>Korean<\/td><td><code>es<\/code><\/td><td>Spanish<\/td><\/tr><tr><td><code>tr<\/code><\/td><td>Turkish<\/td><td><\/td><td><\/td><\/tr><\/tbody><\/table><\/figure>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>One subscription per connection.<\/strong> There is no separate &#8220;unsubscribe&#8221; command \u2014 sending <code>10020<\/code> again with a different <code>lang<\/code> replaces the current subscription.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">3.3 Subscribe acknowledgement (<code>10021<\/code>)<\/h3>\n\n\n\n<div class=\"wp-block-kevinbatdorf-code-block-pro\" data-code-block-pro-font-family=\"Code-Pro-JetBrains-Mono\" style=\"font-size:.875rem;font-family:Code-Pro-JetBrains-Mono,ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;line-height:1.25rem;--cbp-tab-width:2;tab-size:var(--cbp-tab-width, 2)\"><span style=\"display:block;padding:16px 0 0 16px;margin-bottom:-1px;width:100%;text-align:left;background-color:#2e3440ff\"><svg xmlns=\"http:\/\/www.w3.org\/2000\/svg\" width=\"54\" height=\"14\" viewBox=\"0 0 54 14\"><g fill=\"none\" fill-rule=\"evenodd\" transform=\"translate(1 1)\"><circle cx=\"6\" cy=\"6\" r=\"6\" fill=\"#FF5F56\" stroke=\"#E0443E\" stroke-width=\".5\"><\/circle><circle cx=\"26\" cy=\"6\" r=\"6\" fill=\"#FFBD2E\" stroke=\"#DEA123\" stroke-width=\".5\"><\/circle><circle cx=\"46\" cy=\"6\" r=\"6\" fill=\"#27C93F\" stroke=\"#1AAB29\" stroke-width=\".5\"><\/circle><\/g><\/svg><\/span><span role=\"button\" tabindex=\"0\" style=\"color:#d8dee9ff;display:none\" aria-label=\"Copy\" class=\"code-block-pro-copy-button\"><pre class=\"code-block-pro-copy-button-pre\" aria-hidden=\"true\"><textarea class=\"code-block-pro-copy-button-textarea\" tabindex=\"-1\" aria-hidden=\"true\" readonly>{\n    \"code\": 10021,\n    \"trace\": \"5dd7e89cdcc247d78e817b67269167e0\",\n    \"msg\": \"ok\",\n    \"data\": { \"lang\": \"en\" }\n}<\/textarea><\/pre><svg xmlns=\"http:\/\/www.w3.org\/2000\/svg\" style=\"width:24px;height:24px\" fill=\"none\" viewBox=\"0 0 24 24\" stroke=\"currentColor\" stroke-width=\"2\"><path class=\"with-check\" stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2m-6 9l2 2 4-4\"><\/path><path class=\"without-check\" stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2\"><\/path><\/svg><\/span><pre class=\"shiki nord\" style=\"background-color: #2e3440ff\" tabindex=\"0\"><code><span class=\"line\"><span style=\"color: #ECEFF4\">{<\/span><\/span>\n<span class=\"line\"><span style=\"color: #D8DEE9FF\">    <\/span><span style=\"color: #88C0D0\">&quot;code&quot;<\/span><span style=\"color: #88C0D0\">:<\/span><span style=\"color: #D8DEE9FF\"> <\/span><span style=\"color: #B48EAD\">10021<\/span><span style=\"color: #A3BE8C\">,<\/span><\/span>\n<span class=\"line\"><span style=\"color: #D8DEE9FF\">    <\/span><span style=\"color: #88C0D0\">&quot;trace&quot;<\/span><span style=\"color: #88C0D0\">:<\/span><span style=\"color: #D8DEE9FF\"> <\/span><span style=\"color: #ECEFF4\">&quot;<\/span><span style=\"color: #A3BE8C\">5dd7e89cdcc247d78e817b67269167e0<\/span><span style=\"color: #ECEFF4\">&quot;<\/span><span style=\"color: #A3BE8C\">,<\/span><\/span>\n<span class=\"line\"><span style=\"color: #D8DEE9FF\">    <\/span><span style=\"color: #88C0D0\">&quot;msg&quot;<\/span><span style=\"color: #88C0D0\">:<\/span><span style=\"color: #D8DEE9FF\"> <\/span><span style=\"color: #ECEFF4\">&quot;<\/span><span style=\"color: #A3BE8C\">ok<\/span><span style=\"color: #ECEFF4\">&quot;<\/span><span style=\"color: #A3BE8C\">,<\/span><\/span>\n<span class=\"line\"><span style=\"color: #D8DEE9FF\">    <\/span><span style=\"color: #88C0D0\">&quot;data&quot;<\/span><span style=\"color: #88C0D0\">:<\/span><span style=\"color: #D8DEE9FF\"> <\/span><span style=\"color: #A3BE8C\">{<\/span><span style=\"color: #D8DEE9FF\"> <\/span><span style=\"color: #ECEFF4\">&quot;<\/span><span style=\"color: #A3BE8C\">lang<\/span><span style=\"color: #ECEFF4\">&quot;<\/span><span style=\"color: #A3BE8C\">:<\/span><span style=\"color: #D8DEE9FF\"> <\/span><span style=\"color: #ECEFF4\">&quot;<\/span><span style=\"color: #A3BE8C\">en<\/span><span style=\"color: #ECEFF4\">&quot;<\/span><span style=\"color: #D8DEE9FF\"> <\/span><span style=\"color: #A3BE8C\">}<\/span><\/span>\n<span class=\"line\"><span style=\"color: #ECEFF4\">}<\/span><\/span><\/code><\/pre><\/div>\n\n\n\n<p class=\"wp-block-paragraph\">The <code>trace<\/code> matches what you sent; <code>data.lang<\/code> comes back lowercased. After this, news pushes flow.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">3.4 News push (<code>10022<\/code>)<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">The payload your client exists to handle. Full field reference in <a href=\"#elementor-toc__heading-anchor-5\">section 4<\/a>.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">3.5 Heartbeat (<code>10010<\/code>)<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Send a heartbeat <strong>every ~30 seconds<\/strong>:<\/p>\n\n\n\n<div class=\"wp-block-kevinbatdorf-code-block-pro\" data-code-block-pro-font-family=\"Code-Pro-JetBrains-Mono\" style=\"font-size:.875rem;font-family:Code-Pro-JetBrains-Mono,ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;line-height:1.25rem;--cbp-tab-width:2;tab-size:var(--cbp-tab-width, 2)\"><span style=\"display:block;padding:16px 0 0 16px;margin-bottom:-1px;width:100%;text-align:left;background-color:#2e3440ff\"><svg xmlns=\"http:\/\/www.w3.org\/2000\/svg\" width=\"54\" height=\"14\" viewBox=\"0 0 54 14\"><g fill=\"none\" fill-rule=\"evenodd\" transform=\"translate(1 1)\"><circle cx=\"6\" cy=\"6\" r=\"6\" fill=\"#FF5F56\" stroke=\"#E0443E\" stroke-width=\".5\"><\/circle><circle cx=\"26\" cy=\"6\" r=\"6\" fill=\"#FFBD2E\" stroke=\"#DEA123\" stroke-width=\".5\"><\/circle><circle cx=\"46\" cy=\"6\" r=\"6\" fill=\"#27C93F\" stroke=\"#1AAB29\" stroke-width=\".5\"><\/circle><\/g><\/svg><\/span><span role=\"button\" tabindex=\"0\" style=\"color:#d8dee9ff;display:none\" aria-label=\"Copy\" class=\"code-block-pro-copy-button\"><pre class=\"code-block-pro-copy-button-pre\" aria-hidden=\"true\"><textarea class=\"code-block-pro-copy-button-textarea\" tabindex=\"-1\" aria-hidden=\"true\" readonly>{ \"code\": 10010, \"trace\": \"...\" }<\/textarea><\/pre><svg xmlns=\"http:\/\/www.w3.org\/2000\/svg\" style=\"width:24px;height:24px\" fill=\"none\" viewBox=\"0 0 24 24\" stroke=\"currentColor\" stroke-width=\"2\"><path class=\"with-check\" stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2m-6 9l2 2 4-4\"><\/path><path class=\"without-check\" stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2\"><\/path><\/svg><\/span><pre class=\"shiki nord\" style=\"background-color: #2e3440ff\" tabindex=\"0\"><code><span class=\"line\"><span style=\"color: #ECEFF4\">{<\/span><span style=\"color: #D8DEE9FF\"> <\/span><span style=\"color: #88C0D0\">&quot;code&quot;<\/span><span style=\"color: #88C0D0\">:<\/span><span style=\"color: #D8DEE9FF\"> <\/span><span style=\"color: #B48EAD\">10010<\/span><span style=\"color: #A3BE8C\">,<\/span><span style=\"color: #D8DEE9FF\"> <\/span><span style=\"color: #ECEFF4\">&quot;<\/span><span style=\"color: #A3BE8C\">trace<\/span><span style=\"color: #ECEFF4\">&quot;<\/span><span style=\"color: #A3BE8C\">:<\/span><span style=\"color: #D8DEE9FF\"> <\/span><span style=\"color: #ECEFF4\">&quot;<\/span><span style=\"color: #A3BE8C\">...<\/span><span style=\"color: #ECEFF4\">&quot;<\/span><span style=\"color: #D8DEE9FF\"> <\/span><span style=\"color: #A3BE8C\">}<\/span><\/span><\/code><\/pre><\/div>\n\n\n\n<p class=\"wp-block-paragraph\">If the server receives no heartbeat for <strong>60 seconds<\/strong>, neither an application <code>10010<\/code> nor a WebSocket-level ping, it closes the connection. A standard WebSocket ping\/pong keeps the connection alive too, but sending the explicit <code>10010<\/code> is the simplest thing that always works.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Protocol summary<\/h3>\n\n\n\n<figure class=\"wp-block-table\"><table class=\"has-fixed-layout\"><thead><tr><th>Code<\/th><th>Direction<\/th><th>Meaning<\/th><\/tr><\/thead><tbody><tr><td><code>200<\/code><\/td><td>server \u2192 client<\/td><td>Handshake succeeded<\/td><\/tr><tr><td><code>10020<\/code><\/td><td>client \u2192 server<\/td><td>Subscribe to a language channel<\/td><\/tr><tr><td><code>10021<\/code><\/td><td>server \u2192 client<\/td><td>Subscription confirmed<\/td><\/tr><tr><td><code>10022<\/code><\/td><td>server \u2192 client<\/td><td>News push<\/td><\/tr><tr><td><code>10010<\/code><\/td><td>client \u2194 server<\/td><td>Heartbeat \u2014 send every 30s<\/td><\/tr><\/tbody><\/table><\/figure>\n\n\n\n<h2 class=\"wp-block-heading\">4. Anatomy of a news push<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">A real push (body truncated for display):<\/p>\n\n\n\n<div class=\"wp-block-kevinbatdorf-code-block-pro\" data-code-block-pro-font-family=\"Code-Pro-JetBrains-Mono\" style=\"font-size:.875rem;font-family:Code-Pro-JetBrains-Mono,ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;line-height:1.25rem;--cbp-tab-width:2;tab-size:var(--cbp-tab-width, 2)\"><span style=\"display:block;padding:16px 0 0 16px;margin-bottom:-1px;width:100%;text-align:left;background-color:#2e3440ff\"><svg xmlns=\"http:\/\/www.w3.org\/2000\/svg\" width=\"54\" height=\"14\" viewBox=\"0 0 54 14\"><g fill=\"none\" fill-rule=\"evenodd\" transform=\"translate(1 1)\"><circle cx=\"6\" cy=\"6\" r=\"6\" fill=\"#FF5F56\" stroke=\"#E0443E\" stroke-width=\".5\"><\/circle><circle cx=\"26\" cy=\"6\" r=\"6\" fill=\"#FFBD2E\" stroke=\"#DEA123\" stroke-width=\".5\"><\/circle><circle cx=\"46\" cy=\"6\" r=\"6\" fill=\"#27C93F\" stroke=\"#1AAB29\" stroke-width=\".5\"><\/circle><\/g><\/svg><\/span><span role=\"button\" tabindex=\"0\" style=\"color:#d8dee9ff;display:none\" aria-label=\"Copy\" class=\"code-block-pro-copy-button\"><pre class=\"code-block-pro-copy-button-pre\" aria-hidden=\"true\"><textarea class=\"code-block-pro-copy-button-textarea\" tabindex=\"-1\" aria-hidden=\"true\" readonly>{\n    \"code\": 10022,\n    \"data\": {\n        \"dk\": \"e9ecbfbc6799c39d5ec09c8c771ac0fe\",\n        \"country\": \"CN\",\n        \"lang\": \"zh-Hans\",\n        \"route\": \"lang\",\n        \"title\": \"China bond market: easing liquidity lifts short-end, tech rally caps long bonds\",\n        \"published\": 1785815048,\n        \"urgency\": 2,\n        \"provider\": \"reuters\",\n        \"symbols\": &#91;\"399006.SZ\", \"000688.SH\"&#93;,\n        \"link\": \"\",\n        \"content\": \"China's bond market was mixed on Tuesday, with short-dated notes firmer as funding conditions loosened ...\",\n        \"sd\": \"China's bond market was mixed on Tuesday, with short-dated notes firmer ...\"\n    }\n}<\/textarea><\/pre><svg xmlns=\"http:\/\/www.w3.org\/2000\/svg\" style=\"width:24px;height:24px\" fill=\"none\" viewBox=\"0 0 24 24\" stroke=\"currentColor\" stroke-width=\"2\"><path class=\"with-check\" stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2m-6 9l2 2 4-4\"><\/path><path class=\"without-check\" stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2\"><\/path><\/svg><\/span><pre class=\"shiki nord\" style=\"background-color: #2e3440ff\" tabindex=\"0\"><code><span class=\"line\"><span style=\"color: #ECEFF4\">{<\/span><\/span>\n<span class=\"line\"><span style=\"color: #D8DEE9FF\">    <\/span><span style=\"color: #88C0D0\">&quot;code&quot;<\/span><span style=\"color: #88C0D0\">:<\/span><span style=\"color: #D8DEE9FF\"> <\/span><span style=\"color: #B48EAD\">10022<\/span><span style=\"color: #A3BE8C\">,<\/span><\/span>\n<span class=\"line\"><span style=\"color: #D8DEE9FF\">    <\/span><span style=\"color: #88C0D0\">&quot;data&quot;<\/span><span style=\"color: #88C0D0\">:<\/span><span style=\"color: #D8DEE9FF\"> <\/span><span style=\"color: #A3BE8C\">{<\/span><\/span>\n<span class=\"line\"><span style=\"color: #D8DEE9FF\">        <\/span><span style=\"color: #88C0D0\">&quot;dk&quot;<\/span><span style=\"color: #88C0D0\">:<\/span><span style=\"color: #D8DEE9FF\"> <\/span><span style=\"color: #ECEFF4\">&quot;<\/span><span style=\"color: #A3BE8C\">e9ecbfbc6799c39d5ec09c8c771ac0fe<\/span><span style=\"color: #ECEFF4\">&quot;<\/span><span style=\"color: #A3BE8C\">,<\/span><\/span>\n<span class=\"line\"><span style=\"color: #D8DEE9FF\">        <\/span><span style=\"color: #88C0D0\">&quot;country&quot;<\/span><span style=\"color: #88C0D0\">:<\/span><span style=\"color: #D8DEE9FF\"> <\/span><span style=\"color: #ECEFF4\">&quot;<\/span><span style=\"color: #A3BE8C\">CN<\/span><span style=\"color: #ECEFF4\">&quot;<\/span><span style=\"color: #A3BE8C\">,<\/span><\/span>\n<span class=\"line\"><span style=\"color: #D8DEE9FF\">        <\/span><span style=\"color: #88C0D0\">&quot;lang&quot;<\/span><span style=\"color: #88C0D0\">:<\/span><span style=\"color: #D8DEE9FF\"> <\/span><span style=\"color: #ECEFF4\">&quot;<\/span><span style=\"color: #A3BE8C\">zh-Hans<\/span><span style=\"color: #ECEFF4\">&quot;<\/span><span style=\"color: #A3BE8C\">,<\/span><\/span>\n<span class=\"line\"><span style=\"color: #D8DEE9FF\">        <\/span><span style=\"color: #88C0D0\">&quot;route&quot;<\/span><span style=\"color: #88C0D0\">:<\/span><span style=\"color: #D8DEE9FF\"> <\/span><span style=\"color: #ECEFF4\">&quot;<\/span><span style=\"color: #A3BE8C\">lang<\/span><span style=\"color: #ECEFF4\">&quot;<\/span><span style=\"color: #A3BE8C\">,<\/span><\/span>\n<span class=\"line\"><span style=\"color: #D8DEE9FF\">        <\/span><span style=\"color: #88C0D0\">&quot;title&quot;<\/span><span style=\"color: #88C0D0\">:<\/span><span style=\"color: #D8DEE9FF\"> <\/span><span style=\"color: #ECEFF4\">&quot;<\/span><span style=\"color: #A3BE8C\">China bond market: easing liquidity lifts short-end, tech rally caps long bonds<\/span><span style=\"color: #ECEFF4\">&quot;<\/span><span style=\"color: #A3BE8C\">,<\/span><\/span>\n<span class=\"line\"><span style=\"color: #D8DEE9FF\">        <\/span><span style=\"color: #88C0D0\">&quot;published&quot;<\/span><span style=\"color: #88C0D0\">:<\/span><span style=\"color: #D8DEE9FF\"> <\/span><span style=\"color: #B48EAD\">1785815048<\/span><span style=\"color: #A3BE8C\">,<\/span><\/span>\n<span class=\"line\"><span style=\"color: #D8DEE9FF\">        <\/span><span style=\"color: #88C0D0\">&quot;urgency&quot;<\/span><span style=\"color: #88C0D0\">:<\/span><span style=\"color: #D8DEE9FF\"> <\/span><span style=\"color: #B48EAD\">2<\/span><span style=\"color: #A3BE8C\">,<\/span><\/span>\n<span class=\"line\"><span style=\"color: #D8DEE9FF\">        <\/span><span style=\"color: #88C0D0\">&quot;provider&quot;<\/span><span style=\"color: #88C0D0\">:<\/span><span style=\"color: #D8DEE9FF\"> <\/span><span style=\"color: #ECEFF4\">&quot;<\/span><span style=\"color: #A3BE8C\">reuters<\/span><span style=\"color: #ECEFF4\">&quot;<\/span><span style=\"color: #A3BE8C\">,<\/span><\/span>\n<span class=\"line\"><span style=\"color: #D8DEE9FF\">        <\/span><span style=\"color: #88C0D0\">&quot;symbols&quot;<\/span><span style=\"color: #88C0D0\">:<\/span><span style=\"color: #D8DEE9FF\"> &#91;<\/span><span style=\"color: #ECEFF4\">&quot;<\/span><span style=\"color: #A3BE8C\">399006.SZ<\/span><span style=\"color: #ECEFF4\">&quot;<\/span><span style=\"color: #D8DEE9FF\">, <\/span><span style=\"color: #ECEFF4\">&quot;<\/span><span style=\"color: #A3BE8C\">000688.SH<\/span><span style=\"color: #ECEFF4\">&quot;<\/span><span style=\"color: #A3BE8C\">&#93;,<\/span><\/span>\n<span class=\"line\"><span style=\"color: #D8DEE9FF\">        <\/span><span style=\"color: #88C0D0\">&quot;link&quot;<\/span><span style=\"color: #88C0D0\">:<\/span><span style=\"color: #D8DEE9FF\"> <\/span><span style=\"color: #ECEFF4\">&quot;&quot;<\/span><span style=\"color: #A3BE8C\">,<\/span><\/span>\n<span class=\"line\"><span style=\"color: #D8DEE9FF\">        <\/span><span style=\"color: #88C0D0\">&quot;content&quot;<\/span><span style=\"color: #88C0D0\">:<\/span><span style=\"color: #D8DEE9FF\"> <\/span><span style=\"color: #ECEFF4\">&quot;<\/span><span style=\"color: #A3BE8C\">China&#39;s bond market was mixed on Tuesday, with short-dated notes firmer as funding conditions loosened ...<\/span><span style=\"color: #ECEFF4\">&quot;<\/span><span style=\"color: #A3BE8C\">,<\/span><\/span>\n<span class=\"line\"><span style=\"color: #D8DEE9FF\">        <\/span><span style=\"color: #88C0D0\">&quot;sd&quot;<\/span><span style=\"color: #88C0D0\">:<\/span><span style=\"color: #D8DEE9FF\"> <\/span><span style=\"color: #ECEFF4\">&quot;<\/span><span style=\"color: #A3BE8C\">China&#39;s bond market was mixed on Tuesday, with short-dated notes firmer ...<\/span><span style=\"color: #ECEFF4\">&quot;<\/span><\/span>\n<span class=\"line\"><span style=\"color: #D8DEE9FF\">    <\/span><span style=\"color: #ECEFF4\">}<\/span><\/span>\n<span class=\"line\"><span style=\"color: #D8DEE9FF\">}<\/span><\/span><\/code><\/pre><\/div>\n\n\n\n<figure class=\"wp-block-table\"><table class=\"has-fixed-layout\"><thead><tr><th>Field<\/th><th>Type<\/th><th>Always present<\/th><th>Description<\/th><\/tr><\/thead><tbody><tr><td><code>dk<\/code><\/td><td>String<\/td><td>Yes<\/td><td>Deduplication key \u2014 MD5 of title + body. Same event from a different source \u2192 same <code>dk<\/code><\/td><\/tr><tr><td><code>title<\/code><\/td><td>String<\/td><td>Yes<\/td><td>Headline<\/td><\/tr><tr><td><code>content<\/code><\/td><td>String<\/td><td>No<\/td><td>Full article body, plain text<\/td><\/tr><tr><td><code>sd<\/code><\/td><td>String<\/td><td>No<\/td><td>Short summary \/ standfirst<\/td><\/tr><tr><td><code>published<\/code><\/td><td>Long<\/td><td>Yes<\/td><td>Publish time, <strong>Unix seconds, UTC<\/strong><\/td><\/tr><tr><td><code>urgency<\/code><\/td><td>Integer<\/td><td>Yes<\/td><td>Priority \u2014 <strong>lower is more urgent<\/strong><\/td><\/tr><tr><td><code>provider<\/code><\/td><td>String<\/td><td>Yes<\/td><td>Source, e.g. <code>reuters<\/code>, <code>dow-jones<\/code>, <code>business_wire<\/code><\/td><\/tr><tr><td><code>symbols<\/code><\/td><td>Array<\/td><td>No<\/td><td>Associated instrument codes, e.g. <code>[\"DELL.US\", \"SMCI.US\"]<\/code><\/td><\/tr><tr><td><code>country<\/code><\/td><td>String<\/td><td>No<\/td><td>Country \/ region; may be empty for global news<\/td><\/tr><tr><td><code>lang<\/code><\/td><td>String<\/td><td>Yes<\/td><td>Language of the item<\/td><\/tr><tr><td><code>route<\/code><\/td><td>String<\/td><td>Yes<\/td><td>How it was collected: <code>lang<\/code> or <code>country<\/code><\/td><\/tr><tr><td><code>link<\/code><\/td><td>String<\/td><td>No<\/td><td>Original article URL (not always available)<\/td><\/tr><\/tbody><\/table><\/figure>\n\n\n\n<h3 class=\"wp-block-heading\">Three fields worth understanding<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\"><strong><code>symbols<\/code> \u2014 a ready-made news\u2192instrument mapping.<\/strong> The tickers aren&#8217;t just the ones that appear verbatim in the text; they include semantically related instruments. A story about an Indian refiner&#8217;s earnings came through tagged with <code>RELIANCE<\/code>, the <code>NIFTY<\/code> index, and several other oil names \u2014 nine symbols in total. That relationship is exactly what you&#8217;d otherwise build an entity-linking system to produce.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Note that symbols can arrive in a couple of conventions depending on the market: suffix form like <code>AAPL.US<\/code>, <code>005930.KS<\/code>, <code>399006.SZ<\/code>, and exchange-prefixed form like <code>LSE:HSBA<\/code> or <code>NSE:NIFTY<\/code>. Normalize to your own convention on ingest.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong><code>urgency<\/code> \u2014 a priority signal.<\/strong> Breaking wire flashes carry a lower number than routine analysis pieces. Use it to decide what interrupts a user and what just gets logged.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong><code>dk<\/code> \u2014 cross-source idempotency.<\/strong> In our own testing, every push carried a <code>dk<\/code> (100% coverage). Key your storage and your downstream fan-out on it and duplicate reports of the same event collapse to one.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">5. Recipes<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Each recipe is a small function you drop into the message handler. They&#8217;re independent \u2014 take the ones you need.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">5.1 Deduplicate across sources<\/h3>\n\n\n\n<div class=\"wp-block-kevinbatdorf-code-block-pro\" data-code-block-pro-font-family=\"Code-Pro-JetBrains-Mono\" style=\"font-size:.875rem;font-family:Code-Pro-JetBrains-Mono,ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;line-height:1.25rem;--cbp-tab-width:2;tab-size:var(--cbp-tab-width, 2)\"><span style=\"display:block;padding:16px 0 0 16px;margin-bottom:-1px;width:100%;text-align:left;background-color:#2e3440ff\"><svg xmlns=\"http:\/\/www.w3.org\/2000\/svg\" width=\"54\" height=\"14\" viewBox=\"0 0 54 14\"><g fill=\"none\" fill-rule=\"evenodd\" transform=\"translate(1 1)\"><circle cx=\"6\" cy=\"6\" r=\"6\" fill=\"#FF5F56\" stroke=\"#E0443E\" stroke-width=\".5\"><\/circle><circle cx=\"26\" cy=\"6\" r=\"6\" fill=\"#FFBD2E\" stroke=\"#DEA123\" stroke-width=\".5\"><\/circle><circle cx=\"46\" cy=\"6\" r=\"6\" fill=\"#27C93F\" stroke=\"#1AAB29\" stroke-width=\".5\"><\/circle><\/g><\/svg><\/span><span role=\"button\" tabindex=\"0\" style=\"color:#d8dee9ff;display:none\" aria-label=\"Copy\" class=\"code-block-pro-copy-button\"><pre class=\"code-block-pro-copy-button-pre\" aria-hidden=\"true\"><textarea class=\"code-block-pro-copy-button-textarea\" tabindex=\"-1\" aria-hidden=\"true\" readonly>_seen: set&#91;str&#93; = set()\n\ndef is_new(news: dict) -> bool:\n    dk = news.get(\"dk\")\n    if not dk or dk in _seen:\n        return False\n    _seen.add(dk)\n    return True<\/textarea><\/pre><svg xmlns=\"http:\/\/www.w3.org\/2000\/svg\" style=\"width:24px;height:24px\" fill=\"none\" viewBox=\"0 0 24 24\" stroke=\"currentColor\" stroke-width=\"2\"><path class=\"with-check\" stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2m-6 9l2 2 4-4\"><\/path><path class=\"without-check\" stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2\"><\/path><\/svg><\/span><pre class=\"shiki nord\" style=\"background-color: #2e3440ff\" tabindex=\"0\"><code><span class=\"line\"><span style=\"color: #88C0D0\">_seen:<\/span><span style=\"color: #D8DEE9FF\"> <\/span><span style=\"color: #A3BE8C\">set&#91;str&#93;<\/span><span style=\"color: #D8DEE9FF\"> <\/span><span style=\"color: #A3BE8C\">=<\/span><span style=\"color: #D8DEE9FF\"> <\/span><span style=\"color: #A3BE8C\">set<\/span><span style=\"color: #ECEFF4\">()<\/span><\/span>\n<span class=\"line\"><\/span>\n<span class=\"line\"><span style=\"color: #88C0D0\">def<\/span><span style=\"color: #D8DEE9FF\"> <\/span><span style=\"color: #A3BE8C\">is_new<\/span><span style=\"color: #ECEFF4\">(<\/span><span style=\"color: #88C0D0\">news:<\/span><span style=\"color: #D8DEE9FF\"> <\/span><span style=\"color: #A3BE8C\">dict<\/span><span style=\"color: #ECEFF4\">)<\/span><span style=\"color: #D8DEE9FF\"> -<\/span><span style=\"color: #81A1C1\">&gt;<\/span><span style=\"color: #D8DEE9FF\"> <\/span><span style=\"color: #A3BE8C\">bool:<\/span><\/span>\n<span class=\"line\"><span style=\"color: #D8DEE9FF\">    <\/span><span style=\"color: #88C0D0\">dk<\/span><span style=\"color: #D8DEE9FF\"> <\/span><span style=\"color: #A3BE8C\">=<\/span><span style=\"color: #D8DEE9FF\"> <\/span><span style=\"color: #A3BE8C\">news.get<\/span><span style=\"color: #ECEFF4\">(<\/span><span style=\"color: #88C0D0\">&quot;dk&quot;<\/span><span style=\"color: #ECEFF4\">)<\/span><\/span>\n<span class=\"line\"><span style=\"color: #D8DEE9FF\">    <\/span><span style=\"color: #81A1C1\">if<\/span><span style=\"color: #D8DEE9FF\"> <\/span><span style=\"color: #88C0D0\">not<\/span><span style=\"color: #D8DEE9FF\"> <\/span><span style=\"color: #A3BE8C\">dk<\/span><span style=\"color: #D8DEE9FF\"> <\/span><span style=\"color: #A3BE8C\">or<\/span><span style=\"color: #D8DEE9FF\"> <\/span><span style=\"color: #A3BE8C\">dk<\/span><span style=\"color: #D8DEE9FF\"> <\/span><span style=\"color: #A3BE8C\">in<\/span><span style=\"color: #D8DEE9FF\"> <\/span><span style=\"color: #A3BE8C\">_seen:<\/span><\/span>\n<span class=\"line\"><span style=\"color: #D8DEE9FF\">        <\/span><span style=\"color: #81A1C1\">return<\/span><span style=\"color: #D8DEE9FF\"> <\/span><span style=\"color: #A3BE8C\">False<\/span><\/span>\n<span class=\"line\"><span style=\"color: #D8DEE9FF\">    <\/span><span style=\"color: #88C0D0\">_seen.add(dk<\/span><span style=\"color: #D8DEE9FF\">)<\/span><\/span>\n<span class=\"line\"><span style=\"color: #D8DEE9FF\">    <\/span><span style=\"color: #81A1C1\">return<\/span><span style=\"color: #D8DEE9FF\"> <\/span><span style=\"color: #A3BE8C\">True<\/span><\/span><\/code><\/pre><\/div>\n\n\n\n<p class=\"wp-block-paragraph\">In a long-running process, bound the set \u2014 an LRU or a periodic flush of entries older than a day is plenty, since duplicates arrive close together in time.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">5.2 Route news to a portfolio<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Intersect <code>symbols<\/code> with the instruments you care about. No text parsing.<\/p>\n\n\n\n<div class=\"wp-block-kevinbatdorf-code-block-pro\" data-code-block-pro-font-family=\"Code-Pro-JetBrains-Mono\" style=\"font-size:.875rem;font-family:Code-Pro-JetBrains-Mono,ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;line-height:1.25rem;--cbp-tab-width:2;tab-size:var(--cbp-tab-width, 2)\"><span style=\"display:block;padding:16px 0 0 16px;margin-bottom:-1px;width:100%;text-align:left;background-color:#2e3440ff\"><svg xmlns=\"http:\/\/www.w3.org\/2000\/svg\" width=\"54\" height=\"14\" viewBox=\"0 0 54 14\"><g fill=\"none\" fill-rule=\"evenodd\" transform=\"translate(1 1)\"><circle cx=\"6\" cy=\"6\" r=\"6\" fill=\"#FF5F56\" stroke=\"#E0443E\" stroke-width=\".5\"><\/circle><circle cx=\"26\" cy=\"6\" r=\"6\" fill=\"#FFBD2E\" stroke=\"#DEA123\" stroke-width=\".5\"><\/circle><circle cx=\"46\" cy=\"6\" r=\"6\" fill=\"#27C93F\" stroke=\"#1AAB29\" stroke-width=\".5\"><\/circle><\/g><\/svg><\/span><span role=\"button\" tabindex=\"0\" style=\"color:#d8dee9ff;display:none\" aria-label=\"Copy\" class=\"code-block-pro-copy-button\"><pre class=\"code-block-pro-copy-button-pre\" aria-hidden=\"true\"><textarea class=\"code-block-pro-copy-button-textarea\" tabindex=\"-1\" aria-hidden=\"true\" readonly>def route_to_holdings(news: dict, holdings: set&#91;str&#93;) -> set&#91;str&#93;:\n    hits = set(news.get(\"symbols\") or []) &amp; holdings\n    if hits:\n        print(f\"\u26a0 position news [urgency {news&#91;'urgency'&#93;}] {news&#91;'title'&#93;}  \u2192 {hits}\")\n    return hits\n\nroute_to_holdings(news, {\"AAPL.US\", \"005930.KS\", \"DELL.US\"})<\/textarea><\/pre><svg xmlns=\"http:\/\/www.w3.org\/2000\/svg\" style=\"width:24px;height:24px\" fill=\"none\" viewBox=\"0 0 24 24\" stroke=\"currentColor\" stroke-width=\"2\"><path class=\"with-check\" stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2m-6 9l2 2 4-4\"><\/path><path class=\"without-check\" stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2\"><\/path><\/svg><\/span><pre class=\"shiki nord\" style=\"background-color: #2e3440ff\" tabindex=\"0\"><code><span class=\"line\"><span style=\"color: #88C0D0\">def<\/span><span style=\"color: #D8DEE9FF\"> <\/span><span style=\"color: #A3BE8C\">route_to_holdings<\/span><span style=\"color: #ECEFF4\">(<\/span><span style=\"color: #88C0D0\">news:<\/span><span style=\"color: #D8DEE9FF\"> <\/span><span style=\"color: #A3BE8C\">dict,<\/span><span style=\"color: #D8DEE9FF\"> <\/span><span style=\"color: #A3BE8C\">holdings:<\/span><span style=\"color: #D8DEE9FF\"> <\/span><span style=\"color: #A3BE8C\">set&#91;str&#93;<\/span><span style=\"color: #ECEFF4\">)<\/span><span style=\"color: #D8DEE9FF\"> -<\/span><span style=\"color: #81A1C1\">&gt;<\/span><span style=\"color: #D8DEE9FF\"> <\/span><span style=\"color: #A3BE8C\">set&#91;str&#93;:<\/span><\/span>\n<span class=\"line\"><span style=\"color: #D8DEE9FF\">    <\/span><span style=\"color: #88C0D0\">hits<\/span><span style=\"color: #D8DEE9FF\"> <\/span><span style=\"color: #A3BE8C\">=<\/span><span style=\"color: #D8DEE9FF\"> <\/span><span style=\"color: #A3BE8C\">set<\/span><span style=\"color: #ECEFF4\">(<\/span><span style=\"color: #88C0D0\">news.get(<\/span><span style=\"color: #88C0D0\">&quot;symbols&quot;<\/span><span style=\"color: #ECEFF4\">)<\/span><span style=\"color: #D8DEE9FF\"> <\/span><span style=\"color: #A3BE8C\">or<\/span><span style=\"color: #D8DEE9FF\"> []) <\/span><span style=\"color: #ECEFF4\">&amp;<\/span><span style=\"color: #D8DEE9FF\"> <\/span><span style=\"color: #88C0D0\">holdings<\/span><\/span>\n<span class=\"line\"><span style=\"color: #D8DEE9FF\">    <\/span><span style=\"color: #81A1C1\">if<\/span><span style=\"color: #D8DEE9FF\"> <\/span><span style=\"color: #88C0D0\">hits:<\/span><\/span>\n<span class=\"line\"><span style=\"color: #D8DEE9FF\">        <\/span><span style=\"color: #88C0D0\">print<\/span><span style=\"color: #D8DEE9FF\">(f<\/span><span style=\"color: #88C0D0\">&quot;\u26a0 position news [urgency {news&#91;&#39;urgency&#39;&#93;}] {news&#91;&#39;title&#39;&#93;}  \u2192 {hits}&quot;<\/span><span style=\"color: #D8DEE9FF\">)<\/span><\/span>\n<span class=\"line\"><span style=\"color: #D8DEE9FF\">    <\/span><span style=\"color: #81A1C1\">return<\/span><span style=\"color: #D8DEE9FF\"> <\/span><span style=\"color: #A3BE8C\">hits<\/span><\/span>\n<span class=\"line\"><\/span>\n<span class=\"line\"><span style=\"color: #88C0D0\">route_to_holdings(news,<\/span><span style=\"color: #D8DEE9FF\"> <\/span><span style=\"color: #A3BE8C\">{<\/span><span style=\"color: #ECEFF4\">&quot;<\/span><span style=\"color: #A3BE8C\">AAPL.US<\/span><span style=\"color: #ECEFF4\">&quot;<\/span><span style=\"color: #A3BE8C\">,<\/span><span style=\"color: #D8DEE9FF\"> <\/span><span style=\"color: #ECEFF4\">&quot;<\/span><span style=\"color: #A3BE8C\">005930.KS<\/span><span style=\"color: #ECEFF4\">&quot;<\/span><span style=\"color: #A3BE8C\">,<\/span><span style=\"color: #D8DEE9FF\"> <\/span><span style=\"color: #ECEFF4\">&quot;<\/span><span style=\"color: #A3BE8C\">DELL.US<\/span><span style=\"color: #ECEFF4\">&quot;<\/span><span style=\"color: #A3BE8C\">}<\/span><span style=\"color: #D8DEE9FF\">)<\/span><\/span><\/code><\/pre><\/div>\n\n\n\n<h3 class=\"wp-block-heading\">5.3 Prioritize with <code>urgency<\/code><\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Feed the stream into a heap so breaking items are worked first:<\/p>\n\n\n\n<div class=\"wp-block-kevinbatdorf-code-block-pro\" data-code-block-pro-font-family=\"Code-Pro-JetBrains-Mono\" style=\"font-size:.875rem;font-family:Code-Pro-JetBrains-Mono,ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;line-height:1.25rem;--cbp-tab-width:2;tab-size:var(--cbp-tab-width, 2)\"><span style=\"display:block;padding:16px 0 0 16px;margin-bottom:-1px;width:100%;text-align:left;background-color:#2e3440ff\"><svg xmlns=\"http:\/\/www.w3.org\/2000\/svg\" width=\"54\" height=\"14\" viewBox=\"0 0 54 14\"><g fill=\"none\" fill-rule=\"evenodd\" transform=\"translate(1 1)\"><circle cx=\"6\" cy=\"6\" r=\"6\" fill=\"#FF5F56\" stroke=\"#E0443E\" stroke-width=\".5\"><\/circle><circle cx=\"26\" cy=\"6\" r=\"6\" fill=\"#FFBD2E\" stroke=\"#DEA123\" stroke-width=\".5\"><\/circle><circle cx=\"46\" cy=\"6\" r=\"6\" fill=\"#27C93F\" stroke=\"#1AAB29\" stroke-width=\".5\"><\/circle><\/g><\/svg><\/span><span role=\"button\" tabindex=\"0\" style=\"color:#d8dee9ff;display:none\" aria-label=\"Copy\" class=\"code-block-pro-copy-button\"><pre class=\"code-block-pro-copy-button-pre\" aria-hidden=\"true\"><textarea class=\"code-block-pro-copy-button-textarea\" tabindex=\"-1\" aria-hidden=\"true\" readonly>import heapq\n\nqueue: list = []\n\ndef enqueue(news: dict) -> None:\n    # (urgency, published) \u2014 lower urgency first, then oldest first\n    heapq.heappush(queue, (news&#91;\"urgency\"&#93;, news&#91;\"published\"&#93;, news&#91;\"dk\"&#93;, news))\n\ndef next_item():\n    return heapq.heappop(queue)&#91;-1&#93; if queue else None<\/textarea><\/pre><svg xmlns=\"http:\/\/www.w3.org\/2000\/svg\" style=\"width:24px;height:24px\" fill=\"none\" viewBox=\"0 0 24 24\" stroke=\"currentColor\" stroke-width=\"2\"><path class=\"with-check\" stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2m-6 9l2 2 4-4\"><\/path><path class=\"without-check\" stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2\"><\/path><\/svg><\/span><pre class=\"shiki nord\" style=\"background-color: #2e3440ff\" tabindex=\"0\"><code><span class=\"line\"><span style=\"color: #88C0D0\">import<\/span><span style=\"color: #D8DEE9FF\"> <\/span><span style=\"color: #A3BE8C\">heapq<\/span><\/span>\n<span class=\"line\"><\/span>\n<span class=\"line\"><span style=\"color: #88C0D0\">queue:<\/span><span style=\"color: #D8DEE9FF\"> <\/span><span style=\"color: #A3BE8C\">list<\/span><span style=\"color: #D8DEE9FF\"> <\/span><span style=\"color: #A3BE8C\">=<\/span><span style=\"color: #D8DEE9FF\"> []<\/span><\/span>\n<span class=\"line\"><\/span>\n<span class=\"line\"><span style=\"color: #88C0D0\">def<\/span><span style=\"color: #D8DEE9FF\"> <\/span><span style=\"color: #A3BE8C\">enqueue<\/span><span style=\"color: #ECEFF4\">(<\/span><span style=\"color: #88C0D0\">news:<\/span><span style=\"color: #D8DEE9FF\"> <\/span><span style=\"color: #A3BE8C\">dict<\/span><span style=\"color: #ECEFF4\">)<\/span><span style=\"color: #D8DEE9FF\"> -<\/span><span style=\"color: #81A1C1\">&gt;<\/span><span style=\"color: #D8DEE9FF\"> <\/span><span style=\"color: #A3BE8C\">None:<\/span><\/span>\n<span class=\"line\"><span style=\"color: #D8DEE9FF\">    <\/span><span style=\"color: #616E88\"># (urgency, published) \u2014 lower urgency first, then oldest first<\/span><\/span>\n<span class=\"line\"><span style=\"color: #D8DEE9FF\">    <\/span><span style=\"color: #88C0D0\">heapq.heappush(queue,<\/span><span style=\"color: #D8DEE9FF\"> (news&#91;<\/span><span style=\"color: #ECEFF4\">&quot;<\/span><span style=\"color: #A3BE8C\">urgency<\/span><span style=\"color: #ECEFF4\">&quot;<\/span><span style=\"color: #D8DEE9FF\">&#93;, news<\/span><span style=\"color: #ECEFF4\">&#91;<\/span><span style=\"color: #ECEFF4\">&quot;<\/span><span style=\"color: #A3BE8C\">published<\/span><span style=\"color: #ECEFF4\">&quot;<\/span><span style=\"color: #ECEFF4\">&#93;<\/span><span style=\"color: #D8DEE9FF\">, news<\/span><span style=\"color: #ECEFF4\">&#91;<\/span><span style=\"color: #ECEFF4\">&quot;<\/span><span style=\"color: #A3BE8C\">dk<\/span><span style=\"color: #ECEFF4\">&quot;<\/span><span style=\"color: #ECEFF4\">&#93;<\/span><span style=\"color: #D8DEE9FF\">, news))<\/span><\/span>\n<span class=\"line\"><\/span>\n<span class=\"line\"><span style=\"color: #88C0D0\">def<\/span><span style=\"color: #D8DEE9FF\"> <\/span><span style=\"color: #A3BE8C\">next_item<\/span><span style=\"color: #ECEFF4\">()<\/span><span style=\"color: #A3BE8C\">:<\/span><\/span>\n<span class=\"line\"><span style=\"color: #D8DEE9FF\">    <\/span><span style=\"color: #81A1C1\">return<\/span><span style=\"color: #D8DEE9FF\"> <\/span><span style=\"color: #A3BE8C\">heapq.heappop<\/span><span style=\"color: #ECEFF4\">(<\/span><span style=\"color: #88C0D0\">queue<\/span><span style=\"color: #ECEFF4\">)<\/span><span style=\"color: #A3BE8C\">&#91;<\/span><span style=\"color: #B48EAD\">-1<\/span><span style=\"color: #A3BE8C\">&#93;<\/span><span style=\"color: #D8DEE9FF\"> <\/span><span style=\"color: #A3BE8C\">if<\/span><span style=\"color: #D8DEE9FF\"> <\/span><span style=\"color: #A3BE8C\">queue<\/span><span style=\"color: #D8DEE9FF\"> <\/span><span style=\"color: #A3BE8C\">else<\/span><span style=\"color: #D8DEE9FF\"> <\/span><span style=\"color: #A3BE8C\">None<\/span><\/span><\/code><\/pre><\/div>\n\n\n\n<h3 class=\"wp-block-heading\">5.4 A per-instrument news feed for a trading UI<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">If you run a terminal or broker app, the highest-value use is a news strip on every instrument page. <code>symbols<\/code> gives you the mapping directly, so the whole feature is an in-memory index:<\/p>\n\n\n\n<div class=\"wp-block-kevinbatdorf-code-block-pro\" data-code-block-pro-font-family=\"Code-Pro-JetBrains-Mono\" style=\"font-size:.875rem;font-family:Code-Pro-JetBrains-Mono,ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;line-height:1.25rem;--cbp-tab-width:2;tab-size:var(--cbp-tab-width, 2)\"><span style=\"display:block;padding:16px 0 0 16px;margin-bottom:-1px;width:100%;text-align:left;background-color:#2e3440ff\"><svg xmlns=\"http:\/\/www.w3.org\/2000\/svg\" width=\"54\" height=\"14\" viewBox=\"0 0 54 14\"><g fill=\"none\" fill-rule=\"evenodd\" transform=\"translate(1 1)\"><circle cx=\"6\" cy=\"6\" r=\"6\" fill=\"#FF5F56\" stroke=\"#E0443E\" stroke-width=\".5\"><\/circle><circle cx=\"26\" cy=\"6\" r=\"6\" fill=\"#FFBD2E\" stroke=\"#DEA123\" stroke-width=\".5\"><\/circle><circle cx=\"46\" cy=\"6\" r=\"6\" fill=\"#27C93F\" stroke=\"#1AAB29\" stroke-width=\".5\"><\/circle><\/g><\/svg><\/span><span role=\"button\" tabindex=\"0\" style=\"color:#d8dee9ff;display:none\" aria-label=\"Copy\" class=\"code-block-pro-copy-button\"><pre class=\"code-block-pro-copy-button-pre\" aria-hidden=\"true\"><textarea class=\"code-block-pro-copy-button-textarea\" tabindex=\"-1\" aria-hidden=\"true\" readonly>from collections import defaultdict, deque\n\n_by_symbol: dict&#91;str, deque&#93; = defaultdict(lambda: deque(maxlen=50))\n\ndef index_news(news: dict) -> None:\n    item = {\n        \"title\": news&#91;\"title\"&#93;,\n        \"summary\": news.get(\"sd\"),\n        \"published\": news&#91;\"published\"&#93;,\n        \"urgency\": news&#91;\"urgency\"&#93;,\n        \"provider\": news&#91;\"provider\"&#93;,\n        \"link\": news.get(\"link\"),\n    }\n    for sym in news.get(\"symbols\") or []:\n        _by_symbol&#91;sym&#93;.appendleft(item)\n\ndef instrument_news(symbol: str) -> list:\n    return list(_by_symbol.get(symbol, []))\n\n# When a user opens the DELL.US page:\nfor n in instrument_news(\"DELL.US\"):\n    print(f\"[{n&#91;'urgency'&#93;}] {n&#91;'title'&#93;} \u2014 {n&#91;'provider'&#93;}\")<\/textarea><\/pre><svg xmlns=\"http:\/\/www.w3.org\/2000\/svg\" style=\"width:24px;height:24px\" fill=\"none\" viewBox=\"0 0 24 24\" stroke=\"currentColor\" stroke-width=\"2\"><path class=\"with-check\" stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2m-6 9l2 2 4-4\"><\/path><path class=\"without-check\" stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2\"><\/path><\/svg><\/span><pre class=\"shiki nord\" style=\"background-color: #2e3440ff\" tabindex=\"0\"><code><span class=\"line\"><span style=\"color: #88C0D0\">from<\/span><span style=\"color: #D8DEE9FF\"> <\/span><span style=\"color: #A3BE8C\">collections<\/span><span style=\"color: #D8DEE9FF\"> <\/span><span style=\"color: #A3BE8C\">import<\/span><span style=\"color: #D8DEE9FF\"> <\/span><span style=\"color: #A3BE8C\">defaultdict,<\/span><span style=\"color: #D8DEE9FF\"> <\/span><span style=\"color: #A3BE8C\">deque<\/span><\/span>\n<span class=\"line\"><\/span>\n<span class=\"line\"><span style=\"color: #88C0D0\">_by_symbol:<\/span><span style=\"color: #D8DEE9FF\"> <\/span><span style=\"color: #A3BE8C\">dict&#91;str,<\/span><span style=\"color: #D8DEE9FF\"> <\/span><span style=\"color: #A3BE8C\">deque&#93;<\/span><span style=\"color: #D8DEE9FF\"> <\/span><span style=\"color: #A3BE8C\">=<\/span><span style=\"color: #D8DEE9FF\"> <\/span><span style=\"color: #A3BE8C\">defaultdict<\/span><span style=\"color: #ECEFF4\">(<\/span><span style=\"color: #88C0D0\">lambda:<\/span><span style=\"color: #D8DEE9FF\"> <\/span><span style=\"color: #A3BE8C\">deque<\/span><span style=\"color: #ECEFF4\">(<\/span><span style=\"color: #D8DEE9\">maxlen<\/span><span style=\"color: #81A1C1\">=<\/span><span style=\"color: #B48EAD\">50<\/span><span style=\"color: #D8DEE9FF\">))<\/span><\/span>\n<span class=\"line\"><\/span>\n<span class=\"line\"><span style=\"color: #88C0D0\">def<\/span><span style=\"color: #D8DEE9FF\"> <\/span><span style=\"color: #A3BE8C\">index_news<\/span><span style=\"color: #ECEFF4\">(<\/span><span style=\"color: #88C0D0\">news:<\/span><span style=\"color: #D8DEE9FF\"> <\/span><span style=\"color: #A3BE8C\">dict<\/span><span style=\"color: #ECEFF4\">)<\/span><span style=\"color: #D8DEE9FF\"> -<\/span><span style=\"color: #81A1C1\">&gt;<\/span><span style=\"color: #D8DEE9FF\"> <\/span><span style=\"color: #A3BE8C\">None:<\/span><\/span>\n<span class=\"line\"><span style=\"color: #D8DEE9FF\">    <\/span><span style=\"color: #88C0D0\">item<\/span><span style=\"color: #D8DEE9FF\"> <\/span><span style=\"color: #A3BE8C\">=<\/span><span style=\"color: #D8DEE9FF\"> <\/span><span style=\"color: #A3BE8C\">{<\/span><\/span>\n<span class=\"line\"><span style=\"color: #D8DEE9FF\">        <\/span><span style=\"color: #88C0D0\">&quot;title&quot;<\/span><span style=\"color: #88C0D0\">:<\/span><span style=\"color: #D8DEE9FF\"> <\/span><span style=\"color: #A3BE8C\">news&#91;<\/span><span style=\"color: #ECEFF4\">&quot;<\/span><span style=\"color: #A3BE8C\">title<\/span><span style=\"color: #ECEFF4\">&quot;<\/span><span style=\"color: #A3BE8C\">&#93;,<\/span><\/span>\n<span class=\"line\"><span style=\"color: #D8DEE9FF\">        <\/span><span style=\"color: #88C0D0\">&quot;summary&quot;<\/span><span style=\"color: #88C0D0\">:<\/span><span style=\"color: #D8DEE9FF\"> <\/span><span style=\"color: #A3BE8C\">news.get<\/span><span style=\"color: #ECEFF4\">(<\/span><span style=\"color: #88C0D0\">&quot;sd&quot;<\/span><span style=\"color: #ECEFF4\">)<\/span><span style=\"color: #A3BE8C\">,<\/span><\/span>\n<span class=\"line\"><span style=\"color: #D8DEE9FF\">        <\/span><span style=\"color: #88C0D0\">&quot;published&quot;<\/span><span style=\"color: #88C0D0\">:<\/span><span style=\"color: #D8DEE9FF\"> <\/span><span style=\"color: #A3BE8C\">news&#91;<\/span><span style=\"color: #ECEFF4\">&quot;<\/span><span style=\"color: #A3BE8C\">published<\/span><span style=\"color: #ECEFF4\">&quot;<\/span><span style=\"color: #A3BE8C\">&#93;,<\/span><\/span>\n<span class=\"line\"><span style=\"color: #D8DEE9FF\">        <\/span><span style=\"color: #88C0D0\">&quot;urgency&quot;<\/span><span style=\"color: #88C0D0\">:<\/span><span style=\"color: #D8DEE9FF\"> <\/span><span style=\"color: #A3BE8C\">news&#91;<\/span><span style=\"color: #ECEFF4\">&quot;<\/span><span style=\"color: #A3BE8C\">urgency<\/span><span style=\"color: #ECEFF4\">&quot;<\/span><span style=\"color: #A3BE8C\">&#93;,<\/span><\/span>\n<span class=\"line\"><span style=\"color: #D8DEE9FF\">        <\/span><span style=\"color: #88C0D0\">&quot;provider&quot;<\/span><span style=\"color: #88C0D0\">:<\/span><span style=\"color: #D8DEE9FF\"> <\/span><span style=\"color: #A3BE8C\">news&#91;<\/span><span style=\"color: #ECEFF4\">&quot;<\/span><span style=\"color: #A3BE8C\">provider<\/span><span style=\"color: #ECEFF4\">&quot;<\/span><span style=\"color: #A3BE8C\">&#93;,<\/span><\/span>\n<span class=\"line\"><span style=\"color: #D8DEE9FF\">        <\/span><span style=\"color: #88C0D0\">&quot;link&quot;<\/span><span style=\"color: #88C0D0\">:<\/span><span style=\"color: #D8DEE9FF\"> <\/span><span style=\"color: #A3BE8C\">news.get<\/span><span style=\"color: #ECEFF4\">(<\/span><span style=\"color: #88C0D0\">&quot;link&quot;<\/span><span style=\"color: #ECEFF4\">)<\/span><span style=\"color: #A3BE8C\">,<\/span><\/span>\n<span class=\"line\"><span style=\"color: #D8DEE9FF\">    }<\/span><\/span>\n<span class=\"line\"><span style=\"color: #D8DEE9FF\">    <\/span><span style=\"color: #81A1C1\">for<\/span><span style=\"color: #D8DEE9FF\"> <\/span><span style=\"color: #D8DEE9\">sym<\/span><span style=\"color: #D8DEE9FF\"> <\/span><span style=\"color: #81A1C1\">in<\/span><span style=\"color: #D8DEE9FF\"> news.get<\/span><span style=\"color: #ECEFF4\">(<\/span><span style=\"color: #88C0D0\">&quot;symbols&quot;<\/span><span style=\"color: #ECEFF4\">)<\/span><span style=\"color: #D8DEE9FF\"> or <\/span><span style=\"color: #ECEFF4\">[]<\/span><span style=\"color: #D8DEE9FF\">:<\/span><\/span>\n<span class=\"line\"><span style=\"color: #D8DEE9FF\">        <\/span><span style=\"color: #88C0D0\">_by_symbol&#91;sym&#93;.appendleft(item<\/span><span style=\"color: #D8DEE9FF\">)<\/span><\/span>\n<span class=\"line\"><\/span>\n<span class=\"line\"><span style=\"color: #88C0D0\">def<\/span><span style=\"color: #D8DEE9FF\"> <\/span><span style=\"color: #A3BE8C\">instrument_news<\/span><span style=\"color: #ECEFF4\">(<\/span><span style=\"color: #88C0D0\">symbol:<\/span><span style=\"color: #D8DEE9FF\"> <\/span><span style=\"color: #A3BE8C\">str<\/span><span style=\"color: #ECEFF4\">)<\/span><span style=\"color: #D8DEE9FF\"> -<\/span><span style=\"color: #81A1C1\">&gt;<\/span><span style=\"color: #D8DEE9FF\"> <\/span><span style=\"color: #A3BE8C\">list:<\/span><\/span>\n<span class=\"line\"><span style=\"color: #D8DEE9FF\">    <\/span><span style=\"color: #81A1C1\">return<\/span><span style=\"color: #D8DEE9FF\"> <\/span><span style=\"color: #A3BE8C\">list<\/span><span style=\"color: #ECEFF4\">(<\/span><span style=\"color: #88C0D0\">_by_symbol.get(symbol,<\/span><span style=\"color: #D8DEE9FF\"> []<\/span><span style=\"color: #ECEFF4\">)<\/span><span style=\"color: #D8DEE9FF\">)<\/span><\/span>\n<span class=\"line\"><\/span>\n<span class=\"line\"><span style=\"color: #616E88\"># When a user opens the DELL.US page:<\/span><\/span>\n<span class=\"line\"><span style=\"color: #81A1C1\">for<\/span><span style=\"color: #D8DEE9FF\"> <\/span><span style=\"color: #D8DEE9\">n<\/span><span style=\"color: #D8DEE9FF\"> <\/span><span style=\"color: #81A1C1\">in<\/span><span style=\"color: #D8DEE9FF\"> instrument_news<\/span><span style=\"color: #ECEFF4\">(<\/span><span style=\"color: #88C0D0\">&quot;DELL.US&quot;<\/span><span style=\"color: #ECEFF4\">)<\/span><span style=\"color: #D8DEE9FF\">:<\/span><\/span>\n<span class=\"line\"><span style=\"color: #D8DEE9FF\">    <\/span><span style=\"color: #88C0D0\">print<\/span><span style=\"color: #D8DEE9FF\">(f<\/span><span style=\"color: #88C0D0\">&quot;[{n&#91;&#39;urgency&#39;&#93;}] {n&#91;&#39;title&#39;&#93;} \u2014 {n&#91;&#39;provider&#39;&#93;}&quot;<\/span><span style=\"color: #D8DEE9FF\">)<\/span><\/span><\/code><\/pre><\/div>\n\n\n\n<p class=\"wp-block-paragraph\">Your front end calls <code>instrument_news(symbol)<\/code> when rendering a page; <code>urgency<\/code> drives whether a headline is pinned and highlighted. The hard part \u2014 deciding which of thousands of listed instruments a story belongs to \u2014 is already done.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">5.5 Multi-language monitoring<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">One key allows one connection, and one connection carries one language. To watch several channels at once, run one client per language, each with its own key, and merge on <code>dk<\/code>:<\/p>\n\n\n\n<div class=\"wp-block-kevinbatdorf-code-block-pro\" data-code-block-pro-font-family=\"Code-Pro-JetBrains-Mono\" style=\"font-size:.875rem;font-family:Code-Pro-JetBrains-Mono,ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;line-height:1.25rem;--cbp-tab-width:2;tab-size:var(--cbp-tab-width, 2)\"><span style=\"display:block;padding:16px 0 0 16px;margin-bottom:-1px;width:100%;text-align:left;background-color:#2e3440ff\"><svg xmlns=\"http:\/\/www.w3.org\/2000\/svg\" width=\"54\" height=\"14\" viewBox=\"0 0 54 14\"><g fill=\"none\" fill-rule=\"evenodd\" transform=\"translate(1 1)\"><circle cx=\"6\" cy=\"6\" r=\"6\" fill=\"#FF5F56\" stroke=\"#E0443E\" stroke-width=\".5\"><\/circle><circle cx=\"26\" cy=\"6\" r=\"6\" fill=\"#FFBD2E\" stroke=\"#DEA123\" stroke-width=\".5\"><\/circle><circle cx=\"46\" cy=\"6\" r=\"6\" fill=\"#27C93F\" stroke=\"#1AAB29\" stroke-width=\".5\"><\/circle><\/g><\/svg><\/span><span role=\"button\" tabindex=\"0\" style=\"color:#d8dee9ff;display:none\" aria-label=\"Copy\" class=\"code-block-pro-copy-button\"><pre class=\"code-block-pro-copy-button-pre\" aria-hidden=\"true\"><textarea class=\"code-block-pro-copy-button-textarea\" tabindex=\"-1\" aria-hidden=\"true\" readonly>async def run_all(keys_by_lang: dict&#91;str, str&#93;):\n    await asyncio.gather(*(\n        NewsClient(api_key=key, lang=lang).start()\n        for lang, key in keys_by_lang.items()\n    ))\n\n# run_all({\"en\": KEY_1, \"zh-Hans\": KEY_2, \"ja\": KEY_3})<\/textarea><\/pre><svg xmlns=\"http:\/\/www.w3.org\/2000\/svg\" style=\"width:24px;height:24px\" fill=\"none\" viewBox=\"0 0 24 24\" stroke=\"currentColor\" stroke-width=\"2\"><path class=\"with-check\" stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2m-6 9l2 2 4-4\"><\/path><path class=\"without-check\" stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2\"><\/path><\/svg><\/span><pre class=\"shiki nord\" style=\"background-color: #2e3440ff\" tabindex=\"0\"><code><span class=\"line\"><span style=\"color: #88C0D0\">async<\/span><span style=\"color: #D8DEE9FF\"> <\/span><span style=\"color: #A3BE8C\">def<\/span><span style=\"color: #D8DEE9FF\"> <\/span><span style=\"color: #A3BE8C\">run_all<\/span><span style=\"color: #ECEFF4\">(<\/span><span style=\"color: #88C0D0\">keys_by_lang:<\/span><span style=\"color: #D8DEE9FF\"> <\/span><span style=\"color: #A3BE8C\">dict&#91;str,<\/span><span style=\"color: #D8DEE9FF\"> <\/span><span style=\"color: #A3BE8C\">str&#93;<\/span><span style=\"color: #ECEFF4\">)<\/span><span style=\"color: #A3BE8C\">:<\/span><\/span>\n<span class=\"line\"><span style=\"color: #D8DEE9FF\">    <\/span><span style=\"color: #88C0D0\">await<\/span><span style=\"color: #D8DEE9FF\"> <\/span><span style=\"color: #A3BE8C\">asyncio.gather<\/span><span style=\"color: #ECEFF4\">(<\/span><span style=\"color: #81A1C1\">*<\/span><span style=\"color: #ECEFF4\">(<\/span><\/span>\n<span class=\"line\"><span style=\"color: #D8DEE9FF\">        <\/span><span style=\"color: #88C0D0\">NewsClient(api_key<\/span><span style=\"color: #D8DEE9FF\">=key, <\/span><span style=\"color: #A3BE8C\">lang=lang<\/span><span style=\"color: #ECEFF4\">)<\/span><span style=\"color: #88C0D0\">.start<\/span><span style=\"color: #ECEFF4\">()<\/span><\/span>\n<span class=\"line\"><span style=\"color: #D8DEE9FF\">        for lang, key in keys_by_lang.items<\/span><span style=\"color: #ECEFF4\">()<\/span><\/span>\n<span class=\"line\"><span style=\"color: #D8DEE9FF\">    <\/span><span style=\"color: #ECEFF4\">)<\/span><span style=\"color: #D8DEE9FF\">)<\/span><\/span>\n<span class=\"line\"><\/span>\n<span class=\"line\"><span style=\"color: #616E88\"># run_all({&quot;en&quot;: KEY_1, &quot;zh-Hans&quot;: KEY_2, &quot;ja&quot;: KEY_3})<\/span><\/span><\/code><\/pre><\/div>\n\n\n\n<p class=\"wp-block-paragraph\">Because <code>dk<\/code> is content-based, the same event reported in the English and Chinese channels will <em>not<\/em> share a <code>dk<\/code> (different text), so treat cross-language dedup as a separate, semantic problem if you need it. Within a single language, <code>dk<\/code> handles it.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">5.6 Join news to price<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">News and market data share the key, so an event can immediately trigger a price lookup. When a high-urgency item lands for a symbol you follow, pull its latest trade:<\/p>\n\n\n\n<div class=\"wp-block-kevinbatdorf-code-block-pro\" data-code-block-pro-font-family=\"Code-Pro-JetBrains-Mono\" style=\"font-size:.875rem;font-family:Code-Pro-JetBrains-Mono,ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;line-height:1.25rem;--cbp-tab-width:2;tab-size:var(--cbp-tab-width, 2)\"><span style=\"display:block;padding:16px 0 0 16px;margin-bottom:-1px;width:100%;text-align:left;background-color:#2e3440ff\"><svg xmlns=\"http:\/\/www.w3.org\/2000\/svg\" width=\"54\" height=\"14\" viewBox=\"0 0 54 14\"><g fill=\"none\" fill-rule=\"evenodd\" transform=\"translate(1 1)\"><circle cx=\"6\" cy=\"6\" r=\"6\" fill=\"#FF5F56\" stroke=\"#E0443E\" stroke-width=\".5\"><\/circle><circle cx=\"26\" cy=\"6\" r=\"6\" fill=\"#FFBD2E\" stroke=\"#DEA123\" stroke-width=\".5\"><\/circle><circle cx=\"46\" cy=\"6\" r=\"6\" fill=\"#27C93F\" stroke=\"#1AAB29\" stroke-width=\".5\"><\/circle><\/g><\/svg><\/span><span role=\"button\" tabindex=\"0\" style=\"color:#d8dee9ff;display:none\" aria-label=\"Copy\" class=\"code-block-pro-copy-button\"><pre class=\"code-block-pro-copy-button-pre\" aria-hidden=\"true\"><textarea class=\"code-block-pro-copy-button-textarea\" tabindex=\"-1\" aria-hidden=\"true\" readonly>import requests\n\ndef price_on_news(news: dict, watch: set&#91;str&#93;) -> None:\n    hits = set(news.get(\"symbols\") or []) &amp; watch\n    if not hits or news&#91;\"urgency\"&#93; > 2:\n        return\n    codes = \",\".join(s for s in hits if s.endswith(\".US\"))\n    if not codes:\n        return\n    r = requests.get(\n        f\"https:\/\/data.infoway.io\/stock\/batch_trade\/{codes}\",\n        headers={\"apiKey\": os.environ&#91;\"INFOWAY_API_KEY\"&#93;}, timeout=10,\n    ).json()\n    for tick in r.get(\"data\", []):\n        print(f\"  {tick&#91;'s'&#93;}  ${tick&#91;'p'&#93;}  \u2190 {news&#91;'title'&#93;}\")<\/textarea><\/pre><svg xmlns=\"http:\/\/www.w3.org\/2000\/svg\" style=\"width:24px;height:24px\" fill=\"none\" viewBox=\"0 0 24 24\" stroke=\"currentColor\" stroke-width=\"2\"><path class=\"with-check\" stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2m-6 9l2 2 4-4\"><\/path><path class=\"without-check\" stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2\"><\/path><\/svg><\/span><pre class=\"shiki nord\" style=\"background-color: #2e3440ff\" tabindex=\"0\"><code><span class=\"line\"><span style=\"color: #88C0D0\">import<\/span><span style=\"color: #D8DEE9FF\"> <\/span><span style=\"color: #A3BE8C\">requests<\/span><\/span>\n<span class=\"line\"><\/span>\n<span class=\"line\"><span style=\"color: #88C0D0\">def<\/span><span style=\"color: #D8DEE9FF\"> <\/span><span style=\"color: #A3BE8C\">price_on_news<\/span><span style=\"color: #ECEFF4\">(<\/span><span style=\"color: #88C0D0\">news:<\/span><span style=\"color: #D8DEE9FF\"> <\/span><span style=\"color: #A3BE8C\">dict,<\/span><span style=\"color: #D8DEE9FF\"> <\/span><span style=\"color: #A3BE8C\">watch:<\/span><span style=\"color: #D8DEE9FF\"> <\/span><span style=\"color: #A3BE8C\">set&#91;str&#93;<\/span><span style=\"color: #ECEFF4\">)<\/span><span style=\"color: #D8DEE9FF\"> -<\/span><span style=\"color: #81A1C1\">&gt;<\/span><span style=\"color: #D8DEE9FF\"> <\/span><span style=\"color: #A3BE8C\">None:<\/span><\/span>\n<span class=\"line\"><span style=\"color: #D8DEE9FF\">    <\/span><span style=\"color: #88C0D0\">hits<\/span><span style=\"color: #D8DEE9FF\"> <\/span><span style=\"color: #A3BE8C\">=<\/span><span style=\"color: #D8DEE9FF\"> <\/span><span style=\"color: #A3BE8C\">set<\/span><span style=\"color: #ECEFF4\">(<\/span><span style=\"color: #88C0D0\">news.get(<\/span><span style=\"color: #88C0D0\">&quot;symbols&quot;<\/span><span style=\"color: #ECEFF4\">)<\/span><span style=\"color: #D8DEE9FF\"> <\/span><span style=\"color: #A3BE8C\">or<\/span><span style=\"color: #D8DEE9FF\"> []) <\/span><span style=\"color: #ECEFF4\">&amp;<\/span><span style=\"color: #D8DEE9FF\"> <\/span><span style=\"color: #88C0D0\">watch<\/span><\/span>\n<span class=\"line\"><span style=\"color: #D8DEE9FF\">    <\/span><span style=\"color: #81A1C1\">if<\/span><span style=\"color: #D8DEE9FF\"> <\/span><span style=\"color: #88C0D0\">not<\/span><span style=\"color: #D8DEE9FF\"> <\/span><span style=\"color: #A3BE8C\">hits<\/span><span style=\"color: #D8DEE9FF\"> <\/span><span style=\"color: #A3BE8C\">or<\/span><span style=\"color: #D8DEE9FF\"> <\/span><span style=\"color: #A3BE8C\">news&#91;<\/span><span style=\"color: #ECEFF4\">&quot;<\/span><span style=\"color: #A3BE8C\">urgency<\/span><span style=\"color: #ECEFF4\">&quot;<\/span><span style=\"color: #A3BE8C\">&#93;<\/span><span style=\"color: #D8DEE9FF\"> <\/span><span style=\"color: #81A1C1\">&gt;<\/span><span style=\"color: #D8DEE9FF\"> <\/span><span style=\"color: #B48EAD\">2<\/span><span style=\"color: #A3BE8C\">:<\/span><\/span>\n<span class=\"line\"><span style=\"color: #D8DEE9FF\">        <\/span><span style=\"color: #81A1C1\">return<\/span><\/span>\n<span class=\"line\"><span style=\"color: #D8DEE9FF\">    <\/span><span style=\"color: #88C0D0\">codes<\/span><span style=\"color: #D8DEE9FF\"> <\/span><span style=\"color: #A3BE8C\">=<\/span><span style=\"color: #D8DEE9FF\"> <\/span><span style=\"color: #ECEFF4\">&quot;<\/span><span style=\"color: #A3BE8C\">,<\/span><span style=\"color: #ECEFF4\">&quot;<\/span><span style=\"color: #A3BE8C\">.join<\/span><span style=\"color: #ECEFF4\">(<\/span><span style=\"color: #88C0D0\">s<\/span><span style=\"color: #D8DEE9FF\"> <\/span><span style=\"color: #A3BE8C\">for<\/span><span style=\"color: #D8DEE9FF\"> <\/span><span style=\"color: #A3BE8C\">s<\/span><span style=\"color: #D8DEE9FF\"> <\/span><span style=\"color: #A3BE8C\">in<\/span><span style=\"color: #D8DEE9FF\"> <\/span><span style=\"color: #A3BE8C\">hits<\/span><span style=\"color: #D8DEE9FF\"> <\/span><span style=\"color: #A3BE8C\">if<\/span><span style=\"color: #D8DEE9FF\"> <\/span><span style=\"color: #A3BE8C\">s.endswith<\/span><span style=\"color: #ECEFF4\">(<\/span><span style=\"color: #88C0D0\">&quot;.US&quot;<\/span><span style=\"color: #ECEFF4\">))<\/span><\/span>\n<span class=\"line\"><span style=\"color: #D8DEE9FF\">    <\/span><span style=\"color: #81A1C1\">if<\/span><span style=\"color: #D8DEE9FF\"> <\/span><span style=\"color: #88C0D0\">not<\/span><span style=\"color: #D8DEE9FF\"> <\/span><span style=\"color: #A3BE8C\">codes:<\/span><\/span>\n<span class=\"line\"><span style=\"color: #D8DEE9FF\">        <\/span><span style=\"color: #81A1C1\">return<\/span><\/span>\n<span class=\"line\"><span style=\"color: #D8DEE9FF\">    <\/span><span style=\"color: #88C0D0\">r<\/span><span style=\"color: #D8DEE9FF\"> <\/span><span style=\"color: #A3BE8C\">=<\/span><span style=\"color: #D8DEE9FF\"> <\/span><span style=\"color: #A3BE8C\">requests.get<\/span><span style=\"color: #ECEFF4\">(<\/span><\/span>\n<span class=\"line\"><span style=\"color: #D8DEE9FF\">        <\/span><span style=\"color: #88C0D0\">f<\/span><span style=\"color: #88C0D0\">&quot;https:\/\/data.infoway.io\/stock\/batch_trade\/{codes}&quot;<\/span><span style=\"color: #88C0D0\">,<\/span><\/span>\n<span class=\"line\"><span style=\"color: #D8DEE9FF\">        <\/span><span style=\"color: #D8DEE9\">headers<\/span><span style=\"color: #81A1C1\">=<\/span><span style=\"color: #A3BE8C\">{<\/span><span style=\"color: #ECEFF4\">&quot;<\/span><span style=\"color: #A3BE8C\">apiKey<\/span><span style=\"color: #ECEFF4\">&quot;<\/span><span style=\"color: #A3BE8C\">:<\/span><span style=\"color: #D8DEE9FF\"> <\/span><span style=\"color: #88C0D0\">os.environ&#91;<\/span><span style=\"color: #88C0D0\">&quot;INFOWAY_API_KEY&quot;<\/span><span style=\"color: #88C0D0\">&#93;},<\/span><span style=\"color: #D8DEE9FF\"> <\/span><span style=\"color: #A3BE8C\">timeout=<\/span><span style=\"color: #B48EAD\">10<\/span><span style=\"color: #A3BE8C\">,<\/span><\/span>\n<span class=\"line\"><span style=\"color: #D8DEE9FF\">    <\/span><span style=\"color: #ECEFF4\">)<\/span><span style=\"color: #A3BE8C\">.json<\/span><span style=\"color: #ECEFF4\">()<\/span><\/span>\n<span class=\"line\"><span style=\"color: #D8DEE9FF\">    <\/span><span style=\"color: #81A1C1\">for<\/span><span style=\"color: #D8DEE9FF\"> <\/span><span style=\"color: #D8DEE9\">tick<\/span><span style=\"color: #D8DEE9FF\"> <\/span><span style=\"color: #81A1C1\">in<\/span><span style=\"color: #D8DEE9FF\"> r.get<\/span><span style=\"color: #ECEFF4\">(<\/span><span style=\"color: #88C0D0\">&quot;data&quot;<\/span><span style=\"color: #88C0D0\">,<\/span><span style=\"color: #D8DEE9FF\"> []<\/span><span style=\"color: #ECEFF4\">)<\/span><span style=\"color: #D8DEE9FF\">:<\/span><\/span>\n<span class=\"line\"><span style=\"color: #D8DEE9FF\">        <\/span><span style=\"color: #88C0D0\">print<\/span><span style=\"color: #D8DEE9FF\">(f<\/span><span style=\"color: #88C0D0\">&quot;  {tick&#91;&#39;s&#39;&#93;}  <\/span><span style=\"color: #81A1C1\">${<\/span><span style=\"color: #D8DEE9\">tick<\/span><span style=\"color: #ECEFF4\">&#91;<\/span><span style=\"color: #88C0D0\">&#39;p&#39;<\/span><span style=\"color: #ECEFF4\">&#93;<\/span><span style=\"color: #81A1C1\">}<\/span><span style=\"color: #88C0D0\">  \u2190 {news&#91;&#39;title&#39;&#93;}&quot;<\/span><span style=\"color: #D8DEE9FF\">)<\/span><\/span><\/code><\/pre><\/div>\n\n\n\n<p class=\"wp-block-paragraph\">For the reaction <em>after<\/em> a headline \u2014 the price a few minutes before versus a few minutes after \u2014 use the candlestick endpoint (<code>\/stock\/v2\/batch_kline<\/code>) and align on the <code>published<\/code> timestamp.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">6. A production-ready client<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">This is the Quickstart hardened for real use: explicit handshake handling, a heartbeat task bound to the connection, exponential-backoff reconnection, automatic re-subscribe on every new connection, and dedup built in.<\/p>\n\n\n\n<div class=\"wp-block-kevinbatdorf-code-block-pro\" data-code-block-pro-font-family=\"Code-Pro-JetBrains-Mono\" style=\"font-size:.875rem;font-family:Code-Pro-JetBrains-Mono,ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;line-height:1.25rem;--cbp-tab-width:2;tab-size:var(--cbp-tab-width, 2)\"><span style=\"display:block;padding:16px 0 0 16px;margin-bottom:-1px;width:100%;text-align:left;background-color:#2e3440ff\"><svg xmlns=\"http:\/\/www.w3.org\/2000\/svg\" width=\"54\" height=\"14\" viewBox=\"0 0 54 14\"><g fill=\"none\" fill-rule=\"evenodd\" transform=\"translate(1 1)\"><circle cx=\"6\" cy=\"6\" r=\"6\" fill=\"#FF5F56\" stroke=\"#E0443E\" stroke-width=\".5\"><\/circle><circle cx=\"26\" cy=\"6\" r=\"6\" fill=\"#FFBD2E\" stroke=\"#DEA123\" stroke-width=\".5\"><\/circle><circle cx=\"46\" cy=\"6\" r=\"6\" fill=\"#27C93F\" stroke=\"#1AAB29\" stroke-width=\".5\"><\/circle><\/g><\/svg><\/span><span role=\"button\" tabindex=\"0\" style=\"color:#d8dee9ff;display:none\" aria-label=\"Copy\" class=\"code-block-pro-copy-button\"><pre class=\"code-block-pro-copy-button-pre\" aria-hidden=\"true\"><textarea class=\"code-block-pro-copy-button-textarea\" tabindex=\"-1\" aria-hidden=\"true\" readonly>import asyncio\nimport json\nimport logging\nimport os\nimport uuid\nfrom typing import Optional\n\nimport websockets\nfrom websockets.exceptions import ConnectionClosed\n\nlogging.basicConfig(level=logging.INFO, format=\"%(asctime)s %(levelname)s %(message)s\")\nlogger = logging.getLogger(\"infoway-news\")\n\n# --- Protocol codes --------------------------------------------------------\nREQ_SUBSCRIBE = 10020\nREQ_HEARTBEAT = 10010\nACK_SUBSCRIBE = 10021\nPUSH_NEWS     = 10022\nHANDSHAKE_OK  = 200\n\n\nclass NewsClient:\n    \"\"\"Infoway real-time news WebSocket client.\n\n    One connection, one language. Handles heartbeat, reconnect with\n    exponential backoff, re-subscribe on every new connection, and dedup.\n    \"\"\"\n\n    def __init__(self, api_key: str, lang: str = \"en\"):\n        self.url = f\"wss:\/\/data.infoway.io\/news?apikey={api_key}\"\n        self.lang = lang\n        self.ws: Optional&#91;websockets.WebSocketClientProtocol&#93; = None\n        self.running = True\n        self._heartbeat: Optional&#91;asyncio.Task&#93; = None\n        self._seen: set&#91;str&#93; = set()\n\n    @staticmethod\n    def _trace() -> str:\n        return uuid.uuid4().hex\n\n    async def _send(self, msg: dict) -> None:\n        if self.ws:\n            await self.ws.send(json.dumps(msg))\n\n    async def _subscribe(self) -> None:\n        await self._send({\"code\": REQ_SUBSCRIBE, \"trace\": self._trace(),\n                          \"data\": {\"lang\": self.lang}})\n        logger.info(\"Sent subscribe: lang=%s\", self.lang)\n\n    # --- heartbeat -------------------------------------------------------\n    def _start_heartbeat(self) -> None:\n        self._stop_heartbeat()\n\n        async def beat():\n            try:\n                while True:\n                    await asyncio.sleep(30)\n                    if not self.ws or self.ws.close_code is not None:\n                        break\n                    await self._send({\"code\": REQ_HEARTBEAT, \"trace\": self._trace()})\n            except (ConnectionClosed, asyncio.CancelledError):\n                pass\n\n        self._heartbeat = asyncio.create_task(beat())\n\n    def _stop_heartbeat(self) -> None:\n        if self._heartbeat and not self._heartbeat.done():\n            self._heartbeat.cancel()\n        self._heartbeat = None\n\n    # --- message handling ----------------------------------------------\n    def _on_news(self, n: dict) -> None:\n        dk = n.get(\"dk\")\n        if dk and dk in self._seen:\n            return\n        if dk:\n            self._seen.add(dk)\n\n        symbols = \", \".join(n.get(\"symbols\") or []) or \"\u2014\"\n        logger.info(\"NEWS  urgency=%s  provider=%s  symbols=%s\\n      %s\",\n                    n.get(\"urgency\"), n.get(\"provider\"), symbols, n.get(\"title\"))\n        # TODO: persist \/ publish to a queue \/ trigger strategy logic\n\n    def _dispatch(self, raw: str) -> None:\n        try:\n            msg = json.loads(raw)\n        except json.JSONDecodeError:\n            logger.warning(\"non-JSON frame: %r\", raw)\n            return\n\n        code = msg.get(\"code\")\n        if code == PUSH_NEWS:\n            self._on_news(msg.get(\"data\", {}))\n        elif code == ACK_SUBSCRIBE:\n            logger.info(\"Subscription confirmed: lang=%s\",\n                        msg.get(\"data\", {}).get(\"lang\"))\n        elif code == HANDSHAKE_OK:\n            logger.info(\"Handshake OK: %s\", msg.get(\"msg\"))\n        else:\n            logger.debug(\"unhandled code=%s: %s\", code, msg)\n\n    # --- connection lifecycle -----------------------------------------\n    async def _connect_once(self) -> None:\n        async with websockets.connect(self.url, ping_interval=None) as ws:\n            self.ws = ws\n            logger.info(\"Connected\")\n            await self._subscribe()          # MUST re-subscribe on every connection\n            self._start_heartbeat()\n            try:\n                async for message in ws:\n                    self._dispatch(message)\n            finally:\n                self._stop_heartbeat()\n                self.ws = None\n\n    async def start(self) -> None:\n        backoff, backoff_max = 5, 60\n        while self.running:\n            try:\n                await self._connect_once()\n                backoff = 5\n                logger.warning(\"Connection closed by server\")\n            except ConnectionClosed as e:\n                logger.warning(\"Connection closed: %s\", e)\n            except Exception as e:\n                logger.error(\"Connection error: %s\", e)\n            if not self.running:\n                break\n            logger.info(\"Reconnecting in %ss...\", backoff)\n            await asyncio.sleep(backoff)\n            backoff = min(backoff * 2, backoff_max)\n\n    def stop(self) -> None:\n        self.running = False\n\n\nasync def main():\n    api_key = os.environ.get(\"INFOWAY_API_KEY\", \"YOUR_API_KEY\")\n    await NewsClient(api_key, lang=\"en\").start()\n\n\nif __name__ == \"__main__\":\n    try:\n        asyncio.run(main())\n    except KeyboardInterrupt:\n        logger.info(\"stopped\")<\/textarea><\/pre><svg xmlns=\"http:\/\/www.w3.org\/2000\/svg\" style=\"width:24px;height:24px\" fill=\"none\" viewBox=\"0 0 24 24\" stroke=\"currentColor\" stroke-width=\"2\"><path class=\"with-check\" stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2m-6 9l2 2 4-4\"><\/path><path class=\"without-check\" stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2\"><\/path><\/svg><\/span><pre class=\"shiki nord\" style=\"background-color: #2e3440ff\" tabindex=\"0\"><code><span class=\"line\"><span style=\"color: #88C0D0\">import<\/span><span style=\"color: #D8DEE9FF\"> <\/span><span style=\"color: #A3BE8C\">asyncio<\/span><\/span>\n<span class=\"line\"><span style=\"color: #88C0D0\">import<\/span><span style=\"color: #D8DEE9FF\"> <\/span><span style=\"color: #A3BE8C\">json<\/span><\/span>\n<span class=\"line\"><span style=\"color: #88C0D0\">import<\/span><span style=\"color: #D8DEE9FF\"> <\/span><span style=\"color: #A3BE8C\">logging<\/span><\/span>\n<span class=\"line\"><span style=\"color: #88C0D0\">import<\/span><span style=\"color: #D8DEE9FF\"> <\/span><span style=\"color: #A3BE8C\">os<\/span><\/span>\n<span class=\"line\"><span style=\"color: #88C0D0\">import<\/span><span style=\"color: #D8DEE9FF\"> <\/span><span style=\"color: #A3BE8C\">uuid<\/span><\/span>\n<span class=\"line\"><span style=\"color: #88C0D0\">from<\/span><span style=\"color: #D8DEE9FF\"> <\/span><span style=\"color: #A3BE8C\">typing<\/span><span style=\"color: #D8DEE9FF\"> <\/span><span style=\"color: #A3BE8C\">import<\/span><span style=\"color: #D8DEE9FF\"> <\/span><span style=\"color: #A3BE8C\">Optional<\/span><\/span>\n<span class=\"line\"><\/span>\n<span class=\"line\"><span style=\"color: #88C0D0\">import<\/span><span style=\"color: #D8DEE9FF\"> <\/span><span style=\"color: #A3BE8C\">websockets<\/span><\/span>\n<span class=\"line\"><span style=\"color: #88C0D0\">from<\/span><span style=\"color: #D8DEE9FF\"> <\/span><span style=\"color: #A3BE8C\">websockets.exceptions<\/span><span style=\"color: #D8DEE9FF\"> <\/span><span style=\"color: #A3BE8C\">import<\/span><span style=\"color: #D8DEE9FF\"> <\/span><span style=\"color: #A3BE8C\">ConnectionClosed<\/span><\/span>\n<span class=\"line\"><\/span>\n<span class=\"line\"><span style=\"color: #88C0D0\">logging.basicConfig(level<\/span><span style=\"color: #D8DEE9FF\">=logging.INFO, <\/span><span style=\"color: #A3BE8C\">format=<\/span><span style=\"color: #ECEFF4\">&quot;<\/span><span style=\"color: #A3BE8C\">%(asctime)s %(levelname)s %(message)s<\/span><span style=\"color: #ECEFF4\">&quot;<\/span><span style=\"color: #D8DEE9FF\">)<\/span><\/span>\n<span class=\"line\"><span style=\"color: #88C0D0\">logger<\/span><span style=\"color: #D8DEE9FF\"> <\/span><span style=\"color: #A3BE8C\">=<\/span><span style=\"color: #D8DEE9FF\"> <\/span><span style=\"color: #A3BE8C\">logging.getLogger<\/span><span style=\"color: #ECEFF4\">(<\/span><span style=\"color: #88C0D0\">&quot;infoway-news&quot;<\/span><span style=\"color: #ECEFF4\">)<\/span><\/span>\n<span class=\"line\"><\/span>\n<span class=\"line\"><span style=\"color: #616E88\"># --- Protocol codes --------------------------------------------------------<\/span><\/span>\n<span class=\"line\"><span style=\"color: #88C0D0\">REQ_SUBSCRIBE<\/span><span style=\"color: #D8DEE9FF\"> <\/span><span style=\"color: #A3BE8C\">=<\/span><span style=\"color: #D8DEE9FF\"> <\/span><span style=\"color: #B48EAD\">10020<\/span><\/span>\n<span class=\"line\"><span style=\"color: #88C0D0\">REQ_HEARTBEAT<\/span><span style=\"color: #D8DEE9FF\"> <\/span><span style=\"color: #A3BE8C\">=<\/span><span style=\"color: #D8DEE9FF\"> <\/span><span style=\"color: #B48EAD\">10010<\/span><\/span>\n<span class=\"line\"><span style=\"color: #88C0D0\">ACK_SUBSCRIBE<\/span><span style=\"color: #D8DEE9FF\"> <\/span><span style=\"color: #A3BE8C\">=<\/span><span style=\"color: #D8DEE9FF\"> <\/span><span style=\"color: #B48EAD\">10021<\/span><\/span>\n<span class=\"line\"><span style=\"color: #88C0D0\">PUSH_NEWS<\/span><span style=\"color: #D8DEE9FF\">     <\/span><span style=\"color: #A3BE8C\">=<\/span><span style=\"color: #D8DEE9FF\"> <\/span><span style=\"color: #B48EAD\">10022<\/span><\/span>\n<span class=\"line\"><span style=\"color: #88C0D0\">HANDSHAKE_OK<\/span><span style=\"color: #D8DEE9FF\">  <\/span><span style=\"color: #A3BE8C\">=<\/span><span style=\"color: #D8DEE9FF\"> <\/span><span style=\"color: #B48EAD\">200<\/span><\/span>\n<span class=\"line\"><\/span>\n<span class=\"line\"><\/span>\n<span class=\"line\"><span style=\"color: #88C0D0\">class<\/span><span style=\"color: #D8DEE9FF\"> <\/span><span style=\"color: #A3BE8C\">NewsClient:<\/span><\/span>\n<span class=\"line\"><span style=\"color: #D8DEE9FF\">    <\/span><span style=\"color: #88C0D0\">&quot;&quot;&quot;Infoway real-time news WebSocket client.<\/span><\/span>\n<span class=\"line\"><\/span>\n<span class=\"line\"><span style=\"color: #88C0D0\">    One connection, one language. Handles heartbeat, reconnect with<\/span><\/span>\n<span class=\"line\"><span style=\"color: #88C0D0\">    exponential backoff, re-subscribe on every new connection, and dedup.<\/span><\/span>\n<span class=\"line\"><span style=\"color: #88C0D0\">    &quot;&quot;&quot;<\/span><\/span>\n<span class=\"line\"><\/span>\n<span class=\"line\"><span style=\"color: #D8DEE9FF\">    <\/span><span style=\"color: #88C0D0\">def<\/span><span style=\"color: #D8DEE9FF\"> <\/span><span style=\"color: #A3BE8C\">__init__<\/span><span style=\"color: #ECEFF4\">(<\/span><span style=\"color: #88C0D0\">self,<\/span><span style=\"color: #D8DEE9FF\"> <\/span><span style=\"color: #A3BE8C\">api_key:<\/span><span style=\"color: #D8DEE9FF\"> <\/span><span style=\"color: #A3BE8C\">str,<\/span><span style=\"color: #D8DEE9FF\"> <\/span><span style=\"color: #A3BE8C\">lang:<\/span><span style=\"color: #D8DEE9FF\"> <\/span><span style=\"color: #A3BE8C\">str<\/span><span style=\"color: #D8DEE9FF\"> <\/span><span style=\"color: #A3BE8C\">=<\/span><span style=\"color: #D8DEE9FF\"> <\/span><span style=\"color: #ECEFF4\">&quot;<\/span><span style=\"color: #A3BE8C\">en<\/span><span style=\"color: #ECEFF4\">&quot;<\/span><span style=\"color: #ECEFF4\">)<\/span><span style=\"color: #A3BE8C\">:<\/span><\/span>\n<span class=\"line\"><span style=\"color: #D8DEE9FF\">        <\/span><span style=\"color: #88C0D0\">self.url<\/span><span style=\"color: #D8DEE9FF\"> <\/span><span style=\"color: #A3BE8C\">=<\/span><span style=\"color: #D8DEE9FF\"> <\/span><span style=\"color: #A3BE8C\">f<\/span><span style=\"color: #ECEFF4\">&quot;<\/span><span style=\"color: #A3BE8C\">wss:\/\/data.infoway.io\/news?apikey={api_key}<\/span><span style=\"color: #ECEFF4\">&quot;<\/span><\/span>\n<span class=\"line\"><span style=\"color: #D8DEE9FF\">        <\/span><span style=\"color: #88C0D0\">self.lang<\/span><span style=\"color: #D8DEE9FF\"> <\/span><span style=\"color: #A3BE8C\">=<\/span><span style=\"color: #D8DEE9FF\"> <\/span><span style=\"color: #A3BE8C\">lang<\/span><\/span>\n<span class=\"line\"><span style=\"color: #D8DEE9FF\">        <\/span><span style=\"color: #88C0D0\">self.ws:<\/span><span style=\"color: #D8DEE9FF\"> <\/span><span style=\"color: #A3BE8C\">Optional&#91;websockets.WebSocketClientProtocol&#93;<\/span><span style=\"color: #D8DEE9FF\"> <\/span><span style=\"color: #A3BE8C\">=<\/span><span style=\"color: #D8DEE9FF\"> <\/span><span style=\"color: #A3BE8C\">None<\/span><\/span>\n<span class=\"line\"><span style=\"color: #D8DEE9FF\">        <\/span><span style=\"color: #88C0D0\">self.running<\/span><span style=\"color: #D8DEE9FF\"> <\/span><span style=\"color: #A3BE8C\">=<\/span><span style=\"color: #D8DEE9FF\"> <\/span><span style=\"color: #A3BE8C\">True<\/span><\/span>\n<span class=\"line\"><span style=\"color: #D8DEE9FF\">        <\/span><span style=\"color: #88C0D0\">self._heartbeat:<\/span><span style=\"color: #D8DEE9FF\"> <\/span><span style=\"color: #A3BE8C\">Optional&#91;asyncio.Task&#93;<\/span><span style=\"color: #D8DEE9FF\"> <\/span><span style=\"color: #A3BE8C\">=<\/span><span style=\"color: #D8DEE9FF\"> <\/span><span style=\"color: #A3BE8C\">None<\/span><\/span>\n<span class=\"line\"><span style=\"color: #D8DEE9FF\">        <\/span><span style=\"color: #88C0D0\">self._seen:<\/span><span style=\"color: #D8DEE9FF\"> <\/span><span style=\"color: #A3BE8C\">set&#91;str&#93;<\/span><span style=\"color: #D8DEE9FF\"> <\/span><span style=\"color: #A3BE8C\">=<\/span><span style=\"color: #D8DEE9FF\"> <\/span><span style=\"color: #A3BE8C\">set<\/span><span style=\"color: #ECEFF4\">()<\/span><\/span>\n<span class=\"line\"><\/span>\n<span class=\"line\"><span style=\"color: #D8DEE9FF\">    <\/span><span style=\"color: #88C0D0\">@staticmethod<\/span><\/span>\n<span class=\"line\"><span style=\"color: #D8DEE9FF\">    <\/span><span style=\"color: #88C0D0\">def<\/span><span style=\"color: #D8DEE9FF\"> <\/span><span style=\"color: #A3BE8C\">_trace<\/span><span style=\"color: #ECEFF4\">()<\/span><span style=\"color: #D8DEE9FF\"> -<\/span><span style=\"color: #81A1C1\">&gt;<\/span><span style=\"color: #D8DEE9FF\"> <\/span><span style=\"color: #A3BE8C\">str:<\/span><\/span>\n<span class=\"line\"><span style=\"color: #D8DEE9FF\">        <\/span><span style=\"color: #81A1C1\">return<\/span><span style=\"color: #D8DEE9FF\"> <\/span><span style=\"color: #A3BE8C\">uuid.uuid4<\/span><span style=\"color: #ECEFF4\">()<\/span><span style=\"color: #A3BE8C\">.hex<\/span><\/span>\n<span class=\"line\"><\/span>\n<span class=\"line\"><span style=\"color: #D8DEE9FF\">    <\/span><span style=\"color: #88C0D0\">async<\/span><span style=\"color: #D8DEE9FF\"> <\/span><span style=\"color: #A3BE8C\">def<\/span><span style=\"color: #D8DEE9FF\"> <\/span><span style=\"color: #A3BE8C\">_send<\/span><span style=\"color: #ECEFF4\">(<\/span><span style=\"color: #88C0D0\">self,<\/span><span style=\"color: #D8DEE9FF\"> <\/span><span style=\"color: #A3BE8C\">msg:<\/span><span style=\"color: #D8DEE9FF\"> <\/span><span style=\"color: #A3BE8C\">dict<\/span><span style=\"color: #ECEFF4\">)<\/span><span style=\"color: #D8DEE9FF\"> -<\/span><span style=\"color: #81A1C1\">&gt;<\/span><span style=\"color: #D8DEE9FF\"> <\/span><span style=\"color: #A3BE8C\">None:<\/span><\/span>\n<span class=\"line\"><span style=\"color: #D8DEE9FF\">        <\/span><span style=\"color: #81A1C1\">if<\/span><span style=\"color: #D8DEE9FF\"> <\/span><span style=\"color: #88C0D0\">self.ws:<\/span><\/span>\n<span class=\"line\"><span style=\"color: #D8DEE9FF\">            <\/span><span style=\"color: #88C0D0\">await<\/span><span style=\"color: #D8DEE9FF\"> <\/span><span style=\"color: #A3BE8C\">self.ws.send<\/span><span style=\"color: #ECEFF4\">(<\/span><span style=\"color: #88C0D0\">json.dumps(msg<\/span><span style=\"color: #ECEFF4\">)<\/span><span style=\"color: #D8DEE9FF\">)<\/span><\/span>\n<span class=\"line\"><\/span>\n<span class=\"line\"><span style=\"color: #D8DEE9FF\">    <\/span><span style=\"color: #88C0D0\">async<\/span><span style=\"color: #D8DEE9FF\"> <\/span><span style=\"color: #A3BE8C\">def<\/span><span style=\"color: #D8DEE9FF\"> <\/span><span style=\"color: #A3BE8C\">_subscribe<\/span><span style=\"color: #ECEFF4\">(<\/span><span style=\"color: #88C0D0\">self<\/span><span style=\"color: #ECEFF4\">)<\/span><span style=\"color: #D8DEE9FF\"> -<\/span><span style=\"color: #81A1C1\">&gt;<\/span><span style=\"color: #D8DEE9FF\"> <\/span><span style=\"color: #A3BE8C\">None:<\/span><\/span>\n<span class=\"line\"><span style=\"color: #D8DEE9FF\">        <\/span><span style=\"color: #88C0D0\">await<\/span><span style=\"color: #D8DEE9FF\"> <\/span><span style=\"color: #A3BE8C\">self._send<\/span><span style=\"color: #ECEFF4\">(<\/span><span style=\"color: #D8DEE9FF\">{&quot;code&quot;: REQ_SUBSCRIBE, &quot;trace&quot;: self._trace(),<\/span><\/span>\n<span class=\"line\"><span style=\"color: #D8DEE9FF\">                          <\/span><span style=\"color: #88C0D0\">&quot;data&quot;<\/span><span style=\"color: #88C0D0\">:<\/span><span style=\"color: #D8DEE9FF\"> <\/span><span style=\"color: #A3BE8C\">{<\/span><span style=\"color: #ECEFF4\">&quot;<\/span><span style=\"color: #A3BE8C\">lang<\/span><span style=\"color: #ECEFF4\">&quot;<\/span><span style=\"color: #A3BE8C\">:<\/span><span style=\"color: #D8DEE9FF\"> <\/span><span style=\"color: #A3BE8C\">self.lang}}<\/span><span style=\"color: #ECEFF4\">)<\/span><\/span>\n<span class=\"line\"><span style=\"color: #D8DEE9FF\">        <\/span><span style=\"color: #88C0D0\">logger.info(<\/span><span style=\"color: #88C0D0\">&quot;Sent subscribe: lang=%s&quot;<\/span><span style=\"color: #88C0D0\">,<\/span><span style=\"color: #D8DEE9FF\"> <\/span><span style=\"color: #A3BE8C\">self.lang<\/span><span style=\"color: #D8DEE9FF\">)<\/span><\/span>\n<span class=\"line\"><\/span>\n<span class=\"line\"><span style=\"color: #D8DEE9FF\">    <\/span><span style=\"color: #616E88\"># --- heartbeat -------------------------------------------------------<\/span><\/span>\n<span class=\"line\"><span style=\"color: #D8DEE9FF\">    <\/span><span style=\"color: #88C0D0\">def<\/span><span style=\"color: #D8DEE9FF\"> <\/span><span style=\"color: #A3BE8C\">_start_heartbeat<\/span><span style=\"color: #ECEFF4\">(<\/span><span style=\"color: #88C0D0\">self<\/span><span style=\"color: #ECEFF4\">)<\/span><span style=\"color: #D8DEE9FF\"> -<\/span><span style=\"color: #81A1C1\">&gt;<\/span><span style=\"color: #D8DEE9FF\"> <\/span><span style=\"color: #A3BE8C\">None:<\/span><\/span>\n<span class=\"line\"><span style=\"color: #D8DEE9FF\">        <\/span><span style=\"color: #88C0D0\">self._stop_heartbeat<\/span><span style=\"color: #ECEFF4\">()<\/span><\/span>\n<span class=\"line\"><\/span>\n<span class=\"line\"><span style=\"color: #D8DEE9FF\">        async def beat<\/span><span style=\"color: #ECEFF4\">()<\/span><span style=\"color: #D8DEE9FF\">:<\/span><\/span>\n<span class=\"line\"><span style=\"color: #D8DEE9FF\">            <\/span><span style=\"color: #88C0D0\">try:<\/span><\/span>\n<span class=\"line\"><span style=\"color: #D8DEE9FF\">                <\/span><span style=\"color: #81A1C1\">while<\/span><span style=\"color: #D8DEE9FF\"> <\/span><span style=\"color: #88C0D0\">True:<\/span><\/span>\n<span class=\"line\"><span style=\"color: #D8DEE9FF\">                    <\/span><span style=\"color: #88C0D0\">await<\/span><span style=\"color: #D8DEE9FF\"> <\/span><span style=\"color: #A3BE8C\">asyncio.sleep<\/span><span style=\"color: #ECEFF4\">(<\/span><span style=\"color: #88C0D0\">30<\/span><span style=\"color: #ECEFF4\">)<\/span><\/span>\n<span class=\"line\"><span style=\"color: #D8DEE9FF\">                    <\/span><span style=\"color: #81A1C1\">if<\/span><span style=\"color: #D8DEE9FF\"> <\/span><span style=\"color: #88C0D0\">not<\/span><span style=\"color: #D8DEE9FF\"> <\/span><span style=\"color: #A3BE8C\">self.ws<\/span><span style=\"color: #D8DEE9FF\"> <\/span><span style=\"color: #A3BE8C\">or<\/span><span style=\"color: #D8DEE9FF\"> <\/span><span style=\"color: #A3BE8C\">self.ws.close_code<\/span><span style=\"color: #D8DEE9FF\"> <\/span><span style=\"color: #A3BE8C\">is<\/span><span style=\"color: #D8DEE9FF\"> <\/span><span style=\"color: #A3BE8C\">not<\/span><span style=\"color: #D8DEE9FF\"> <\/span><span style=\"color: #A3BE8C\">None:<\/span><\/span>\n<span class=\"line\"><span style=\"color: #D8DEE9FF\">                        <\/span><span style=\"color: #81A1C1\">break<\/span><\/span>\n<span class=\"line\"><span style=\"color: #D8DEE9FF\">                    <\/span><span style=\"color: #88C0D0\">await<\/span><span style=\"color: #D8DEE9FF\"> <\/span><span style=\"color: #A3BE8C\">self._send<\/span><span style=\"color: #ECEFF4\">(<\/span><span style=\"color: #D8DEE9FF\">{&quot;code&quot;: REQ_HEARTBEAT, &quot;trace&quot;: self._trace()})<\/span><\/span>\n<span class=\"line\"><span style=\"color: #D8DEE9FF\">            <\/span><span style=\"color: #88C0D0\">except<\/span><span style=\"color: #D8DEE9FF\"> (ConnectionClosed, <\/span><span style=\"color: #A3BE8C\">asyncio.CancelledError<\/span><span style=\"color: #ECEFF4\">)<\/span><span style=\"color: #A3BE8C\">:<\/span><\/span>\n<span class=\"line\"><span style=\"color: #D8DEE9FF\">                <\/span><span style=\"color: #88C0D0\">pass<\/span><\/span>\n<span class=\"line\"><\/span>\n<span class=\"line\"><span style=\"color: #D8DEE9FF\">        <\/span><span style=\"color: #88C0D0\">self._heartbeat<\/span><span style=\"color: #D8DEE9FF\"> <\/span><span style=\"color: #A3BE8C\">=<\/span><span style=\"color: #D8DEE9FF\"> <\/span><span style=\"color: #A3BE8C\">asyncio.create_task<\/span><span style=\"color: #ECEFF4\">(<\/span><span style=\"color: #88C0D0\">beat<\/span><span style=\"color: #ECEFF4\">())<\/span><\/span>\n<span class=\"line\"><\/span>\n<span class=\"line\"><span style=\"color: #D8DEE9FF\">    <\/span><span style=\"color: #88C0D0\">def<\/span><span style=\"color: #D8DEE9FF\"> <\/span><span style=\"color: #A3BE8C\">_stop_heartbeat<\/span><span style=\"color: #ECEFF4\">(<\/span><span style=\"color: #88C0D0\">self<\/span><span style=\"color: #ECEFF4\">)<\/span><span style=\"color: #D8DEE9FF\"> -<\/span><span style=\"color: #81A1C1\">&gt;<\/span><span style=\"color: #D8DEE9FF\"> <\/span><span style=\"color: #A3BE8C\">None:<\/span><\/span>\n<span class=\"line\"><span style=\"color: #D8DEE9FF\">        <\/span><span style=\"color: #81A1C1\">if<\/span><span style=\"color: #D8DEE9FF\"> <\/span><span style=\"color: #88C0D0\">self._heartbeat<\/span><span style=\"color: #D8DEE9FF\"> <\/span><span style=\"color: #A3BE8C\">and<\/span><span style=\"color: #D8DEE9FF\"> <\/span><span style=\"color: #A3BE8C\">not<\/span><span style=\"color: #D8DEE9FF\"> <\/span><span style=\"color: #A3BE8C\">self._heartbeat.done<\/span><span style=\"color: #ECEFF4\">()<\/span><span style=\"color: #A3BE8C\">:<\/span><\/span>\n<span class=\"line\"><span style=\"color: #D8DEE9FF\">            <\/span><span style=\"color: #88C0D0\">self._heartbeat.cancel<\/span><span style=\"color: #ECEFF4\">()<\/span><\/span>\n<span class=\"line\"><span style=\"color: #D8DEE9FF\">        self._heartbeat = None<\/span><\/span>\n<span class=\"line\"><\/span>\n<span class=\"line\"><span style=\"color: #D8DEE9FF\">    # --- message handling ----------------------------------------------<\/span><\/span>\n<span class=\"line\"><span style=\"color: #D8DEE9FF\">    def _on_news<\/span><span style=\"color: #ECEFF4\">(<\/span><span style=\"color: #88C0D0\">self,<\/span><span style=\"color: #D8DEE9FF\"> <\/span><span style=\"color: #A3BE8C\">n:<\/span><span style=\"color: #D8DEE9FF\"> <\/span><span style=\"color: #A3BE8C\">dict<\/span><span style=\"color: #ECEFF4\">)<\/span><span style=\"color: #D8DEE9FF\"> -<\/span><span style=\"color: #81A1C1\">&gt;<\/span><span style=\"color: #D8DEE9FF\"> None:<\/span><\/span>\n<span class=\"line\"><span style=\"color: #D8DEE9FF\">        <\/span><span style=\"color: #88C0D0\">dk<\/span><span style=\"color: #D8DEE9FF\"> <\/span><span style=\"color: #A3BE8C\">=<\/span><span style=\"color: #D8DEE9FF\"> <\/span><span style=\"color: #A3BE8C\">n.get<\/span><span style=\"color: #ECEFF4\">(<\/span><span style=\"color: #88C0D0\">&quot;dk&quot;<\/span><span style=\"color: #ECEFF4\">)<\/span><\/span>\n<span class=\"line\"><span style=\"color: #D8DEE9FF\">        <\/span><span style=\"color: #81A1C1\">if<\/span><span style=\"color: #D8DEE9FF\"> <\/span><span style=\"color: #88C0D0\">dk<\/span><span style=\"color: #D8DEE9FF\"> <\/span><span style=\"color: #A3BE8C\">and<\/span><span style=\"color: #D8DEE9FF\"> <\/span><span style=\"color: #A3BE8C\">dk<\/span><span style=\"color: #D8DEE9FF\"> <\/span><span style=\"color: #A3BE8C\">in<\/span><span style=\"color: #D8DEE9FF\"> <\/span><span style=\"color: #A3BE8C\">self._seen:<\/span><\/span>\n<span class=\"line\"><span style=\"color: #D8DEE9FF\">            <\/span><span style=\"color: #81A1C1\">return<\/span><\/span>\n<span class=\"line\"><span style=\"color: #D8DEE9FF\">        <\/span><span style=\"color: #81A1C1\">if<\/span><span style=\"color: #D8DEE9FF\"> <\/span><span style=\"color: #88C0D0\">dk:<\/span><\/span>\n<span class=\"line\"><span style=\"color: #D8DEE9FF\">            <\/span><span style=\"color: #88C0D0\">self._seen.add(dk<\/span><span style=\"color: #D8DEE9FF\">)<\/span><\/span>\n<span class=\"line\"><\/span>\n<span class=\"line\"><span style=\"color: #D8DEE9FF\">        <\/span><span style=\"color: #88C0D0\">symbols<\/span><span style=\"color: #D8DEE9FF\"> <\/span><span style=\"color: #A3BE8C\">=<\/span><span style=\"color: #D8DEE9FF\"> <\/span><span style=\"color: #ECEFF4\">&quot;<\/span><span style=\"color: #A3BE8C\">, <\/span><span style=\"color: #ECEFF4\">&quot;<\/span><span style=\"color: #A3BE8C\">.join<\/span><span style=\"color: #ECEFF4\">(<\/span><span style=\"color: #88C0D0\">n.get(<\/span><span style=\"color: #88C0D0\">&quot;symbols&quot;<\/span><span style=\"color: #ECEFF4\">)<\/span><span style=\"color: #D8DEE9FF\"> <\/span><span style=\"color: #A3BE8C\">or<\/span><span style=\"color: #D8DEE9FF\"> []) or <\/span><span style=\"color: #ECEFF4\">&quot;<\/span><span style=\"color: #A3BE8C\">\u2014<\/span><span style=\"color: #ECEFF4\">&quot;<\/span><\/span>\n<span class=\"line\"><span style=\"color: #D8DEE9FF\">        <\/span><span style=\"color: #88C0D0\">logger.info(<\/span><span style=\"color: #88C0D0\">&quot;NEWS  urgency=%s  provider=%s  symbols=%s\\n      %s&quot;<\/span><span style=\"color: #88C0D0\">,<\/span><\/span>\n<span class=\"line\"><span style=\"color: #D8DEE9FF\">                    <\/span><span style=\"color: #88C0D0\">n.get(<\/span><span style=\"color: #88C0D0\">&quot;urgency&quot;<\/span><span style=\"color: #D8DEE9FF\">), n.get<\/span><span style=\"color: #ECEFF4\">(<\/span><span style=\"color: #88C0D0\">&quot;provider&quot;<\/span><span style=\"color: #ECEFF4\">)<\/span><span style=\"color: #D8DEE9FF\">, symbols, n.get<\/span><span style=\"color: #ECEFF4\">(<\/span><span style=\"color: #88C0D0\">&quot;title&quot;<\/span><span style=\"color: #ECEFF4\">)<\/span><span style=\"color: #D8DEE9FF\">)<\/span><\/span>\n<span class=\"line\"><span style=\"color: #D8DEE9FF\">        <\/span><span style=\"color: #616E88\"># TODO: persist \/ publish to a queue \/ trigger strategy logic<\/span><\/span>\n<span class=\"line\"><\/span>\n<span class=\"line\"><span style=\"color: #D8DEE9FF\">    <\/span><span style=\"color: #88C0D0\">def<\/span><span style=\"color: #D8DEE9FF\"> <\/span><span style=\"color: #A3BE8C\">_dispatch<\/span><span style=\"color: #ECEFF4\">(<\/span><span style=\"color: #88C0D0\">self,<\/span><span style=\"color: #D8DEE9FF\"> <\/span><span style=\"color: #A3BE8C\">raw:<\/span><span style=\"color: #D8DEE9FF\"> <\/span><span style=\"color: #A3BE8C\">str<\/span><span style=\"color: #ECEFF4\">)<\/span><span style=\"color: #D8DEE9FF\"> -<\/span><span style=\"color: #81A1C1\">&gt;<\/span><span style=\"color: #D8DEE9FF\"> <\/span><span style=\"color: #A3BE8C\">None:<\/span><\/span>\n<span class=\"line\"><span style=\"color: #D8DEE9FF\">        <\/span><span style=\"color: #88C0D0\">try:<\/span><\/span>\n<span class=\"line\"><span style=\"color: #D8DEE9FF\">            <\/span><span style=\"color: #88C0D0\">msg<\/span><span style=\"color: #D8DEE9FF\"> <\/span><span style=\"color: #A3BE8C\">=<\/span><span style=\"color: #D8DEE9FF\"> <\/span><span style=\"color: #A3BE8C\">json.loads<\/span><span style=\"color: #ECEFF4\">(<\/span><span style=\"color: #88C0D0\">raw<\/span><span style=\"color: #ECEFF4\">)<\/span><\/span>\n<span class=\"line\"><span style=\"color: #D8DEE9FF\">        <\/span><span style=\"color: #88C0D0\">except<\/span><span style=\"color: #D8DEE9FF\"> <\/span><span style=\"color: #A3BE8C\">json.JSONDecodeError:<\/span><\/span>\n<span class=\"line\"><span style=\"color: #D8DEE9FF\">            <\/span><span style=\"color: #88C0D0\">logger.warning(<\/span><span style=\"color: #88C0D0\">&quot;non-JSON frame: %r&quot;<\/span><span style=\"color: #88C0D0\">,<\/span><span style=\"color: #D8DEE9FF\"> <\/span><span style=\"color: #A3BE8C\">raw<\/span><span style=\"color: #D8DEE9FF\">)<\/span><\/span>\n<span class=\"line\"><span style=\"color: #D8DEE9FF\">            <\/span><span style=\"color: #81A1C1\">return<\/span><\/span>\n<span class=\"line\"><\/span>\n<span class=\"line\"><span style=\"color: #D8DEE9FF\">        <\/span><span style=\"color: #88C0D0\">code<\/span><span style=\"color: #D8DEE9FF\"> <\/span><span style=\"color: #A3BE8C\">=<\/span><span style=\"color: #D8DEE9FF\"> <\/span><span style=\"color: #A3BE8C\">msg.get<\/span><span style=\"color: #ECEFF4\">(<\/span><span style=\"color: #88C0D0\">&quot;code&quot;<\/span><span style=\"color: #ECEFF4\">)<\/span><\/span>\n<span class=\"line\"><span style=\"color: #D8DEE9FF\">        <\/span><span style=\"color: #81A1C1\">if<\/span><span style=\"color: #D8DEE9FF\"> <\/span><span style=\"color: #88C0D0\">code<\/span><span style=\"color: #D8DEE9FF\"> <\/span><span style=\"color: #A3BE8C\">==<\/span><span style=\"color: #D8DEE9FF\"> <\/span><span style=\"color: #A3BE8C\">PUSH_NEWS:<\/span><\/span>\n<span class=\"line\"><span style=\"color: #D8DEE9FF\">            <\/span><span style=\"color: #88C0D0\">self._on_news(msg.get(<\/span><span style=\"color: #88C0D0\">&quot;data&quot;<\/span><span style=\"color: #88C0D0\">,<\/span><span style=\"color: #D8DEE9FF\"> <\/span><span style=\"color: #A3BE8C\">{}<\/span><span style=\"color: #D8DEE9FF\">))<\/span><\/span>\n<span class=\"line\"><span style=\"color: #D8DEE9FF\">        <\/span><span style=\"color: #81A1C1\">elif<\/span><span style=\"color: #D8DEE9FF\"> <\/span><span style=\"color: #88C0D0\">code<\/span><span style=\"color: #D8DEE9FF\"> <\/span><span style=\"color: #A3BE8C\">==<\/span><span style=\"color: #D8DEE9FF\"> <\/span><span style=\"color: #A3BE8C\">ACK_SUBSCRIBE:<\/span><\/span>\n<span class=\"line\"><span style=\"color: #D8DEE9FF\">            <\/span><span style=\"color: #88C0D0\">logger.info(<\/span><span style=\"color: #88C0D0\">&quot;Subscription confirmed: lang=%s&quot;<\/span><span style=\"color: #88C0D0\">,<\/span><\/span>\n<span class=\"line\"><span style=\"color: #D8DEE9FF\">                        <\/span><span style=\"color: #88C0D0\">msg.get(<\/span><span style=\"color: #88C0D0\">&quot;data&quot;<\/span><span style=\"color: #88C0D0\">,<\/span><span style=\"color: #D8DEE9FF\"> <\/span><span style=\"color: #A3BE8C\">{}<\/span><span style=\"color: #D8DEE9FF\">).get<\/span><span style=\"color: #ECEFF4\">(<\/span><span style=\"color: #88C0D0\">&quot;lang&quot;<\/span><span style=\"color: #ECEFF4\">)<\/span><span style=\"color: #D8DEE9FF\">)<\/span><\/span>\n<span class=\"line\"><span style=\"color: #D8DEE9FF\">        <\/span><span style=\"color: #81A1C1\">elif<\/span><span style=\"color: #D8DEE9FF\"> <\/span><span style=\"color: #88C0D0\">code<\/span><span style=\"color: #D8DEE9FF\"> <\/span><span style=\"color: #A3BE8C\">==<\/span><span style=\"color: #D8DEE9FF\"> <\/span><span style=\"color: #A3BE8C\">HANDSHAKE_OK:<\/span><\/span>\n<span class=\"line\"><span style=\"color: #D8DEE9FF\">            <\/span><span style=\"color: #88C0D0\">logger.info(<\/span><span style=\"color: #88C0D0\">&quot;Handshake OK: %s&quot;<\/span><span style=\"color: #88C0D0\">,<\/span><span style=\"color: #D8DEE9FF\"> <\/span><span style=\"color: #A3BE8C\">msg.get<\/span><span style=\"color: #ECEFF4\">(<\/span><span style=\"color: #88C0D0\">&quot;msg&quot;<\/span><span style=\"color: #ECEFF4\">)<\/span><span style=\"color: #D8DEE9FF\">)<\/span><\/span>\n<span class=\"line\"><span style=\"color: #D8DEE9FF\">        else:<\/span><\/span>\n<span class=\"line\"><span style=\"color: #D8DEE9FF\">            <\/span><span style=\"color: #88C0D0\">logger.debug(<\/span><span style=\"color: #88C0D0\">&quot;unhandled code=%s: %s&quot;<\/span><span style=\"color: #88C0D0\">,<\/span><span style=\"color: #D8DEE9FF\"> <\/span><span style=\"color: #A3BE8C\">code,<\/span><span style=\"color: #D8DEE9FF\"> <\/span><span style=\"color: #A3BE8C\">msg<\/span><span style=\"color: #D8DEE9FF\">)<\/span><\/span>\n<span class=\"line\"><\/span>\n<span class=\"line\"><span style=\"color: #D8DEE9FF\">    <\/span><span style=\"color: #616E88\"># --- connection lifecycle -----------------------------------------<\/span><\/span>\n<span class=\"line\"><span style=\"color: #D8DEE9FF\">    <\/span><span style=\"color: #88C0D0\">async<\/span><span style=\"color: #D8DEE9FF\"> <\/span><span style=\"color: #A3BE8C\">def<\/span><span style=\"color: #D8DEE9FF\"> <\/span><span style=\"color: #A3BE8C\">_connect_once<\/span><span style=\"color: #ECEFF4\">(<\/span><span style=\"color: #88C0D0\">self<\/span><span style=\"color: #ECEFF4\">)<\/span><span style=\"color: #D8DEE9FF\"> -<\/span><span style=\"color: #81A1C1\">&gt;<\/span><span style=\"color: #D8DEE9FF\"> <\/span><span style=\"color: #A3BE8C\">None:<\/span><\/span>\n<span class=\"line\"><span style=\"color: #D8DEE9FF\">        <\/span><span style=\"color: #88C0D0\">async<\/span><span style=\"color: #D8DEE9FF\"> <\/span><span style=\"color: #A3BE8C\">with<\/span><span style=\"color: #D8DEE9FF\"> <\/span><span style=\"color: #A3BE8C\">websockets.connect<\/span><span style=\"color: #ECEFF4\">(<\/span><span style=\"color: #88C0D0\">self.url,<\/span><span style=\"color: #D8DEE9FF\"> <\/span><span style=\"color: #A3BE8C\">ping_interval=None<\/span><span style=\"color: #ECEFF4\">)<\/span><span style=\"color: #D8DEE9FF\"> <\/span><span style=\"color: #A3BE8C\">as<\/span><span style=\"color: #D8DEE9FF\"> <\/span><span style=\"color: #A3BE8C\">ws:<\/span><\/span>\n<span class=\"line\"><span style=\"color: #D8DEE9FF\">            <\/span><span style=\"color: #88C0D0\">self.ws<\/span><span style=\"color: #D8DEE9FF\"> <\/span><span style=\"color: #A3BE8C\">=<\/span><span style=\"color: #D8DEE9FF\"> <\/span><span style=\"color: #A3BE8C\">ws<\/span><\/span>\n<span class=\"line\"><span style=\"color: #D8DEE9FF\">            <\/span><span style=\"color: #88C0D0\">logger.info(<\/span><span style=\"color: #88C0D0\">&quot;Connected&quot;<\/span><span style=\"color: #D8DEE9FF\">)<\/span><\/span>\n<span class=\"line\"><span style=\"color: #D8DEE9FF\">            <\/span><span style=\"color: #88C0D0\">await<\/span><span style=\"color: #D8DEE9FF\"> <\/span><span style=\"color: #A3BE8C\">self._subscribe<\/span><span style=\"color: #ECEFF4\">()<\/span><span style=\"color: #D8DEE9FF\">          <\/span><span style=\"color: #616E88\"># MUST re-subscribe on every connection<\/span><\/span>\n<span class=\"line\"><span style=\"color: #D8DEE9FF\">            <\/span><span style=\"color: #88C0D0\">self._start_heartbeat<\/span><span style=\"color: #ECEFF4\">()<\/span><\/span>\n<span class=\"line\"><span style=\"color: #D8DEE9FF\">            try:<\/span><\/span>\n<span class=\"line\"><span style=\"color: #D8DEE9FF\">                async for message in ws:<\/span><\/span>\n<span class=\"line\"><span style=\"color: #D8DEE9FF\">                    self._dispatch<\/span><span style=\"color: #ECEFF4\">(<\/span><span style=\"color: #88C0D0\">message<\/span><span style=\"color: #ECEFF4\">)<\/span><\/span>\n<span class=\"line\"><span style=\"color: #D8DEE9FF\">            <\/span><span style=\"color: #88C0D0\">finally:<\/span><\/span>\n<span class=\"line\"><span style=\"color: #D8DEE9FF\">                <\/span><span style=\"color: #88C0D0\">self._stop_heartbeat<\/span><span style=\"color: #ECEFF4\">()<\/span><\/span>\n<span class=\"line\"><span style=\"color: #D8DEE9FF\">                self.ws = None<\/span><\/span>\n<span class=\"line\"><\/span>\n<span class=\"line\"><span style=\"color: #D8DEE9FF\">    async def start<\/span><span style=\"color: #ECEFF4\">(<\/span><span style=\"color: #88C0D0\">self<\/span><span style=\"color: #ECEFF4\">)<\/span><span style=\"color: #D8DEE9FF\"> -<\/span><span style=\"color: #81A1C1\">&gt;<\/span><span style=\"color: #D8DEE9FF\"> None:<\/span><\/span>\n<span class=\"line\"><span style=\"color: #D8DEE9FF\">        <\/span><span style=\"color: #88C0D0\">backoff,<\/span><span style=\"color: #D8DEE9FF\"> <\/span><span style=\"color: #A3BE8C\">backoff_max<\/span><span style=\"color: #D8DEE9FF\"> <\/span><span style=\"color: #A3BE8C\">=<\/span><span style=\"color: #D8DEE9FF\"> <\/span><span style=\"color: #B48EAD\">5<\/span><span style=\"color: #A3BE8C\">,<\/span><span style=\"color: #D8DEE9FF\"> <\/span><span style=\"color: #B48EAD\">60<\/span><\/span>\n<span class=\"line\"><span style=\"color: #D8DEE9FF\">        <\/span><span style=\"color: #81A1C1\">while<\/span><span style=\"color: #D8DEE9FF\"> <\/span><span style=\"color: #88C0D0\">self.running:<\/span><\/span>\n<span class=\"line\"><span style=\"color: #D8DEE9FF\">            <\/span><span style=\"color: #88C0D0\">try:<\/span><\/span>\n<span class=\"line\"><span style=\"color: #D8DEE9FF\">                <\/span><span style=\"color: #88C0D0\">await<\/span><span style=\"color: #D8DEE9FF\"> <\/span><span style=\"color: #A3BE8C\">self._connect_once<\/span><span style=\"color: #ECEFF4\">()<\/span><\/span>\n<span class=\"line\"><span style=\"color: #D8DEE9FF\">                <\/span><span style=\"color: #88C0D0\">backoff<\/span><span style=\"color: #D8DEE9FF\"> <\/span><span style=\"color: #A3BE8C\">=<\/span><span style=\"color: #D8DEE9FF\"> <\/span><span style=\"color: #B48EAD\">5<\/span><\/span>\n<span class=\"line\"><span style=\"color: #D8DEE9FF\">                <\/span><span style=\"color: #88C0D0\">logger.warning(<\/span><span style=\"color: #88C0D0\">&quot;Connection closed by server&quot;<\/span><span style=\"color: #D8DEE9FF\">)<\/span><\/span>\n<span class=\"line\"><span style=\"color: #D8DEE9FF\">            <\/span><span style=\"color: #88C0D0\">except<\/span><span style=\"color: #D8DEE9FF\"> <\/span><span style=\"color: #A3BE8C\">ConnectionClosed<\/span><span style=\"color: #D8DEE9FF\"> <\/span><span style=\"color: #A3BE8C\">as<\/span><span style=\"color: #D8DEE9FF\"> <\/span><span style=\"color: #A3BE8C\">e:<\/span><\/span>\n<span class=\"line\"><span style=\"color: #D8DEE9FF\">                <\/span><span style=\"color: #88C0D0\">logger.warning(<\/span><span style=\"color: #88C0D0\">&quot;Connection closed: %s&quot;<\/span><span style=\"color: #88C0D0\">,<\/span><span style=\"color: #D8DEE9FF\"> <\/span><span style=\"color: #A3BE8C\">e<\/span><span style=\"color: #D8DEE9FF\">)<\/span><\/span>\n<span class=\"line\"><span style=\"color: #D8DEE9FF\">            <\/span><span style=\"color: #88C0D0\">except<\/span><span style=\"color: #D8DEE9FF\"> <\/span><span style=\"color: #A3BE8C\">Exception<\/span><span style=\"color: #D8DEE9FF\"> <\/span><span style=\"color: #A3BE8C\">as<\/span><span style=\"color: #D8DEE9FF\"> <\/span><span style=\"color: #A3BE8C\">e:<\/span><\/span>\n<span class=\"line\"><span style=\"color: #D8DEE9FF\">                <\/span><span style=\"color: #88C0D0\">logger.error(<\/span><span style=\"color: #88C0D0\">&quot;Connection error: %s&quot;<\/span><span style=\"color: #88C0D0\">,<\/span><span style=\"color: #D8DEE9FF\"> <\/span><span style=\"color: #A3BE8C\">e<\/span><span style=\"color: #D8DEE9FF\">)<\/span><\/span>\n<span class=\"line\"><span style=\"color: #D8DEE9FF\">            <\/span><span style=\"color: #81A1C1\">if<\/span><span style=\"color: #D8DEE9FF\"> <\/span><span style=\"color: #88C0D0\">not<\/span><span style=\"color: #D8DEE9FF\"> <\/span><span style=\"color: #A3BE8C\">self.running:<\/span><\/span>\n<span class=\"line\"><span style=\"color: #D8DEE9FF\">                <\/span><span style=\"color: #81A1C1\">break<\/span><\/span>\n<span class=\"line\"><span style=\"color: #D8DEE9FF\">            <\/span><span style=\"color: #88C0D0\">logger.info(<\/span><span style=\"color: #88C0D0\">&quot;Reconnecting in %ss...&quot;<\/span><span style=\"color: #88C0D0\">,<\/span><span style=\"color: #D8DEE9FF\"> <\/span><span style=\"color: #A3BE8C\">backoff<\/span><span style=\"color: #D8DEE9FF\">)<\/span><\/span>\n<span class=\"line\"><span style=\"color: #D8DEE9FF\">            <\/span><span style=\"color: #88C0D0\">await<\/span><span style=\"color: #D8DEE9FF\"> <\/span><span style=\"color: #A3BE8C\">asyncio.sleep<\/span><span style=\"color: #ECEFF4\">(<\/span><span style=\"color: #88C0D0\">backoff<\/span><span style=\"color: #ECEFF4\">)<\/span><\/span>\n<span class=\"line\"><span style=\"color: #D8DEE9FF\">            <\/span><span style=\"color: #88C0D0\">backoff<\/span><span style=\"color: #D8DEE9FF\"> <\/span><span style=\"color: #A3BE8C\">=<\/span><span style=\"color: #D8DEE9FF\"> <\/span><span style=\"color: #A3BE8C\">min<\/span><span style=\"color: #ECEFF4\">(<\/span><span style=\"color: #88C0D0\">backoff<\/span><span style=\"color: #D8DEE9FF\"> <\/span><span style=\"color: #81A1C1\">*<\/span><span style=\"color: #D8DEE9FF\"> <\/span><span style=\"color: #B48EAD\">2<\/span><span style=\"color: #A3BE8C\">,<\/span><span style=\"color: #D8DEE9FF\"> <\/span><span style=\"color: #A3BE8C\">backoff_max<\/span><span style=\"color: #ECEFF4\">)<\/span><\/span>\n<span class=\"line\"><\/span>\n<span class=\"line\"><span style=\"color: #D8DEE9FF\">    <\/span><span style=\"color: #88C0D0\">def<\/span><span style=\"color: #D8DEE9FF\"> <\/span><span style=\"color: #A3BE8C\">stop<\/span><span style=\"color: #ECEFF4\">(<\/span><span style=\"color: #88C0D0\">self<\/span><span style=\"color: #ECEFF4\">)<\/span><span style=\"color: #D8DEE9FF\"> -<\/span><span style=\"color: #81A1C1\">&gt;<\/span><span style=\"color: #D8DEE9FF\"> <\/span><span style=\"color: #A3BE8C\">None:<\/span><\/span>\n<span class=\"line\"><span style=\"color: #D8DEE9FF\">        <\/span><span style=\"color: #88C0D0\">self.running<\/span><span style=\"color: #D8DEE9FF\"> <\/span><span style=\"color: #A3BE8C\">=<\/span><span style=\"color: #D8DEE9FF\"> <\/span><span style=\"color: #A3BE8C\">False<\/span><\/span>\n<span class=\"line\"><\/span>\n<span class=\"line\"><\/span>\n<span class=\"line\"><span style=\"color: #88C0D0\">async<\/span><span style=\"color: #D8DEE9FF\"> <\/span><span style=\"color: #A3BE8C\">def<\/span><span style=\"color: #D8DEE9FF\"> <\/span><span style=\"color: #A3BE8C\">main<\/span><span style=\"color: #ECEFF4\">()<\/span><span style=\"color: #A3BE8C\">:<\/span><\/span>\n<span class=\"line\"><span style=\"color: #D8DEE9FF\">    <\/span><span style=\"color: #88C0D0\">api_key<\/span><span style=\"color: #D8DEE9FF\"> <\/span><span style=\"color: #A3BE8C\">=<\/span><span style=\"color: #D8DEE9FF\"> <\/span><span style=\"color: #A3BE8C\">os.environ.get<\/span><span style=\"color: #ECEFF4\">(<\/span><span style=\"color: #88C0D0\">&quot;INFOWAY_API_KEY&quot;<\/span><span style=\"color: #88C0D0\">,<\/span><span style=\"color: #D8DEE9FF\"> <\/span><span style=\"color: #ECEFF4\">&quot;<\/span><span style=\"color: #A3BE8C\">YOUR_API_KEY<\/span><span style=\"color: #ECEFF4\">&quot;<\/span><span style=\"color: #ECEFF4\">)<\/span><\/span>\n<span class=\"line\"><span style=\"color: #D8DEE9FF\">    <\/span><span style=\"color: #88C0D0\">await<\/span><span style=\"color: #D8DEE9FF\"> <\/span><span style=\"color: #A3BE8C\">NewsClient<\/span><span style=\"color: #ECEFF4\">(<\/span><span style=\"color: #88C0D0\">api_key,<\/span><span style=\"color: #D8DEE9FF\"> <\/span><span style=\"color: #A3BE8C\">lang=<\/span><span style=\"color: #ECEFF4\">&quot;<\/span><span style=\"color: #A3BE8C\">en<\/span><span style=\"color: #ECEFF4\">&quot;<\/span><span style=\"color: #ECEFF4\">)<\/span><span style=\"color: #A3BE8C\">.start<\/span><span style=\"color: #ECEFF4\">()<\/span><\/span>\n<span class=\"line\"><\/span>\n<span class=\"line\"><\/span>\n<span class=\"line\"><span style=\"color: #81A1C1\">if<\/span><span style=\"color: #D8DEE9FF\"> <\/span><span style=\"color: #88C0D0\">__name__<\/span><span style=\"color: #D8DEE9FF\"> <\/span><span style=\"color: #A3BE8C\">==<\/span><span style=\"color: #D8DEE9FF\"> <\/span><span style=\"color: #ECEFF4\">&quot;<\/span><span style=\"color: #A3BE8C\">__main__<\/span><span style=\"color: #ECEFF4\">&quot;<\/span><span style=\"color: #A3BE8C\">:<\/span><\/span>\n<span class=\"line\"><span style=\"color: #D8DEE9FF\">    <\/span><span style=\"color: #88C0D0\">try:<\/span><\/span>\n<span class=\"line\"><span style=\"color: #D8DEE9FF\">        <\/span><span style=\"color: #88C0D0\">asyncio.run(main(<\/span><span style=\"color: #D8DEE9FF\">))<\/span><\/span>\n<span class=\"line\"><span style=\"color: #D8DEE9FF\">    <\/span><span style=\"color: #88C0D0\">except<\/span><span style=\"color: #D8DEE9FF\"> <\/span><span style=\"color: #A3BE8C\">KeyboardInterrupt:<\/span><\/span>\n<span class=\"line\"><span style=\"color: #D8DEE9FF\">        <\/span><span style=\"color: #88C0D0\">logger.info(<\/span><span style=\"color: #88C0D0\">&quot;stopped&quot;<\/span><span style=\"color: #D8DEE9FF\">)<\/span><\/span><\/code><\/pre><\/div>\n\n\n\n<h3 class=\"wp-block-heading\">Running it<\/h3>\n\n\n\n<div class=\"wp-block-kevinbatdorf-code-block-pro\" data-code-block-pro-font-family=\"Code-Pro-JetBrains-Mono\" style=\"font-size:.875rem;font-family:Code-Pro-JetBrains-Mono,ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;line-height:1.25rem;--cbp-tab-width:2;tab-size:var(--cbp-tab-width, 2)\"><span style=\"display:block;padding:16px 0 0 16px;margin-bottom:-1px;width:100%;text-align:left;background-color:#2e3440ff\"><svg xmlns=\"http:\/\/www.w3.org\/2000\/svg\" width=\"54\" height=\"14\" viewBox=\"0 0 54 14\"><g fill=\"none\" fill-rule=\"evenodd\" transform=\"translate(1 1)\"><circle cx=\"6\" cy=\"6\" r=\"6\" fill=\"#FF5F56\" stroke=\"#E0443E\" stroke-width=\".5\"><\/circle><circle cx=\"26\" cy=\"6\" r=\"6\" fill=\"#FFBD2E\" stroke=\"#DEA123\" stroke-width=\".5\"><\/circle><circle cx=\"46\" cy=\"6\" r=\"6\" fill=\"#27C93F\" stroke=\"#1AAB29\" stroke-width=\".5\"><\/circle><\/g><\/svg><\/span><span role=\"button\" tabindex=\"0\" style=\"color:#d8dee9ff;display:none\" aria-label=\"Copy\" class=\"code-block-pro-copy-button\"><pre class=\"code-block-pro-copy-button-pre\" aria-hidden=\"true\"><textarea class=\"code-block-pro-copy-button-textarea\" tabindex=\"-1\" aria-hidden=\"true\" readonly>pip install websockets\nexport INFOWAY_API_KEY=\"your_key_here\"\npython infoway_news.py<\/textarea><\/pre><svg xmlns=\"http:\/\/www.w3.org\/2000\/svg\" style=\"width:24px;height:24px\" fill=\"none\" viewBox=\"0 0 24 24\" stroke=\"currentColor\" stroke-width=\"2\"><path class=\"with-check\" stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2m-6 9l2 2 4-4\"><\/path><path class=\"without-check\" stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2\"><\/path><\/svg><\/span><pre class=\"shiki nord\" style=\"background-color: #2e3440ff\" tabindex=\"0\"><code><span class=\"line\"><span style=\"color: #88C0D0\">pip<\/span><span style=\"color: #D8DEE9FF\"> <\/span><span style=\"color: #A3BE8C\">install<\/span><span style=\"color: #D8DEE9FF\"> <\/span><span style=\"color: #A3BE8C\">websockets<\/span><\/span>\n<span class=\"line\"><span style=\"color: #81A1C1\">export<\/span><span style=\"color: #D8DEE9FF\"> <\/span><span style=\"color: #D8DEE9\">INFOWAY_API_KEY<\/span><span style=\"color: #81A1C1\">=<\/span><span style=\"color: #ECEFF4\">&quot;<\/span><span style=\"color: #A3BE8C\">your_key_here<\/span><span style=\"color: #ECEFF4\">&quot;<\/span><\/span>\n<span class=\"line\"><span style=\"color: #88C0D0\">python<\/span><span style=\"color: #D8DEE9FF\"> <\/span><span style=\"color: #A3BE8C\">infoway_news.py<\/span><\/span><\/code><\/pre><\/div>\n\n\n\n<p class=\"wp-block-paragraph\">You should see the handshake, the subscription confirmation, and then news pushes:<\/p>\n\n\n\n<div class=\"wp-block-kevinbatdorf-code-block-pro\" data-code-block-pro-font-family=\"Code-Pro-JetBrains-Mono\" style=\"font-size:.875rem;font-family:Code-Pro-JetBrains-Mono,ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;line-height:1.25rem;--cbp-tab-width:2;tab-size:var(--cbp-tab-width, 2)\"><span style=\"display:block;padding:16px 0 0 16px;margin-bottom:-1px;width:100%;text-align:left;background-color:#2e3440ff\"><svg xmlns=\"http:\/\/www.w3.org\/2000\/svg\" width=\"54\" height=\"14\" viewBox=\"0 0 54 14\"><g fill=\"none\" fill-rule=\"evenodd\" transform=\"translate(1 1)\"><circle cx=\"6\" cy=\"6\" r=\"6\" fill=\"#FF5F56\" stroke=\"#E0443E\" stroke-width=\".5\"><\/circle><circle cx=\"26\" cy=\"6\" r=\"6\" fill=\"#FFBD2E\" stroke=\"#DEA123\" stroke-width=\".5\"><\/circle><circle cx=\"46\" cy=\"6\" r=\"6\" fill=\"#27C93F\" stroke=\"#1AAB29\" stroke-width=\".5\"><\/circle><\/g><\/svg><\/span><span role=\"button\" tabindex=\"0\" style=\"color:#d8dee9ff;display:none\" aria-label=\"Copy\" class=\"code-block-pro-copy-button\"><pre class=\"code-block-pro-copy-button-pre\" aria-hidden=\"true\"><textarea class=\"code-block-pro-copy-button-textarea\" tabindex=\"-1\" aria-hidden=\"true\" readonly>2026-09-08 13:02:11 INFO Connected\n2026-09-08 13:02:11 INFO Sent subscribe: lang=en\n2026-09-08 13:02:11 INFO Handshake OK: ws connect success\n2026-09-08 13:02:11 INFO Subscription confirmed: lang=en\n2026-09-08 13:02:39 INFO NEWS  urgency=2  provider=dow-jones  symbols=LSE:HSBA\n      HSBC to Complete Up to $1 Billion Share Buyback<\/textarea><\/pre><svg xmlns=\"http:\/\/www.w3.org\/2000\/svg\" style=\"width:24px;height:24px\" fill=\"none\" viewBox=\"0 0 24 24\" stroke=\"currentColor\" stroke-width=\"2\"><path class=\"with-check\" stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2m-6 9l2 2 4-4\"><\/path><path class=\"without-check\" stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2\"><\/path><\/svg><\/span><pre class=\"shiki nord\" style=\"background-color: #2e3440ff\" tabindex=\"0\"><code><span class=\"line\"><span style=\"color: #88C0D0\">2026-09-08<\/span><span style=\"color: #D8DEE9FF\"> <\/span><span style=\"color: #B48EAD\">13<\/span><span style=\"color: #A3BE8C\">:02:11<\/span><span style=\"color: #D8DEE9FF\"> <\/span><span style=\"color: #A3BE8C\">INFO<\/span><span style=\"color: #D8DEE9FF\"> <\/span><span style=\"color: #A3BE8C\">Connected<\/span><\/span>\n<span class=\"line\"><span style=\"color: #88C0D0\">2026-09-08<\/span><span style=\"color: #D8DEE9FF\"> <\/span><span style=\"color: #B48EAD\">13<\/span><span style=\"color: #A3BE8C\">:02:11<\/span><span style=\"color: #D8DEE9FF\"> <\/span><span style=\"color: #A3BE8C\">INFO<\/span><span style=\"color: #D8DEE9FF\"> <\/span><span style=\"color: #A3BE8C\">Sent<\/span><span style=\"color: #D8DEE9FF\"> <\/span><span style=\"color: #A3BE8C\">subscribe:<\/span><span style=\"color: #D8DEE9FF\"> <\/span><span style=\"color: #A3BE8C\">lang=en<\/span><\/span>\n<span class=\"line\"><span style=\"color: #88C0D0\">2026-09-08<\/span><span style=\"color: #D8DEE9FF\"> <\/span><span style=\"color: #B48EAD\">13<\/span><span style=\"color: #A3BE8C\">:02:11<\/span><span style=\"color: #D8DEE9FF\"> <\/span><span style=\"color: #A3BE8C\">INFO<\/span><span style=\"color: #D8DEE9FF\"> <\/span><span style=\"color: #A3BE8C\">Handshake<\/span><span style=\"color: #D8DEE9FF\"> <\/span><span style=\"color: #A3BE8C\">OK:<\/span><span style=\"color: #D8DEE9FF\"> <\/span><span style=\"color: #A3BE8C\">ws<\/span><span style=\"color: #D8DEE9FF\"> <\/span><span style=\"color: #A3BE8C\">connect<\/span><span style=\"color: #D8DEE9FF\"> <\/span><span style=\"color: #A3BE8C\">success<\/span><\/span>\n<span class=\"line\"><span style=\"color: #88C0D0\">2026-09-08<\/span><span style=\"color: #D8DEE9FF\"> <\/span><span style=\"color: #B48EAD\">13<\/span><span style=\"color: #A3BE8C\">:02:11<\/span><span style=\"color: #D8DEE9FF\"> <\/span><span style=\"color: #A3BE8C\">INFO<\/span><span style=\"color: #D8DEE9FF\"> <\/span><span style=\"color: #A3BE8C\">Subscription<\/span><span style=\"color: #D8DEE9FF\"> <\/span><span style=\"color: #A3BE8C\">confirmed:<\/span><span style=\"color: #D8DEE9FF\"> <\/span><span style=\"color: #A3BE8C\">lang=en<\/span><\/span>\n<span class=\"line\"><span style=\"color: #88C0D0\">2026-09-08<\/span><span style=\"color: #D8DEE9FF\"> <\/span><span style=\"color: #B48EAD\">13<\/span><span style=\"color: #A3BE8C\">:02:39<\/span><span style=\"color: #D8DEE9FF\"> <\/span><span style=\"color: #A3BE8C\">INFO<\/span><span style=\"color: #D8DEE9FF\"> <\/span><span style=\"color: #A3BE8C\">NEWS<\/span><span style=\"color: #D8DEE9FF\">  <\/span><span style=\"color: #A3BE8C\">urgency=<\/span><span style=\"color: #B48EAD\">2<\/span><span style=\"color: #D8DEE9FF\">  <\/span><span style=\"color: #A3BE8C\">provider=dow-jones<\/span><span style=\"color: #D8DEE9FF\">  <\/span><span style=\"color: #A3BE8C\">symbols=LSE:HSBA<\/span><\/span>\n<span class=\"line\"><span style=\"color: #D8DEE9FF\">      <\/span><span style=\"color: #88C0D0\">HSBC<\/span><span style=\"color: #D8DEE9FF\"> <\/span><span style=\"color: #A3BE8C\">to<\/span><span style=\"color: #D8DEE9FF\"> <\/span><span style=\"color: #A3BE8C\">Complete<\/span><span style=\"color: #D8DEE9FF\"> <\/span><span style=\"color: #A3BE8C\">Up<\/span><span style=\"color: #D8DEE9FF\"> <\/span><span style=\"color: #A3BE8C\">to<\/span><span style=\"color: #D8DEE9FF\"> <\/span><span style=\"color: #D8DEE9\">$1<\/span><span style=\"color: #D8DEE9FF\"> <\/span><span style=\"color: #A3BE8C\">Billion<\/span><span style=\"color: #D8DEE9FF\"> <\/span><span style=\"color: #A3BE8C\">Share<\/span><span style=\"color: #D8DEE9FF\"> <\/span><span style=\"color: #A3BE8C\">Buyback<\/span><\/span><\/code><\/pre><\/div>\n\n\n\n<h2 class=\"wp-block-heading\">7. Operational checklist<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">The mistakes that cost the most time, and how to avoid them:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li><strong>Keep the heartbeat running.<\/strong> Send <code>10010<\/code> every ~30s. No traffic for 60s and the server drops you. The client above binds the heartbeat task to the connection&#8217;s lifetime so it can&#8217;t outlive or lag the socket.<\/li>\n\n\n\n<li><strong>Re-subscribe on every reconnect.<\/strong> The connection has no memory. After any drop-and-reconnect you must send <code>10020<\/code> again or no pushes arrive. The client does this at the top of <code>_connect_once<\/code>.<\/li>\n\n\n\n<li><strong>One key, one connection, one language.<\/strong> A second connection on the same key is rejected. For multiple languages, use multiple keys.<\/li>\n\n\n\n<li><strong>Always dedup on <code>dk<\/code>.<\/strong> Multiple sources report the same event; <code>dk<\/code> is content-based and cross-source. Make your ingestion idempotent on it.<\/li>\n\n\n\n<li><strong>Treat <code>published<\/code> as UTC seconds.<\/strong> Convert to the user&#8217;s timezone only at display time. Don&#8217;t assume it equals your receive time \u2014 it&#8217;s the source&#8217;s stated publish time (see next section).<\/li>\n\n\n\n<li><strong>Normalize <code>symbols<\/code>.<\/strong> Expect both <code>TICKER.SUFFIX<\/code> and <code>EXCHANGE:TICKER<\/code> forms; map to your own convention on ingest, and handle items with no <code>symbols<\/code> at all.<\/li>\n\n\n\n<li><strong>Protect the key.<\/strong> It travels in the URL query string. Keep it server-side, in an environment variable or secret manager \u2014 never in browser code or a public repo.<\/li>\n<\/ul>\n\n\n\n<h2 class=\"wp-block-heading\">8. Error codes<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">If the handshake or a subscription fails, the server tells you why. The ones you&#8217;ll actually encounter:<\/p>\n\n\n\n<figure class=\"wp-block-table\"><table class=\"has-fixed-layout\"><thead><tr><th>Code<\/th><th>Meaning<\/th><th>Fix<\/th><\/tr><\/thead><tbody><tr><td><code>200<\/code><\/td><td>Handshake success<\/td><td>\u2014<\/td><\/tr><tr><td><code>506<\/code> \/ <code>507<\/code><\/td><td>Missing \/ malformed parameters<\/td><td>Check the <code>10020<\/code> payload shape; <code>data.lang<\/code> is required<\/td><\/tr><tr><td><code>513<\/code><\/td><td>Heartbeat timeout<\/td><td>Send <code>10010<\/code> every 30s<\/td><\/tr><tr><td><code>514<\/code><\/td><td>Wrong WebSocket path<\/td><td>Use <code>\/news<\/code>, not <code>\/ws<\/code><\/td><\/tr><tr><td><code>515<\/code><\/td><td>Payload is not valid JSON<\/td><td>Serialize the message properly<\/td><\/tr><tr><td><code>517<\/code><\/td><td>Missing <code>apikey<\/code><\/td><td>Add <code>?apikey=...<\/code> to the URL<\/td><\/tr><tr><td><code>518<\/code><\/td><td><code>apikey<\/code> does not exist<\/td><td>Check the key<\/td><\/tr><tr><td><code>519<\/code><\/td><td>No news permission<\/td><td>Upgrade to Premium or an equivalent custom plan<\/td><\/tr><tr><td><code>520<\/code><\/td><td>This key already has a connection<\/td><td>Close the other connection, or use another key<\/td><\/tr><tr><td><code>521<\/code><\/td><td>Server at capacity<\/td><td>Retry with backoff<\/td><\/tr><\/tbody><\/table><\/figure>\n\n\n\n<p class=\"wp-block-paragraph\">Full, current definitions are in the <a href=\"https:\/\/docs.infoway.io\/websocket-api\/subscribe-and-unsubscribe\/real-time-news\" target=\"_blank\" rel=\"noopener\">official docs<\/a>.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">9. Measure the latency yourself<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Don&#8217;t take anyone&#8217;s &#8220;real-time&#8221; label at face value, including ours.<\/strong> Here&#8217;s the method we use, and the numbers we get.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>End-to-end latency<\/strong> = the moment your client receives an item minus the item&#8217;s own <code>published<\/code> timestamp:<\/p>\n\n\n\n<div class=\"wp-block-kevinbatdorf-code-block-pro\" data-code-block-pro-font-family=\"Code-Pro-JetBrains-Mono\" style=\"font-size:.875rem;font-family:Code-Pro-JetBrains-Mono,ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;line-height:1.25rem;--cbp-tab-width:2;tab-size:var(--cbp-tab-width, 2)\"><span style=\"display:block;padding:16px 0 0 16px;margin-bottom:-1px;width:100%;text-align:left;background-color:#2e3440ff\"><svg xmlns=\"http:\/\/www.w3.org\/2000\/svg\" width=\"54\" height=\"14\" viewBox=\"0 0 54 14\"><g fill=\"none\" fill-rule=\"evenodd\" transform=\"translate(1 1)\"><circle cx=\"6\" cy=\"6\" r=\"6\" fill=\"#FF5F56\" stroke=\"#E0443E\" stroke-width=\".5\"><\/circle><circle cx=\"26\" cy=\"6\" r=\"6\" fill=\"#FFBD2E\" stroke=\"#DEA123\" stroke-width=\".5\"><\/circle><circle cx=\"46\" cy=\"6\" r=\"6\" fill=\"#27C93F\" stroke=\"#1AAB29\" stroke-width=\".5\"><\/circle><\/g><\/svg><\/span><span role=\"button\" tabindex=\"0\" style=\"color:#d8dee9ff;display:none\" aria-label=\"Copy\" class=\"code-block-pro-copy-button\"><pre class=\"code-block-pro-copy-button-pre\" aria-hidden=\"true\"><textarea class=\"code-block-pro-copy-button-textarea\" tabindex=\"-1\" aria-hidden=\"true\" readonly>import time\n\ndef latency_seconds(news: dict) -> float:\n    return time.time() - news&#91;\"published\"&#93;   # both UTC seconds<\/textarea><\/pre><svg xmlns=\"http:\/\/www.w3.org\/2000\/svg\" style=\"width:24px;height:24px\" fill=\"none\" viewBox=\"0 0 24 24\" stroke=\"currentColor\" stroke-width=\"2\"><path class=\"with-check\" stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2m-6 9l2 2 4-4\"><\/path><path class=\"without-check\" stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2\"><\/path><\/svg><\/span><pre class=\"shiki nord\" style=\"background-color: #2e3440ff\" tabindex=\"0\"><code><span class=\"line\"><span style=\"color: #88C0D0\">import<\/span><span style=\"color: #D8DEE9FF\"> <\/span><span style=\"color: #A3BE8C\">time<\/span><\/span>\n<span class=\"line\"><\/span>\n<span class=\"line\"><span style=\"color: #88C0D0\">def<\/span><span style=\"color: #D8DEE9FF\"> <\/span><span style=\"color: #A3BE8C\">latency_seconds<\/span><span style=\"color: #ECEFF4\">(<\/span><span style=\"color: #88C0D0\">news:<\/span><span style=\"color: #D8DEE9FF\"> <\/span><span style=\"color: #A3BE8C\">dict<\/span><span style=\"color: #ECEFF4\">)<\/span><span style=\"color: #D8DEE9FF\"> -<\/span><span style=\"color: #81A1C1\">&gt;<\/span><span style=\"color: #D8DEE9FF\"> <\/span><span style=\"color: #A3BE8C\">float:<\/span><\/span>\n<span class=\"line\"><span style=\"color: #D8DEE9FF\">    <\/span><span style=\"color: #81A1C1\">return<\/span><span style=\"color: #D8DEE9FF\"> <\/span><span style=\"color: #A3BE8C\">time.time<\/span><span style=\"color: #ECEFF4\">()<\/span><span style=\"color: #D8DEE9FF\"> <\/span><span style=\"color: #A3BE8C\">-<\/span><span style=\"color: #D8DEE9FF\"> <\/span><span style=\"color: #A3BE8C\">news&#91;<\/span><span style=\"color: #ECEFF4\">&quot;<\/span><span style=\"color: #A3BE8C\">published<\/span><span style=\"color: #ECEFF4\">&quot;<\/span><span style=\"color: #A3BE8C\">&#93;<\/span><span style=\"color: #D8DEE9FF\">   <\/span><span style=\"color: #616E88\"># both UTC seconds<\/span><\/span><\/code><\/pre><\/div>\n\n\n\n<p class=\"wp-block-paragraph\">Log that for every push over a fixed window and look at the distribution, not the best case.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>What we observed<\/strong> on a 10-minute sample of the <code>en<\/code> channel in production: 13 pushes (~1.3\/min), fastest end-to-end ~22 seconds, median ~54 seconds, ~77% of items carrying <code>symbols<\/code>, and 100% carrying <code>dk<\/code>.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Two things to keep in mind when you read your own numbers:<\/p>\n\n\n\n<ol class=\"wp-block-list\">\n<li><strong><code>published<\/code> is the source&#8217;s timestamp, not ours.<\/strong> Wire flashes are near-instant, so their end-to-end latency is dominated by collection and delivery. Long-form analysis pieces often enter the pipeline already minutes old at the source \u2014 that lag is inherent to the source and no downstream API removes it.<\/li>\n\n\n\n<li><strong>The streaming model adds no interval of its own.<\/strong> A polling client&#8217;s discovery latency is source lag <em>plus<\/em> up to one poll interval. The WebSocket feed removes that second term \u2014 you hear about an event as soon as it&#8217;s collected. If you want the lowest latency, subscribe to the busiest channel with the most wire sources, which is usually <code>en<\/code>.<\/li>\n<\/ol>\n\n\n\n<h2 class=\"wp-block-heading\">10. How to get access<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">The real-time news feed is included with the <strong>Premium plan ($199\/month) and above<\/strong>, and with equivalent custom plans. Once news is enabled, the <strong>same API key<\/strong> you use for market data works for the news WebSocket \u2014 same authentication, same SDK patterns, same operational model. If you already stream prices from Infoway, adding news is one more connection.<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>Sign up or upgrade: <a href=\"https:\/\/infoway.io\/en\" target=\"_blank\" rel=\"noopener\">Infoway API Website<\/a><\/li>\n\n\n\n<li>Full field, language, and error-code reference: <a href=\"https:\/\/docs.infoway.io\/websocket-api\/subscribe-and-unsubscribe\/real-time-news\" target=\"_blank\" rel=\"noopener\">API Documentations<\/a><\/li>\n<\/ul>\n\n\n\n<h2 class=\"wp-block-heading\">FAQ<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Is there a REST version of the news feed?<\/strong><br>The structured news feed is delivered over WebSocket. That&#8217;s deliberate \u2014 the value is in receiving events the instant they&#8217;re collected, without a polling interval in the path.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Does reconnecting restore my subscription?<\/strong><br>No. The connection is stateless. After every connect \u2014 including automatic reconnects \u2014 you must send the <code>10020<\/code> subscribe message again. The production client above does this in <code>_connect_once<\/code>.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Can one connection stream multiple languages?<\/strong><br>No. One connection carries one language, and one API key allows one connection. Run one client per language, each with its own key, and merge the streams in your application.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>How do I stop getting duplicates?<\/strong><br>Deduplicate on <code>dk<\/code>, which is a content hash and matches across sources reporting the same event. In our testing every push carried one. Within a language channel this fully handles duplicates; across languages the text differs, so <code>dk<\/code> values differ too.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>What symbol format does <code>symbols<\/code> use?<\/strong><br>Mostly suffix form (<code>AAPL.US<\/code>, <code>005930.KS<\/code>, <code>399006.SZ<\/code>), with some exchange-prefixed entries (<code>LSE:HSBA<\/code>, <code>NSE:NIFTY<\/code>). Normalize to your own convention when you ingest, and handle items that carry no symbols.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Why is my measured latency higher than the median you quote?<\/strong><br><code>published<\/code> is the source&#8217;s publish time. Breaking wire items are near-instant; long-form pieces can be minutes old at the source before collection. Subscribe to the <code>en<\/code> channel for the highest throughput and lowest typical latency.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Can I use this to trigger trades?<\/strong><br>Yes, that&#8217;s a core use case. Filter on low <code>urgency<\/code> and a <code>symbols<\/code> match against your universe, then use the shared key to pull live prices or candles for confirmation before acting.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><\/p>\n","protected":false},"excerpt":{"rendered":"<p>Infoway&#8217;s real-time financial news API is live. With the same API key you already use for market data, you can now open a WebSocket and receive a structured stream of global financial news.<\/p>\n","protected":false},"author":1,"featured_media":276,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"_acf_changed":false,"footnotes":""},"categories":[12,8],"tags":[],"class_list":["post-273","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-new-feature","category-how-to"],"acf":[],"_links":{"self":[{"href":"https:\/\/blog.infoway.io\/en\/wp-json\/wp\/v2\/posts\/273","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/blog.infoway.io\/en\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/blog.infoway.io\/en\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/blog.infoway.io\/en\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/blog.infoway.io\/en\/wp-json\/wp\/v2\/comments?post=273"}],"version-history":[{"count":3,"href":"https:\/\/blog.infoway.io\/en\/wp-json\/wp\/v2\/posts\/273\/revisions"}],"predecessor-version":[{"id":277,"href":"https:\/\/blog.infoway.io\/en\/wp-json\/wp\/v2\/posts\/273\/revisions\/277"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/blog.infoway.io\/en\/wp-json\/wp\/v2\/media\/276"}],"wp:attachment":[{"href":"https:\/\/blog.infoway.io\/en\/wp-json\/wp\/v2\/media?parent=273"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/blog.infoway.io\/en\/wp-json\/wp\/v2\/categories?post=273"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/blog.infoway.io\/en\/wp-json\/wp\/v2\/tags?post=273"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}