nadex-cli 1.0.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,239 @@
1
+ Metadata-Version: 2.4
2
+ Name: nadex-cli
3
+ Version: 1.0.0
4
+ Description-Content-Type: text/markdown
5
+ License-File: LICENSE
6
+ Requires-Dist: python-dotenv
7
+ Requires-Dist: requests
8
+ Requires-Dist: websockets
9
+ Dynamic: description
10
+ Dynamic: description-content-type
11
+ Dynamic: license-file
12
+ Dynamic: requires-dist
13
+
14
+ # πŸ“ˆ nadex β€” Real-Time 5-Minute Binary Options Market Data CLI
15
+
16
+ `nadex` is a powerful Python CLI tool that fetches **live 5-minute binary options forex data** directly from the Nadex exchange. With a single command, you can access comprehensive market data including all available forex pairs, trading levels (strikes), bid/ask prices, and order book quantities β€” transforming your terminal into a real-time trading dashboard.
17
+
18
+ Perfect for traders, developers, and financial analysts who need instant access to Nadex's binary options market structure and live pricing data.
19
+
20
+ ---
21
+
22
+ ## πŸš€ Key Features
23
+
24
+ - ⏱ **Real-time 5-minute binary options data** for all major forex pairs
25
+ - πŸ’΅ **Complete strike levels** with bid/ask prices and available quantities
26
+ - πŸ“Š **Full order book visualization** in your terminal
27
+ - πŸ” **Built-in test credentials** (easily replaceable with your own)
28
+ - 🎯 **Clean, parsable CLI output** for both human reading and automation
29
+ - 🐍 **One-command installation** via PyPI (`pip install nadex`)
30
+ - 🌐 **WebSocket streaming** for real-time updates
31
+ - πŸ“ˆ **Professional-grade market data** from Nadex exchange
32
+
33
+ ---
34
+
35
+ ## πŸ“¦ Quick Installation
36
+
37
+ Install the package globally using pip:
38
+
39
+ ```bash
40
+ pip install nadex
41
+ ```
42
+
43
+ That's it! No additional setup required.
44
+
45
+ ---
46
+
47
+ ## ⚑ Quick Start
48
+
49
+ After installation, simply run:
50
+
51
+ ```bash
52
+ nadex_dashboard
53
+ ```
54
+
55
+ The CLI will immediately:
56
+ 1. Connect to Nadex using built-in test credentials
57
+ 2. Subscribe to the live 5-minute binary options feed
58
+ 3. Display real-time market data in a clean, organized format
59
+
60
+ ## 🎯 What You Get
61
+
62
+ ### Complete Market Overview
63
+ - **All Active Forex Pairs**: EUR/USD, GBP/USD, USD/JPY, AUD/USD, USD/CAD, EUR/GBP,
64
+ - **Strike Levels**: Every available trading level for 5-minute binary options
65
+ - **Bid/Ask Prices**: Real-time pricing from the Nadex order book
66
+ - **Contract Quantities**: Available volume at each price level
67
+
68
+ ### Real-Time Updates
69
+ The dashboard refreshes automatically as new market data arrives via WebSocket connection, ensuring you always see the latest:
70
+ - Price movements
71
+ - Quantity changes
72
+ - New strike levels
73
+ - Market status updates
74
+
75
+ ---
76
+
77
+ ## πŸ”‘ Authentication & Credentials
78
+
79
+ ### Default Test Mode
80
+ The package comes with **built-in test credentials** that connect to Nadex's demo environment. This means:
81
+ - βœ… No real money involved
82
+ - βœ… Full access to live market data structure
83
+ - βœ… Perfect for learning and development
84
+ - βœ… No registration required
85
+
86
+ ### Using Your Own Credentials
87
+
88
+ If you have a Nadex account and want to use your own credentials:
89
+
90
+ #### Method 1: Environment Variables
91
+ ```bash
92
+ export NADEX_USERNAME=your-username
93
+ export NADEX_PASSWORD=your-password
94
+ nadex_dashboard
95
+ ```
96
+
97
+ #### Method 2: .env File
98
+ Create a `.env` file in your working directory:
99
+ ```env
100
+ NADEX_USERNAME=your-username
101
+ NADEX_PASSWORD=your-password
102
+ ```
103
+
104
+ Then run the command as usual:
105
+ ```bash
106
+ nadex_dashboard
107
+ ```
108
+
109
+ #### Method 3: Direct Configuration
110
+ ```python
111
+ # Custom script using the package
112
+ from nadex_dashboard import NadexClient
113
+
114
+ client = NadexClient(
115
+ username="your-username",
116
+ password="your-password"
117
+ )
118
+ client.start_dashboard()
119
+ ```
120
+
121
+ ---
122
+
123
+ ## πŸ—οΈ Architecture & Project Structure
124
+
125
+ ```
126
+ nadex/
127
+ β”œβ”€β”€ nadex_dashboard/
128
+ β”‚ β”œβ”€β”€ __init__.py # Package initialization
129
+ β”‚ β”œβ”€β”€ config.py # Configuration and environment handling
130
+ β”‚ β”œβ”€β”€ helpers.py # Utility functions and data processing
131
+ β”‚ β”œβ”€β”€ messages.py # WebSocket message formats and protocols
132
+ β”‚ β”œβ”€β”€ parsing.py # Market data parsing and validation
133
+ β”‚ β”œβ”€β”€ dashboard.py # CLI formatting and display logic
134
+ β”‚ β”œβ”€β”€ websocket_manager.py # WebSocket connection management
135
+ β”‚ └── __main__.py # CLI entry point
136
+ β”œβ”€β”€ setup.py # Package configuration
137
+ β”œβ”€β”€ requirements.txt # Dependencies
138
+ β”œβ”€β”€ README.md # This file
139
+ └── LICENSE # MIT License
140
+ ```
141
+
142
+ ---
143
+
144
+ ## πŸ“Š Market Data Specifications
145
+
146
+ ### Supported Instruments
147
+ - **EUR/USD** - Euro vs US Dollar
148
+ - **GBP/USD** - British Pound vs US Dollar
149
+ - **USD/JPY** - US Dollar vs Japanese Yen
150
+ - **AUD/USD** - Australian Dollar vs US Dollar
151
+ - **USD/CAD** - US Dollar vs Canadian Dollar
152
+ - **EUR/JPY** - Euro vs Japanese Yen
153
+ - **GBP/JPY** - British Pound vs Japanese Yen
154
+
155
+ ---
156
+
157
+ ### Local Development Setup
158
+ ```bash
159
+ # Clone the repository
160
+ git clone https://github.com/shivamgarg-dev/nadex-dashboard.git
161
+ cd nadex-dashboard
162
+
163
+ # Install in development mode
164
+ pip install -e .
165
+
166
+ # Run tests
167
+ python -m pytest tests/
168
+
169
+ # Run the CLI
170
+ nadex_dashboard
171
+ ```
172
+
173
+ ---
174
+
175
+ ## ❓ Frequently Asked Questions
176
+
177
+ ### General Questions
178
+
179
+ **Q: Is this connected to real money?**
180
+ A: By default, no. The package uses Nadex's test environment with demo credentials. No real funds are involved unless you explicitly provide your own production credentials.
181
+
182
+ **Q: Do I need a Nadex account?**
183
+ A: No, the package works out-of-the-box with built-in test credentials. However, you can use your own Nadex account if you prefer.
184
+
185
+ **Q: Is the data real-time?**
186
+ A: Yes, the data is streamed live via WebSocket from Nadex's servers with minimal latency.
187
+
188
+ ### Technical Questions
189
+
190
+ **Q: Can I use this in trading bots?**
191
+ A: Absolutely. The package is designed with automation in mind. You can import modules and build custom applications on top of it.
192
+
193
+ **Q: How often does the data update?**
194
+ A: Updates are pushed in real-time as market conditions change, typically 1-5 times per second during active trading hours.
195
+
196
+ ---
197
+
198
+ ## πŸ—ΊοΈ Roadmap
199
+
200
+ ### Upcoming Features
201
+ - [ ] **Historical data export** for backtesting
202
+ - [ ] **Alert system** for price/volume thresholds
203
+ - [ ] **Multiple timeframes** (1-minute, 15-minute options)
204
+ - [ ] **Advanced filtering** by instrument, strike range, etc.
205
+ - [ ] **REST API mode** for web applications
206
+ - [ ] **Docker container** for easy deployment
207
+ - [ ] **Grafana dashboard** integration
208
+ - [ ] **Telegram/Discord bot** notifications
209
+
210
+ ### Long-term Vision
211
+ - Support for other Nadex instrument types (indices, commodities)
212
+ - Machine learning integration for pattern recognition
213
+ - Advanced analytics and visualization tools
214
+ - Mobile app companion
215
+
216
+ ---
217
+
218
+ ## πŸ‘¨β€πŸ’» Author & Maintainer
219
+
220
+ **Shivam Garg**
221
+ - 🌐 **GitHub**: [@shivamgarg001](https://github.com/shivamgarg001)
222
+
223
+ ---
224
+
225
+ ## ⭐️ Show Your Support
226
+
227
+ If you find this tool helpful for your trading, development, or learning:
228
+
229
+ - ⭐ **Star the repository** on GitHub
230
+ - 🍴 **Fork it** to contribute
231
+ - πŸ› **Report issues** to help improve it
232
+ - πŸ’‘ **Suggest features** for future releases
233
+ - πŸ“’ **Share it** with others who might benefit
234
+
235
+ **GitHub Repository**: [https://github.com/shivamgarg001/Nadex]
236
+
237
+ ---
238
+
239
+ **Happy trading! πŸ“ˆ**
@@ -0,0 +1,15 @@
1
+ nadex_cli-1.0.0.dist-info/licenses/LICENSE,sha256=3u7cBUwc4t0GRkNpKuqj7M7cKldqG0FRIIgNRHP1_qs,1071
2
+ nadex_dashboard/__init__.py,sha256=_a2dwUetiQjYrOahxY50UOLcXe6VpSgx24pzkD1Z9Ow,27
3
+ nadex_dashboard/__main__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
4
+ nadex_dashboard/config.py,sha256=iFLZLKwkjhjN5ERKpe66O0ftdSbqGmPnUCzDHl8ehRU,3929
5
+ nadex_dashboard/frontend.py,sha256=QWELbcTebP5KBIuCk-F1MdurykvqHhlTIPWcDEL_sAI,2826
6
+ nadex_dashboard/helpers.py,sha256=VueSqwUCQg698XDjKe2mVevJVSlX6NhEKF7nNz2TY3E,3906
7
+ nadex_dashboard/main.py,sha256=0KA7gzktuJ5PbLnK6DRN5A4cJosZCfAeYB38Ia6ZcnM,2868
8
+ nadex_dashboard/messages.py,sha256=v3sTlpJBMm2gzqU2a_2oOxaOXCkgvSrmRnfM30zES-g,7157
9
+ nadex_dashboard/parsing.py,sha256=KwcVUh549fIlcwihtmxOh6XyVncBJRCgB7HtVf2K6BA,5130
10
+ nadex_dashboard/websocket_manager.py,sha256=ufB7HUp4j5P5YZi_cYf_tzjtqKYN5YVH3qZMlVFM4hw,8224
11
+ nadex_cli-1.0.0.dist-info/METADATA,sha256=1HUOJveb2hb0ssWfTrmoAn1mYnrxUbRenpzSo3UQHWY,7130
12
+ nadex_cli-1.0.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
13
+ nadex_cli-1.0.0.dist-info/entry_points.txt,sha256=fLHzipiielS1qZy8tsrK9692ReBsHb327623qr2AF-I,62
14
+ nadex_cli-1.0.0.dist-info/top_level.txt,sha256=xiH5k9agqsuuhDha_9I31BecV6jXHvfkzhOlE-eHKXQ,16
15
+ nadex_cli-1.0.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (80.9.0)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ nadex_dashboard = nadex_dashboard:cli_entry
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) [2025] [SHIVAM GARG]
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1 @@
1
+ nadex_dashboard
@@ -0,0 +1 @@
1
+ from .main import cli_entry
File without changes
@@ -0,0 +1,105 @@
1
+ # ---------------------------------------------------------------
2
+ # File : config.py
3
+ # Author : Shivam Garg
4
+ # Created on : 27-06-2005
5
+
6
+ # Copyright (c) Shivam Garg. All rights reserved.
7
+ # ---------------------------------------------------------------
8
+
9
+ import os
10
+ from dotenv import load_dotenv
11
+
12
+ # Load environment variables
13
+ load_dotenv()
14
+
15
+ class Config:
16
+ # Authentication
17
+ NADEX_USERNAME = os.getenv('NADEX_USERNAME')
18
+ NADEX_PASSWORD = os.getenv('NADEX_PASSWORD')
19
+ NADEX_USER_ID = os.getenv('NADEX_USER_ID')
20
+
21
+ # API URLs
22
+ NADEX_AUTH_URL = os.getenv('NADEX_AUTH_URL')
23
+ NADEX_SESSION_URL = os.getenv('NADEX_SESSION_URL')
24
+ NADEX_MARKET_TREE_URL = os.getenv('NADEX_MARKET_TREE_URL')
25
+ NADEX_NAVIGATION_URL = os.getenv('NADEX_NAVIGATION_URL')
26
+ FRONTEND_PORT = os.getenv('FRONTEND_PORT')
27
+ # WebSocket Configuration
28
+ PING_INTERVAL = int(os.getenv('PING_INTERVAL', 30))
29
+ RESUBSCRIBE_INTERVAL = int(os.getenv('RESUBSCRIBE_INTERVAL', 300))
30
+ INITIAL_TABLE_COUNTER = int(os.getenv('INITIAL_TABLE_COUNTER', 15))
31
+ INITIAL_REQ_PHASE_COUNTER = int(os.getenv('INITIAL_REQ_PHASE_COUNTER', 663))
32
+ WIN_PHASE = int(os.getenv('WIN_PHASE', 63))
33
+
34
+ # Headers for authentication
35
+ AUTH_HEADERS = {
36
+ "Accept": "application/json; charset=UTF-8",
37
+ "Content-Type": "application/json; charset=UTF-8",
38
+ "Origin": "https://platform.nadex.com",
39
+ "User-Agent": "Mozilla/5.0",
40
+ "x-device-user-agent": "vendor=IG | applicationType=Nadex | platform=web | deviceType=phone | version=0.907.0+78b9f706"
41
+ }
42
+
43
+ # Headers for session creation
44
+ SESSION_HEADERS = {
45
+ "Content-Type": "application/x-www-form-urlencoded",
46
+ "Origin": "https://platform.nadex.com",
47
+ "Referer": "https://platform.nadex.com/",
48
+ "User-Agent": "Mozilla/5.0"
49
+ }
50
+
51
+ # Headers for market data requests
52
+ MARKET_HEADERS = {
53
+ "accept": "application/json; charset=UTF-8",
54
+ "accept-encoding": "gzip, deflate, br, zstd",
55
+ "accept-language": "en-US,en;q=0.9,hi;q=0.8",
56
+ "authorization": "Bearer undefined",
57
+ "content-type": "application/json; charset=UTF-8",
58
+ "origin": "https://platform.nadex.com",
59
+ "priority": "u=1, i",
60
+ "sec-ch-ua": '"Google Chrome";v="137", "Chromium";v="137", "Not/A)Brand";v="24"',
61
+ "sec-ch-ua-mobile": "?0",
62
+ "sec-ch-ua-platform": '"macOS"',
63
+ "sec-fetch-dest": "empty",
64
+ "sec-fetch-mode": "cors",
65
+ "sec-fetch-site": "same-site",
66
+ "user-agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/137.0.0.0 Safari/537.36",
67
+ "x-device-user-agent": "vendor=IG | applicationType=Nadex | platform=web | deviceType=phone | version=0.907.0+78b9f706"
68
+ }
69
+
70
+ # Navigation headers
71
+ NAVIGATION_HEADERS = {
72
+ "accept": "application/json; charset=UTF-8",
73
+ "accept-encoding": "gzip, deflate, br, zstd",
74
+ "accept-language": "en-US,en;q=0.9,hi;q=0.8",
75
+ "content-type": "application/json; charset=UTF-8",
76
+ "origin": "https://platform.nadex.com",
77
+ "referer": "https://platform.nadex.com/",
78
+ "sec-fetch-dest": "empty",
79
+ "sec-fetch-mode": "cors",
80
+ "sec-fetch-site": "same-site",
81
+ "user-agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7)",
82
+ "x-device-user-agent": "vendor=IG | applicationType=Nadex | platform=web | deviceType=phone | version=0.907.0+78b9f706"
83
+ }
84
+
85
+ def get_auth_payload():
86
+ """Get authentication payload with credentials from environment"""
87
+ return {
88
+ "username": Config.NADEX_USERNAME,
89
+ "password": Config.NADEX_PASSWORD
90
+ }
91
+
92
+ def get_session_payload(xst_token):
93
+ """Get session creation payload"""
94
+ return {
95
+ "LS_phase": "2301",
96
+ "LS_cause": "new.api",
97
+ "LS_polling": "true",
98
+ "LS_polling_millis": "0",
99
+ "LS_idle_millis": "0",
100
+ "LS_client_version": "6.1",
101
+ "LS_adapter_set": "InVisionProvider",
102
+ "LS_user": Config.NADEX_USER_ID,
103
+ "LS_password": f"XST-{xst_token}",
104
+ "LS_container": "lsc"
105
+ }
@@ -0,0 +1,84 @@
1
+ # ---------------------------------------------------------------
2
+ # File : frontend.py
3
+ # Author : Shivam Garg
4
+ # Created on : 27-06-2005
5
+
6
+ # Copyright (c) Shivam Garg. All rights reserved.
7
+ # ---------------------------------------------------------------
8
+
9
+ import asyncio
10
+ import websockets
11
+ import json
12
+ from typing import Set
13
+
14
+ # Store connected frontend clients
15
+ frontend_clients: Set[websockets.WebSocketServerProtocol] = set()
16
+
17
+ async def frontend_handler(websocket, path):
18
+ """Handle frontend WebSocket connections."""
19
+ frontend_clients.add(websocket)
20
+ print(f"[+] Frontend client connected: {websocket.remote_address}")
21
+
22
+ try:
23
+ async for message in websocket:
24
+ # Handle messages from frontend clients if needed
25
+ print(f"[FRONTEND] Received: {message}")
26
+ # Echo back or process as needed
27
+ await websocket.send(f"Echo: {message}")
28
+ except websockets.exceptions.ConnectionClosed:
29
+ print(f"[-] Frontend client disconnected: {websocket.remote_address}")
30
+ finally:
31
+ frontend_clients.discard(websocket)
32
+
33
+ async def relay_to_frontend(message: str):
34
+ """Relay message to all connected frontend clients."""
35
+ if not frontend_clients:
36
+ return
37
+
38
+ # Create a copy of the set to avoid modification during iteration
39
+ clients_copy = frontend_clients.copy()
40
+
41
+ # Send to all connected clients
42
+ disconnected_clients = []
43
+ for client in clients_copy:
44
+ try:
45
+ await client.send(message)
46
+ except websockets.exceptions.ConnectionClosed:
47
+ disconnected_clients.append(client)
48
+ except Exception as e:
49
+ print(f"[ERROR] Failed to send to frontend client: {e}")
50
+ disconnected_clients.append(client)
51
+
52
+ # Remove disconnected clients
53
+ for client in disconnected_clients:
54
+ frontend_clients.discard(client)
55
+
56
+ async def broadcast_to_frontend(data: dict):
57
+ """Broadcast structured data to frontend clients."""
58
+ message = json.dumps(data)
59
+ await relay_to_frontend(message)
60
+
61
+ def get_frontend_client_count() -> int:
62
+ """Get the number of connected frontend clients."""
63
+ return len(frontend_clients)
64
+
65
+ async def close_all_frontend_connections():
66
+ """Close all frontend connections gracefully."""
67
+ if not frontend_clients:
68
+ return
69
+
70
+ print(f"[+] Closing {len(frontend_clients)} frontend connections...")
71
+
72
+ # Create a copy to avoid modification during iteration
73
+ clients_copy = frontend_clients.copy()
74
+
75
+ # Close all connections
76
+ for client in clients_copy:
77
+ try:
78
+ await client.close()
79
+ except Exception as e:
80
+ print(f"[ERROR] Failed to close frontend client: {e}")
81
+
82
+ # Clear the set
83
+ frontend_clients.clear()
84
+ print("[+] All frontend connections closed.")
@@ -0,0 +1,117 @@
1
+ # ---------------------------------------------------------------
2
+ # File : helpers.py
3
+ # Author : Shivam Garg
4
+ # Created on : 27-06-2005
5
+
6
+ # Copyright (c) Shivam Garg. All rights reserved.
7
+ # ---------------------------------------------------------------
8
+
9
+ import requests
10
+ import re
11
+ from collections import defaultdict
12
+
13
+ from .config import (
14
+ Config,
15
+ AUTH_HEADERS,
16
+ SESSION_HEADERS,
17
+ MARKET_HEADERS,
18
+ NAVIGATION_HEADERS,
19
+ get_auth_payload,
20
+ get_session_payload,
21
+ )
22
+
23
+ # Global XST token
24
+ xst_token = None
25
+
26
+ def get_xst_token():
27
+ """Authenticate with Nadex and get XST token."""
28
+ global xst_token
29
+ print("[+] Authenticating with Nadex...")
30
+ resp = requests.post(
31
+ Config.NADEX_AUTH_URL,
32
+ json=get_auth_payload(),
33
+ headers=AUTH_HEADERS
34
+ )
35
+ resp.raise_for_status()
36
+ token = resp.headers.get("x-security-token")
37
+ if not token:
38
+ raise RuntimeError("Missing x-security-token")
39
+ xst_token = token
40
+ print(f"[+] Obtained XST token: {token[:20]}…")
41
+ return token
42
+
43
+ def get_session_info():
44
+ """Create Lightstreamer session and return session info."""
45
+ get_xst_token()
46
+ print("[+] Creating Lightstreamer session…")
47
+ resp = requests.post(
48
+ Config.NADEX_SESSION_URL,
49
+ data=get_session_payload(xst_token),
50
+ headers=SESSION_HEADERS
51
+ )
52
+ resp.raise_for_status()
53
+ body = resp.text
54
+ sid = re.search(r"start\('([^']+)'", body).group(1)
55
+ host = re.search(r"start\('[^']+',\s*'([^']+)'", body).group(1)
56
+ phase = re.search(r"setPhase\((\d+)\);", body).group(1)
57
+ return sid, host, int(phase)
58
+
59
+ def fetch_market_tree():
60
+ """Fetch market tree from Nadex API."""
61
+ if not xst_token:
62
+ raise RuntimeError("xst_token not set")
63
+ print("[+] Fetching market tree…")
64
+ hdrs = {k: v for k, v in MARKET_HEADERS.items() if not k.startswith(":")}
65
+ hdrs["x-security-token"] = xst_token
66
+ resp = requests.get(Config.NADEX_MARKET_TREE_URL, headers=hdrs)
67
+ resp.raise_for_status()
68
+ print(f"[+] Market Tree status: {resp.status_code}")
69
+ return resp.json()
70
+
71
+ def extract_forex_ids(tree):
72
+ """Extract forex market IDs from market tree."""
73
+ for node in tree.get("topLevelNodes", []):
74
+ if node.get("name", "").lower() == "5 minute binaries":
75
+ for c in node.get("children", []):
76
+ if c.get("name", "").lower() == "forex":
77
+ return [x["id"] for x in c.get("children", [])]
78
+ return []
79
+
80
+ def fetch_navigation_by_id(mid):
81
+ """Fetch navigation data for a specific market ID."""
82
+ url = f"{Config.NADEX_NAVIGATION_URL}/{mid}"
83
+ hdrs = NAVIGATION_HEADERS.copy()
84
+ hdrs["x-security-token"] = xst_token
85
+ resp = requests.get(url, headers=hdrs)
86
+ resp.raise_for_status()
87
+ return resp.json()
88
+
89
+ def map_market_data(fx_ids):
90
+ """Map market data from forex IDs to underlying epics."""
91
+ mapping = defaultdict(lambda: defaultdict(list))
92
+ for mid in fx_ids:
93
+ print(f"⟳ Processing market ID {mid}")
94
+ nav = fetch_navigation_by_id(mid)
95
+ for m in nav.get("markets", []):
96
+ ue = m.get("underlyingEpic", "")
97
+ ep = m.get("epic", "")
98
+ if ue and ep:
99
+ mapping[mid][ue].append(ep)
100
+ print(f" β†’ Found {len(nav.get('markets', []))} epics")
101
+ return mapping
102
+
103
+ def print_market_mapping(mapping):
104
+ """Print a summary of the market mapping."""
105
+ print("\n" + "=" * 50 + "\nMARKET MAPPING SUMMARY\n" + "=" * 50)
106
+ total_under, total_ep = 0, 0
107
+ for mid, ueps in mapping.items():
108
+ print(f"Market ID {mid}:")
109
+ for ue, eps in ueps.items():
110
+ print(f" β€’ {ue}: {len(eps)} epics")
111
+ total_under += 1
112
+ total_ep += len(eps)
113
+ print(f"\n[+] {len(mapping)} markets, {total_under} underlyings, {total_ep} total epics")
114
+
115
+ def get_current_xst_token():
116
+ """Get the current XST token."""
117
+ return xst_token
@@ -0,0 +1,95 @@
1
+ # ---------------------------------------------------------------
2
+ # File : main.py
3
+ # Author : Shivam Garg
4
+ # Created on : 27-06-2005
5
+
6
+ # Copyright (c) Shivam Garg. All rights reserved.
7
+ # ---------------------------------------------------------------
8
+
9
+ import asyncio
10
+ import signal
11
+ import sys
12
+ import websockets
13
+ import contextlib
14
+
15
+ from .config import Config
16
+ from .helpers import get_session_info, fetch_market_tree, extract_forex_ids, map_market_data, print_market_mapping
17
+ from .websocket_manager import WebSocketManager
18
+ from .frontend import frontend_handler, close_all_frontend_connections
19
+
20
+ # Global shutdown event
21
+ shutdown_event = asyncio.Event()
22
+
23
+ def signal_handler(signum, frame):
24
+ """Handle shutdown signals gracefully."""
25
+ print(f"\n[!] Received signal {signum}. Initiating graceful shutdown...")
26
+ shutdown_event.set()
27
+
28
+ async def main():
29
+ """Main application entry point."""
30
+ # Set up signal handlers
31
+ signal.signal(signal.SIGINT, signal_handler)
32
+ signal.signal(signal.SIGTERM, signal_handler)
33
+
34
+ # 1) Start frontend server
35
+ server = await websockets.serve(frontend_handler, "0.0.0.0", Config.FRONTEND_PORT)
36
+ print(f"[+] Frontend WS listening on ws://0.0.0.0:{Config.FRONTEND_PORT}")
37
+
38
+ try:
39
+ # 2) Get session info and market data
40
+ sid, host, phase = get_session_info()
41
+ print(f"[+] Session={sid} phase={phase+2} host={host}")
42
+
43
+ tree = fetch_market_tree()
44
+ fx_ids = extract_forex_ids(tree)
45
+ if not fx_ids:
46
+ print("[-] No forex IDs found, exiting.")
47
+ return
48
+
49
+ mapping = map_market_data(fx_ids)
50
+ print_market_mapping(mapping)
51
+
52
+ # 3) Start WebSocket manager
53
+ mgr = WebSocketManager(sid, phase, host, mapping, fx_ids, shutdown_event)
54
+ nadex_task = asyncio.create_task(mgr.listen_and_relay())
55
+
56
+ # 4) Wait for shutdown signal
57
+ await shutdown_event.wait()
58
+ print("\n[!] Shutdown requested β€” cleaning up…")
59
+
60
+ # 5) Cancel WebSocket manager
61
+ nadex_task.cancel()
62
+ with contextlib.suppress(asyncio.CancelledError):
63
+ await nadex_task
64
+
65
+ except Exception as e:
66
+ print(f"[ERROR] {e}")
67
+ raise
68
+ finally:
69
+ # 6) Close all connections
70
+ await close_all_frontend_connections()
71
+ server.close()
72
+ await server.wait_closed()
73
+ print("[+] All done. Bye.")
74
+
75
+ def cli_entry():
76
+ try:
77
+ asyncio.run(main())
78
+ except KeyboardInterrupt:
79
+ print("\n[!] Interrupted by user")
80
+ except Exception as e:
81
+ print(f"[ERROR] {e}")
82
+ sys.exit(1)
83
+ else:
84
+ sys.exit(0)
85
+
86
+ if __name__ == "__main__":
87
+ try:
88
+ asyncio.run(main())
89
+ except KeyboardInterrupt:
90
+ print("\n[!] Interrupted by user")
91
+ except Exception as e:
92
+ print(f"[ERROR] {e}")
93
+ sys.exit(1)
94
+ else:
95
+ sys.exit(0)
@@ -0,0 +1,154 @@
1
+ # ---------------------------------------------------------------
2
+ # File : messages.py
3
+ # Author : Shivam Garg
4
+ # Created on : 27-06-2005
5
+
6
+ # Copyright (c) Shivam Garg. All rights reserved.
7
+ # ---------------------------------------------------------------
8
+
9
+ from .config import Config
10
+
11
+ class WebSocketMessages:
12
+ """Class to handle all WebSocket message templates"""
13
+
14
+ @staticmethod
15
+ def get_bind_session_message(session_id, phase):
16
+ """Generate bind session message (Table 1)"""
17
+ return (
18
+ "bind_session\r\n"
19
+ f"LS_session={session_id}&LS_phase={phase}&LS_cause=loop1&LS_container=lsc&control\r\n"
20
+ f"LS_mode=RAW&LS_id=M___.HB%7CHB.U.HEARTBEAT.IP&LS_schema=HEARTBEAT&"
21
+ f"LS_requested_max_frequency=1&LS_table=1&LS_req_phase=619&LS_win_phase=50&LS_op=add&LS_session={session_id}&"
22
+ )
23
+
24
+ @staticmethod
25
+ def get_core_subscriptions(session_id, user_id):
26
+ """Generate core subscription messages (Tables 2-7)"""
27
+ return [
28
+ # Table 2 - Message Event Handler
29
+ f"control\r\nLS_mode=RAW&LS_id=V2-M-MESSAGE_EVENT_HANDLER%7C{user_id}&LS_schema=message&"
30
+ f"LS_requested_max_frequency=1&LS_table=2&LS_req_phase=620&LS_win_phase=50&LS_op=add&LS_session={session_id}&",
31
+
32
+ # Table 3 - Account Balance
33
+ f"control\r\nLS_mode=MERGE&LS_id=V2-AD-AC_AVAILABLE_BALANCE%2CAC_USED_MARGIN%7CACC.{user_id}&"
34
+ f"LS_schema=AC_AVAILABLE_BALANCE%20AC_USED_MARGIN&LS_snapshot=true&LS_requested_max_frequency=1&"
35
+ f"LS_table=3&LS_req_phase=621&LS_win_phase=50&LS_op=add&LS_session={session_id}&",
36
+
37
+ # Table 4 - Message Event Handler JSON
38
+ f"control\r\nLS_mode=RAW&LS_id=V2-M-MESSAGE_EVENT_HANDLER%7C{user_id}-OP-JSON&LS_schema=json&"
39
+ f"LS_requested_max_frequency=1&LS_table=4&LS_req_phase=622&LS_win_phase=50&LS_op=add&LS_session={session_id}&",
40
+
41
+ # Table 5 - MGE
42
+ f"control\r\nLS_mode=RAW&LS_id=M___.MGE%7C{user_id}-LGT&LS_schema=message&"
43
+ f"LS_requested_max_frequency=1&LS_table=5&LS_req_phase=623&LS_win_phase=50&LS_op=add&LS_session={session_id}&",
44
+
45
+ # Table 6 - WO JSON
46
+ f"control\r\nLS_mode=RAW&LS_id=V2-M-MESSAGE_EVENT_HANDLER%7C{user_id}-WO-JSON&LS_schema=json&"
47
+ f"LS_requested_max_frequency=1&LS_table=6&LS_req_phase=624&LS_win_phase=50&LS_op=add&LS_session={session_id}&",
48
+
49
+ # Table 7 - OH JSON
50
+ f"control\r\nLS_mode=RAW&LS_id=V2-M-MESSAGE_EVENT_HANDLER%7C{user_id}-OH-JSON&LS_schema=json&"
51
+ f"LS_requested_max_frequency=1&LS_table=7&LS_req_phase=625&LS_win_phase=50&LS_op=add&LS_session={session_id}&"
52
+ ]
53
+
54
+ @staticmethod
55
+ def get_binary_fx_subscriptions(session_id):
56
+ """Generate binary FX pairs subscriptions (Tables 8-14)"""
57
+ pairs = [
58
+ ("8", "SAUDUSD"),
59
+ ("9", "SEURUSD"),
60
+ ("10", "SGBPUSD"),
61
+ ("11", "SUSDJPY"),
62
+ ("12", "SEURJPY"),
63
+ ("13", "SGBPJPY"),
64
+ ("14", "SUSDCAD"),
65
+ ]
66
+
67
+ messages = []
68
+ for table, symbol in pairs:
69
+ phase_val = str(625 + int(table))
70
+ msg = (
71
+ "control\r\n"
72
+ f"LS_mode=MERGE&LS_id=V2-F-LTP%2CUTM%7CCH.U.X%3A{symbol}:1321%3ABLD.OPT-1-1.IP&"
73
+ "LS_schema=lastTradedPrice%20updateTime&LS_snapshot=true&LS_requested_max_frequency=1&"
74
+ f"LS_table={table}&LS_req_phase={phase_val}&LS_win_phase=50&LS_op=add&LS_session={session_id}&"
75
+ )
76
+ messages.append(msg)
77
+
78
+ return messages
79
+
80
+ @staticmethod
81
+ def get_strike_message_type1(session_id, encoded_epic, table_counter, req_phase_counter, win_phase):
82
+ """Generate strike subscription message type 1"""
83
+ return (
84
+ "control\r\n"
85
+ f"LS_mode=MERGE&LS_id=V2-F-BD1%2CAK1%2CBS1%2CAS1%2CUTM%2CDLY%2CUBS%2CSWAP_3_SHORT%2CSWAP_3_LONG%7C{encoded_epic}&"
86
+ "LS_schema=displayOffer%20displayBid%20bidSize%20offerSize%20updateTime%20delayTime%20marketStatus%20swapPointSell%20swapPointBuy&"
87
+ f"LS_snapshot=true&LS_requested_max_frequency=1&LS_table={table_counter}&"
88
+ f"LS_req_phase={req_phase_counter}&LS_win_phase={win_phase}&LS_op=add&LS_session={session_id}&"
89
+ )
90
+
91
+ @staticmethod
92
+ def get_strike_message_type2(session_id, encoded_epic, table_counter, req_phase_counter, win_phase):
93
+ """Generate strike subscription message type 2 (BID/ASK)"""
94
+ return (
95
+ "control\r\n"
96
+ f"LS_mode=MERGE&LS_id=V2-F-BD1%2CAK1%2CBS1%2CAS1%2CBD2%2CAK2%2CBS2%2CAS2%2CBD3%2CAK3%2CBS3%2CAS3%2CBD4%2CAK4%2CBS4%2CAS4%2CBD5%2CAK5%2CBS5%2CAS5%7C{encoded_epic}&"
97
+ "LS_schema=displayOffer%20displayBid%20bidSize%20offerSize%20displayOffer2%20displayBid2%20bidSize2%20offerSize2%20displayOffer3%20displayBid3%20bidSize3%20offerSize3%20displayOffer4%20displayBid4%20bidSize4%20offerSize4%20displayOffer5%20displayBid5%20bidSize5%20offerSize5&"
98
+ f"LS_snapshot=true&LS_requested_max_frequency=1&LS_table={table_counter}&"
99
+ f"LS_req_phase={req_phase_counter}&LS_win_phase={win_phase}&LS_op=add&LS_session={session_id}&"
100
+ )
101
+
102
+ @staticmethod
103
+ def get_hierarchy_message(session_id, forex_id, table_counter, req_phase_counter, win_phase):
104
+ """Generate hierarchy subscription message"""
105
+ return (
106
+ "control\r\n"
107
+ f"LS_mode=RAW&LS_id=M___.MGE%7CHIER-{forex_id}-JSON&LS_schema=json&"
108
+ f"LS_requested_max_frequency=1&LS_table={table_counter}&"
109
+ f"LS_req_phase={req_phase_counter}&LS_win_phase={win_phase}&LS_op=add&LS_session={session_id}&"
110
+ )
111
+
112
+ @staticmethod
113
+ def get_ping_message(session_id, phase):
114
+ """Generate ping/keepalive message"""
115
+ return (
116
+ f"control\r\nLS_op=constrain&LS_session={session_id}&LS_phase={phase}&LS_cause=keepalive&"
117
+ f"LS_polling=true&LS_polling_millis=0&LS_idle_millis=0&LS_container=lsc&"
118
+ )
119
+
120
+ class MessageTable:
121
+ """Class to display message information in a table format"""
122
+
123
+ def __init__(self):
124
+ self.messages = []
125
+
126
+ def add_message(self, message_type, table_id, description, epic=None):
127
+ """Add a message to the table"""
128
+ self.messages.append({
129
+ 'type': message_type,
130
+ 'table': table_id,
131
+ 'description': description,
132
+ 'epic': epic or 'N/A'
133
+ })
134
+
135
+ def print_table(self):
136
+ """Print the message table"""
137
+ print("\n" + "="*100)
138
+ print("WEBSOCKET MESSAGE SUBSCRIPTION TABLE")
139
+ print("="*100)
140
+ print(f"{'Type':<20} {'Table':<8} {'Epic':<30} {'Description':<40}")
141
+ print("-"*100)
142
+
143
+ for msg in self.messages:
144
+ print(f"{msg['type']:<20} {msg['table']:<8} {msg['epic']:<30} {msg['description']:<40}")
145
+
146
+ print("-"*100)
147
+ print(f"Total Messages: {len(self.messages)}")
148
+ print("="*100)
149
+
150
+ def clear(self):
151
+ """Clear the message table"""
152
+ self.messages = []
153
+
154
+
@@ -0,0 +1,158 @@
1
+ # ---------------------------------------------------------------
2
+ # File : parsing.py
3
+ # Author : Shivam Garg
4
+ # Created on : 27-06-2005
5
+
6
+ # Copyright (c) Shivam Garg. All rights reserved.
7
+ # ---------------------------------------------------------------
8
+
9
+ import re
10
+ from collections import defaultdict
11
+
12
+ # Global table to epic mapping
13
+ table_to_epic = {}
14
+
15
+ # Regex to extract z() and d() calls
16
+ CALL_RE = re.compile(r"(z|d)\(\s*([^)]*?)\s*\)")
17
+
18
+ def update_table_mapping(epic, table_id, type):
19
+ """
20
+ Update the table_to_epic mapping when new subscriptions are made.
21
+ Call this function whenever you process the subscription table from logs.
22
+ """
23
+ global table_to_epic
24
+
25
+ table_to_epic[table_id] = epic + " " + type
26
+
27
+ print(f"[INFO] Updated table mappings for {len(table_to_epic)} tables")
28
+
29
+ def parse_csv_args(argstr):
30
+ """
31
+ Parse comma-separated arguments, handling quoted strings properly.
32
+ Returns list of cleaned argument strings.
33
+ """
34
+ # Handle quoted strings and regular comma separation
35
+ tokens = re.findall(r"""'[^']*'|[^,]+""", argstr)
36
+ # Strip whitespace and quotes, convert $ to empty string, # to None representation
37
+ parts = []
38
+ for token in tokens:
39
+ cleaned = token.strip().strip("'")
40
+ if cleaned == '$':
41
+ parts.append('') # Empty value
42
+ elif cleaned == '#':
43
+ parts.append(None) # Null value
44
+ else:
45
+ parts.append(cleaned)
46
+
47
+ return [p for p in parts if p is not None] # Filter out None values
48
+
49
+ def find_time_field(parts, start_idx=3):
50
+ """
51
+ Find the timestamp field in the message parts.
52
+ Looks for HH:MM:SS pattern starting from start_idx.
53
+ """
54
+ for i in range(start_idx, len(parts)):
55
+ if parts[i] and re.match(r"\d{2}:\d{2}:\d{2}", str(parts[i])):
56
+ return i, parts[i]
57
+ return None, None
58
+
59
+ def process_forex_prices(msg: str):
60
+ """
61
+ Process underlying forex price updates (tables 8-14).
62
+ These are the base currency pair prices that affect all options.
63
+ """
64
+ # Look for d() calls on tables 8-14 (forex underlying prices)
65
+ forex_tables = {
66
+ 8: "AUD/USD",
67
+ 9: "EUR/USD",
68
+ 10: "GBP/USD",
69
+ 11: "USD/JPY",
70
+ 12: "EUR/JPY",
71
+ 13: "GBP/JPY",
72
+ 14: "USD/CAD"
73
+ }
74
+
75
+ for match in CALL_RE.finditer(msg):
76
+ call_type, argstr = match.groups()
77
+ if call_type != 'd': # Only process updates for forex
78
+ continue
79
+
80
+ parts = parse_csv_args(argstr)
81
+ if len(parts) < 3:
82
+ continue
83
+
84
+ try:
85
+ tbl = int(parts[0])
86
+ except (ValueError, TypeError):
87
+ continue
88
+
89
+ if tbl in forex_tables:
90
+ price = parts[2] if len(parts) > 2 else "N/A"
91
+ time_idx, timestamp = find_time_field(parts)
92
+ if not timestamp:
93
+ timestamp = "N/A"
94
+
95
+ pair = forex_tables[tbl]
96
+ print(f"[FOREX] {pair:8} -> {price:>10} @ {timestamp}")
97
+
98
+ def process_option_prices(msg: str):
99
+ """
100
+ Process binary option price updates.
101
+ """
102
+ for match in CALL_RE.finditer(msg):
103
+ call_type, argstr = match.groups()
104
+ parts = parse_csv_args(argstr)
105
+
106
+ if len(parts) < 3:
107
+ continue
108
+
109
+ try:
110
+ tbl = int(parts[0])
111
+ except (ValueError, TypeError):
112
+ continue
113
+
114
+ epic = table_to_epic.get(tbl)
115
+ if not epic:
116
+ continue # Not one of our tracked option tables
117
+
118
+ # Extract key information
119
+ item = parts[1] if len(parts) > 1 else "1"
120
+ price = parts[2] if len(parts) > 2 else "N/A"
121
+
122
+ # For z() calls, bid/ask are typically in positions 2,3
123
+ # For d() calls, structure can vary
124
+ if call_type == "z":
125
+ # Initial price setting: z(tbl, item, bid, ask, size1, size2, time, ...)
126
+ bid = parts[2] if len(parts) > 2 else "N/A"
127
+ ask = parts[3] if len(parts) > 3 else "N/A"
128
+ time_idx, timestamp = find_time_field(parts, 4)
129
+ tag = "INIT"
130
+ else: # 'd' call
131
+ # Price update: d(tbl, item, price, [other_fields...], time, [more_fields...])
132
+ bid = parts[2] if len(parts) > 2 else "N/A"
133
+ ask = parts[3] if len(parts) > 3 else "N/A"
134
+ time_idx, timestamp = find_time_field(parts)
135
+ tag = "UPDATE"
136
+
137
+ if not timestamp:
138
+ timestamp = "N/A"
139
+
140
+ # Clean up epic name for display
141
+ epic_short = epic.replace("NB.I.", "").replace(".IP", "")
142
+
143
+ print(f"[{tag:6}] {epic_short:35} bid={bid:>6} ask={ask:>6} @ {timestamp}")
144
+
145
+ def process_message(msg: str):
146
+ """
147
+ Main message processor that handles both forex and option updates.
148
+ """
149
+ # Process forex underlying prices first
150
+ process_forex_prices(msg)
151
+
152
+ # Then process option prices
153
+ process_option_prices(msg)
154
+
155
+ def clear_table_mapping():
156
+ """Clear the global table_to_epic mapping."""
157
+ global table_to_epic
158
+ table_to_epic.clear()
@@ -0,0 +1,197 @@
1
+ # ---------------------------------------------------------------
2
+ # File : websocket_manager.py
3
+ # Author : Shivam Garg
4
+ # Created on : 27-06-2005
5
+
6
+ # Copyright (c) Shivam Garg. All rights reserved.
7
+ # ---------------------------------------------------------------
8
+
9
+ import asyncio
10
+ import datetime
11
+ import time
12
+ import websockets
13
+ from collections import defaultdict
14
+
15
+ from .config import Config
16
+ from nadex_dashboard.messages import WebSocketMessages, MessageTable
17
+ from .parsing import update_table_mapping, clear_table_mapping, process_message
18
+ from .frontend import relay_to_frontend
19
+ from .helpers import fetch_market_tree, extract_forex_ids, map_market_data
20
+
21
+ class WebSocketManager:
22
+ """Manages WebSocket connections and subscriptions to Nadex."""
23
+
24
+ def __init__(self, session_id, phase, host, market_mapping, forex_ids, shutdown_event):
25
+ self.session = session_id
26
+ self.phase = phase + 2
27
+ self.host = host
28
+ self.mapping = market_mapping
29
+ self.fx_ids = forex_ids
30
+ self.shutdown_event = shutdown_event
31
+
32
+ self.table_counter = Config.INITIAL_TABLE_COUNTER
33
+ self.req_phase_counter = Config.INITIAL_REQ_PHASE_COUNTER
34
+ self.win_phase = Config.WIN_PHASE
35
+
36
+ self.last_ping = time.time()
37
+ self.ping_interval = Config.PING_INTERVAL
38
+
39
+ self.message_table = MessageTable()
40
+
41
+ async def send_initial_subscriptions(self, ws):
42
+ """Send initial subscription messages."""
43
+ print("[+] Sending initial subscriptions…")
44
+ self.message_table.clear()
45
+
46
+ # bind_session
47
+ msg = WebSocketMessages.get_bind_session_message(self.session, self.phase)
48
+ await ws.send(msg)
49
+ self.message_table.add_message("BIND", 1, "Session bind")
50
+ await asyncio.sleep(0.1)
51
+
52
+ # core (2–7)
53
+ core = WebSocketMessages.get_core_subscriptions(self.session, Config.NADEX_USER_ID)
54
+ for i, m in enumerate(core, start=2):
55
+ await ws.send(m)
56
+ self.message_table.add_message("CORE", i, f"Core idx {i}")
57
+ await asyncio.sleep(0.1)
58
+
59
+ # binary FX (8–14)
60
+ bins = WebSocketMessages.get_binary_fx_subscriptions(self.session)
61
+ for idx, m in enumerate(bins, start=8):
62
+ await ws.send(m)
63
+ self.message_table.add_message("BINARY", idx, f"Bin idx {idx}")
64
+ await asyncio.sleep(0.1)
65
+
66
+ print(f"[+] Done init (1–14)")
67
+
68
+ async def send_strike_subscriptions(self, ws):
69
+ """Send strike subscription messages."""
70
+ print(f"[+] Starting strike subs at table {self.table_counter}")
71
+ count = 0
72
+ for mid, ueps in self.mapping.items():
73
+ for ue, eps in ueps.items():
74
+ for epic in eps:
75
+ if self.shutdown_event.is_set():
76
+ return
77
+
78
+ update_table_mapping(epic, self.table_counter, "STRIKE")
79
+
80
+ enc = epic.replace(".", "%2E").replace("-", "%2D")
81
+ m1 = WebSocketMessages.get_strike_message_type1(
82
+ self.session, enc, self.table_counter, self.req_phase_counter, self.win_phase
83
+ )
84
+ await ws.send(m1)
85
+ self.message_table.add_message("STRIKE1", self.table_counter, epic)
86
+ self.table_counter += 1
87
+ self.req_phase_counter += 1
88
+ await asyncio.sleep(0.05)
89
+
90
+ update_table_mapping(epic, self.table_counter, "ORDERBOOK")
91
+ m2 = WebSocketMessages.get_strike_message_type2(
92
+ self.session, enc, self.table_counter, self.req_phase_counter, self.win_phase
93
+ )
94
+ await ws.send(m2)
95
+ self.message_table.add_message("STRIKE2", self.table_counter, epic)
96
+ self.table_counter += 1
97
+ self.req_phase_counter += 1
98
+ await asyncio.sleep(0.05)
99
+ count += 1
100
+ print(f"[+] Sent {count} strike subs")
101
+
102
+ async def send_hierarchy_subscriptions(self, ws):
103
+ """Send hierarchy subscription messages."""
104
+ print(f"[+] Starting hierarchy subs at table {self.table_counter}")
105
+ for fid in self.fx_ids:
106
+ if self.shutdown_event.is_set():
107
+ return
108
+ m = WebSocketMessages.get_hierarchy_message(
109
+ self.session, fid, self.table_counter, self.req_phase_counter, self.win_phase
110
+ )
111
+ await ws.send(m)
112
+ self.message_table.add_message("HIER", self.table_counter, fid)
113
+ self.table_counter += 1
114
+ self.req_phase_counter += 1
115
+ await asyncio.sleep(0.1)
116
+ print(f"[+] Sent {len(self.fx_ids)} hierarchy subs")
117
+
118
+ async def handle_ping_pong(self, ws):
119
+ """Handle ping/pong messages to keep connection alive."""
120
+ while not self.shutdown_event.is_set():
121
+ if time.time() - self.last_ping >= self.ping_interval:
122
+ ping = WebSocketMessages.get_ping_message(self.session, self.phase)
123
+ await ws.send(ping)
124
+ self.last_ping = time.time()
125
+ print(f"[PING] @ {time.strftime('%H:%M:%S')}")
126
+ try:
127
+ await asyncio.wait_for(self.shutdown_event.wait(), timeout=1.0)
128
+ break
129
+ except asyncio.TimeoutError:
130
+ continue
131
+
132
+ async def resubscribe_instruments(self, ws):
133
+ """Periodically resubscribe to instruments."""
134
+ while not self.shutdown_event.is_set():
135
+ now = datetime.datetime.now()
136
+ # next 5-min mark
137
+ nxt = ((now.minute // 5) + 1) * 5
138
+ if nxt >= 60:
139
+ nxt_time = now.replace(hour=now.hour + 1, minute=0, second=0, microsecond=0)
140
+ else:
141
+ nxt_time = now.replace(minute=nxt, second=0, microsecond=0)
142
+ wait = (nxt_time - now).total_seconds()
143
+ print(f"[TIMER] wait {int(wait)}s until {nxt_time.strftime('%H:%M:%S')}")
144
+ try:
145
+ await asyncio.wait_for(self.shutdown_event.wait(), timeout=wait)
146
+ break
147
+ except asyncio.TimeoutError:
148
+ pass
149
+ if self.shutdown_event.is_set():
150
+ break
151
+
152
+ print(f"[RESUB] @ {datetime.datetime.now().strftime('%H:%M:%S')}")
153
+ tree = fetch_market_tree()
154
+ fx_ids = extract_forex_ids(tree)
155
+ if not fx_ids:
156
+ print("[-] none found on resub")
157
+ continue
158
+ self.mapping = map_market_data(fx_ids)
159
+ clear_table_mapping() # Clear old mappings
160
+ await self.send_strike_subscriptions(ws)
161
+
162
+ async def listen_and_relay(self):
163
+ """Main WebSocket listener that relays messages."""
164
+ uri = f"wss://{self.host}/lightstreamer"
165
+ async with websockets.connect(uri, subprotocols=["js.lightstreamer.com"]) as nadex_ws:
166
+ await self.send_initial_subscriptions(nadex_ws)
167
+ self.message_table.print_table()
168
+
169
+ await self.send_strike_subscriptions(nadex_ws)
170
+ await self.send_hierarchy_subscriptions(nadex_ws)
171
+ self.message_table.print_table()
172
+
173
+ # Start background tasks
174
+ ping_task = asyncio.create_task(self.handle_ping_pong(nadex_ws))
175
+ resub_task = asyncio.create_task(self.resubscribe_instruments(nadex_ws))
176
+
177
+ print("[+] Relaying Nadex β†’ Frontends…")
178
+ async for msg in nadex_ws:
179
+ if self.shutdown_event.is_set():
180
+ break
181
+
182
+ # Detect PONG
183
+ if "PONG" in msg.upper():
184
+ print(f"[PONG] @ {time.strftime('%H:%M:%S')}")
185
+
186
+ # Relay to frontend and process message
187
+ await relay_to_frontend(msg)
188
+ process_message(msg)
189
+
190
+ # Clean up background tasks
191
+ ping_task.cancel()
192
+ resub_task.cancel()
193
+ try:
194
+ await ping_task
195
+ await resub_task
196
+ except asyncio.CancelledError:
197
+ pass