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.
- nadex_cli-1.0.0.dist-info/METADATA +239 -0
- nadex_cli-1.0.0.dist-info/RECORD +15 -0
- nadex_cli-1.0.0.dist-info/WHEEL +5 -0
- nadex_cli-1.0.0.dist-info/entry_points.txt +2 -0
- nadex_cli-1.0.0.dist-info/licenses/LICENSE +21 -0
- nadex_cli-1.0.0.dist-info/top_level.txt +1 -0
- nadex_dashboard/__init__.py +1 -0
- nadex_dashboard/__main__.py +0 -0
- nadex_dashboard/config.py +105 -0
- nadex_dashboard/frontend.py +84 -0
- nadex_dashboard/helpers.py +117 -0
- nadex_dashboard/main.py +95 -0
- nadex_dashboard/messages.py +154 -0
- nadex_dashboard/parsing.py +158 -0
- nadex_dashboard/websocket_manager.py +197 -0
@@ -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,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
|
nadex_dashboard/main.py
ADDED
@@ -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
|