cnhkmcp 1.1.6__tar.gz → 1.1.8__tar.gz
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.
- {cnhkmcp-1.1.6/cnhkmcp.egg-info → cnhkmcp-1.1.8}/PKG-INFO +1 -1
- {cnhkmcp-1.1.6 → cnhkmcp-1.1.8}/cnhkmcp/__init__.py +1 -1
- {cnhkmcp-1.1.6 → cnhkmcp-1.1.8}/cnhkmcp/untracked/platform_functions.py +120 -17
- {cnhkmcp-1.1.6 → cnhkmcp-1.1.8/cnhkmcp.egg-info}/PKG-INFO +1 -1
- {cnhkmcp-1.1.6 → cnhkmcp-1.1.8}/setup.py +1 -1
- {cnhkmcp-1.1.6 → cnhkmcp-1.1.8}/LICENSE +0 -0
- {cnhkmcp-1.1.6 → cnhkmcp-1.1.8}/MANIFEST.in +0 -0
- {cnhkmcp-1.1.6 → cnhkmcp-1.1.8}/README.md +0 -0
- {cnhkmcp-1.1.6 → cnhkmcp-1.1.8}/cnhkmcp/untracked/forum_functions.py +0 -0
- {cnhkmcp-1.1.6 → cnhkmcp-1.1.8}/cnhkmcp/untracked/user_config.json +0 -0
- {cnhkmcp-1.1.6 → cnhkmcp-1.1.8}/cnhkmcp.egg-info/SOURCES.txt +0 -0
- {cnhkmcp-1.1.6 → cnhkmcp-1.1.8}/cnhkmcp.egg-info/dependency_links.txt +0 -0
- {cnhkmcp-1.1.6 → cnhkmcp-1.1.8}/cnhkmcp.egg-info/entry_points.txt +0 -0
- {cnhkmcp-1.1.6 → cnhkmcp-1.1.8}/cnhkmcp.egg-info/not-zip-safe +0 -0
- {cnhkmcp-1.1.6 → cnhkmcp-1.1.8}/cnhkmcp.egg-info/requires.txt +0 -0
- {cnhkmcp-1.1.6 → cnhkmcp-1.1.8}/cnhkmcp.egg-info/top_level.txt +0 -0
- {cnhkmcp-1.1.6 → cnhkmcp-1.1.8}/requirements.txt +0 -0
- {cnhkmcp-1.1.6 → cnhkmcp-1.1.8}/setup.cfg +0 -0
|
@@ -81,8 +81,8 @@ class BrainApiClient:
|
|
|
81
81
|
print(f"[{level}] {message}", file=sys.stderr)
|
|
82
82
|
|
|
83
83
|
async def authenticate(self, email: str, password: str) -> Dict[str, Any]:
|
|
84
|
-
"""Authenticate with WorldQuant BRAIN platform
|
|
85
|
-
self.log("🔐 Starting
|
|
84
|
+
"""Authenticate with WorldQuant BRAIN platform with biometric support."""
|
|
85
|
+
self.log("🔐 Starting Authentication process...", "INFO")
|
|
86
86
|
|
|
87
87
|
try:
|
|
88
88
|
# Store credentials for potential re-authentication
|
|
@@ -106,7 +106,7 @@ class BrainApiClient:
|
|
|
106
106
|
|
|
107
107
|
# Check for successful authentication (status code 201)
|
|
108
108
|
if response.status_code == 201:
|
|
109
|
-
self.log("✅
|
|
109
|
+
self.log("✅ Authentication successful", "SUCCESS")
|
|
110
110
|
|
|
111
111
|
# Check if JWT token was automatically stored by session
|
|
112
112
|
jwt_token = self.session.cookies.get('t')
|
|
@@ -120,10 +120,26 @@ class BrainApiClient:
|
|
|
120
120
|
'user': {'email': email},
|
|
121
121
|
'status': 'authenticated',
|
|
122
122
|
'permissions': ['read', 'write'],
|
|
123
|
-
'message': '
|
|
123
|
+
'message': 'Authentication successful',
|
|
124
124
|
'status_code': response.status_code,
|
|
125
125
|
'has_jwt': jwt_token is not None
|
|
126
126
|
}
|
|
127
|
+
|
|
128
|
+
# Check if biometric authentication is required (401 with persona)
|
|
129
|
+
elif response.status_code == 401:
|
|
130
|
+
www_auth = response.headers.get("WWW-Authenticate")
|
|
131
|
+
location = response.headers.get("Location")
|
|
132
|
+
|
|
133
|
+
if www_auth == "persona" and location:
|
|
134
|
+
self.log("🔴 Biometric authentication required", "INFO")
|
|
135
|
+
|
|
136
|
+
# Handle biometric authentication
|
|
137
|
+
from urllib.parse import urljoin
|
|
138
|
+
biometric_url = urljoin(response.url, location)
|
|
139
|
+
|
|
140
|
+
return await self._handle_biometric_auth(biometric_url, email)
|
|
141
|
+
else:
|
|
142
|
+
raise Exception("Incorrect email or password")
|
|
127
143
|
else:
|
|
128
144
|
raise Exception(f"Authentication failed with status code: {response.status_code}")
|
|
129
145
|
|
|
@@ -134,6 +150,97 @@ class BrainApiClient:
|
|
|
134
150
|
self.log(f"❌ Authentication failed: {str(e)}", "ERROR")
|
|
135
151
|
raise
|
|
136
152
|
|
|
153
|
+
async def _handle_biometric_auth(self, biometric_url: str, email: str) -> Dict[str, Any]:
|
|
154
|
+
"""Handle biometric authentication using browser automation."""
|
|
155
|
+
self.log("🌐 Starting biometric authentication...", "INFO")
|
|
156
|
+
|
|
157
|
+
try:
|
|
158
|
+
# Import selenium for browser automation
|
|
159
|
+
from selenium import webdriver
|
|
160
|
+
from selenium.webdriver.chrome.options import Options
|
|
161
|
+
import time
|
|
162
|
+
|
|
163
|
+
# Setup Chrome options
|
|
164
|
+
options = Options()
|
|
165
|
+
options.add_argument('--no-sandbox')
|
|
166
|
+
options.add_argument('--disable-dev-shm-usage')
|
|
167
|
+
|
|
168
|
+
driver = None
|
|
169
|
+
try:
|
|
170
|
+
# Open browser with timeout
|
|
171
|
+
driver = webdriver.Chrome(options=options)
|
|
172
|
+
# Set a short timeout so it doesn't wait forever
|
|
173
|
+
driver.set_page_load_timeout(80) # Only wait 5 seconds
|
|
174
|
+
|
|
175
|
+
self.log("🌐 Opening browser for biometric authentication...", "INFO")
|
|
176
|
+
|
|
177
|
+
# Try to open the URL but handle timeout
|
|
178
|
+
try:
|
|
179
|
+
driver.get(biometric_url)
|
|
180
|
+
self.log("✅ Browser page loaded successfully", "SUCCESS")
|
|
181
|
+
except Exception as timeout_error:
|
|
182
|
+
self.log(f"⚠️ Page load timeout (expected): {str(timeout_error)[:50]}...", "WARNING")
|
|
183
|
+
self.log("✅ Browser window is open for biometric authentication", "INFO")
|
|
184
|
+
|
|
185
|
+
# Print instructions
|
|
186
|
+
print("\n" + "="*60, file=sys.stderr)
|
|
187
|
+
print("🔒 BIOMETRIC AUTHENTICATION REQUIRED", file=sys.stderr)
|
|
188
|
+
print("="*60, file=sys.stderr)
|
|
189
|
+
print("🌐 Browser window is open with biometric authentication page", file=sys.stderr)
|
|
190
|
+
print("🔧 Complete the biometric authentication in the browser", file=sys.stderr)
|
|
191
|
+
print("⏳ The system will automatically check when you're done...", file=sys.stderr)
|
|
192
|
+
print("="*60, file=sys.stderr)
|
|
193
|
+
|
|
194
|
+
# Keep checking until authentication is complete
|
|
195
|
+
max_attempts = 60 # 5 minutes maximum (60 * 5 seconds)
|
|
196
|
+
attempt = 0
|
|
197
|
+
|
|
198
|
+
while attempt < max_attempts:
|
|
199
|
+
time.sleep(5) # Check every 5 seconds
|
|
200
|
+
attempt += 1
|
|
201
|
+
|
|
202
|
+
# Check if authentication completed
|
|
203
|
+
check_response = self.session.post(biometric_url)
|
|
204
|
+
self.log(f"🔄 Checking authentication status (attempt {attempt}/{max_attempts}): {check_response.status_code}", "INFO")
|
|
205
|
+
|
|
206
|
+
if check_response.status_code == 201:
|
|
207
|
+
self.log("✅ Biometric authentication successful!", "SUCCESS")
|
|
208
|
+
|
|
209
|
+
# Close browser
|
|
210
|
+
driver.quit()
|
|
211
|
+
|
|
212
|
+
# Check JWT token
|
|
213
|
+
jwt_token = self.session.cookies.get('t')
|
|
214
|
+
if jwt_token:
|
|
215
|
+
self.log("✅ JWT token received", "SUCCESS")
|
|
216
|
+
|
|
217
|
+
# Return success response
|
|
218
|
+
return {
|
|
219
|
+
'user': {'email': email},
|
|
220
|
+
'status': 'authenticated',
|
|
221
|
+
'permissions': ['read', 'write'],
|
|
222
|
+
'message': 'Biometric authentication successful',
|
|
223
|
+
'status_code': check_response.status_code,
|
|
224
|
+
'has_jwt': jwt_token is not None
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
# If we get here, authentication timed out
|
|
228
|
+
if driver:
|
|
229
|
+
driver.quit()
|
|
230
|
+
raise Exception("Biometric authentication timed out")
|
|
231
|
+
|
|
232
|
+
except Exception as driver_error:
|
|
233
|
+
if driver:
|
|
234
|
+
try:
|
|
235
|
+
driver.quit()
|
|
236
|
+
except:
|
|
237
|
+
pass
|
|
238
|
+
raise Exception(f"Browser automation error: {driver_error}")
|
|
239
|
+
|
|
240
|
+
except Exception as e:
|
|
241
|
+
self.log(f"❌ Biometric authentication failed: {str(e)}", "ERROR")
|
|
242
|
+
raise
|
|
243
|
+
|
|
137
244
|
async def is_authenticated(self) -> bool:
|
|
138
245
|
"""Check if currently authenticated using JWT token."""
|
|
139
246
|
try:
|
|
@@ -322,13 +429,12 @@ class BrainApiClient:
|
|
|
322
429
|
self.log(f"Failed to get datafields: {str(e)}", "ERROR")
|
|
323
430
|
raise
|
|
324
431
|
|
|
325
|
-
async def get_alpha_pnl(self, alpha_id: str
|
|
432
|
+
async def get_alpha_pnl(self, alpha_id: str) -> Dict[str, Any]:
|
|
326
433
|
"""Get PnL data for an alpha."""
|
|
327
434
|
await self.ensure_authenticated()
|
|
328
435
|
|
|
329
436
|
try:
|
|
330
|
-
|
|
331
|
-
response = self.session.get(f"{self.base_url}/alphas/{alpha_id}/recordsets/{pnl_type}", params=params)
|
|
437
|
+
response = self.session.get(f"{self.base_url}/alphas/{alpha_id}/recordsets/pnl")
|
|
332
438
|
response.raise_for_status()
|
|
333
439
|
return response.json()
|
|
334
440
|
except Exception as e:
|
|
@@ -820,8 +926,7 @@ class BrainApiClient:
|
|
|
820
926
|
self.log(f"Failed to get user activities: {str(e)}", "ERROR")
|
|
821
927
|
raise
|
|
822
928
|
|
|
823
|
-
async def get_pyramid_multipliers(self
|
|
824
|
-
category: Optional[str] = None) -> Dict[str, Any]:
|
|
929
|
+
async def get_pyramid_multipliers(self) -> Dict[str, Any]:
|
|
825
930
|
"""Get current pyramid multipliers showing BRAIN's encouragement levels."""
|
|
826
931
|
await self.ensure_authenticated()
|
|
827
932
|
|
|
@@ -1352,19 +1457,18 @@ async def get_datafields(
|
|
|
1352
1457
|
return {"error": str(e)}
|
|
1353
1458
|
|
|
1354
1459
|
@mcp.tool()
|
|
1355
|
-
async def get_alpha_pnl(alpha_id: str
|
|
1460
|
+
async def get_alpha_pnl(alpha_id: str) -> Dict[str, Any]:
|
|
1356
1461
|
"""
|
|
1357
1462
|
📈 Get PnL (Profit and Loss) data for an alpha.
|
|
1358
1463
|
|
|
1359
1464
|
Args:
|
|
1360
1465
|
alpha_id: The ID of the alpha
|
|
1361
|
-
pnl_type: Type of PnL data ("pnl" for cumulative, "daily-pnl" for daily)
|
|
1362
1466
|
|
|
1363
1467
|
Returns:
|
|
1364
1468
|
PnL data for the alpha
|
|
1365
1469
|
"""
|
|
1366
1470
|
try:
|
|
1367
|
-
return await brain_client.get_alpha_pnl(alpha_id
|
|
1471
|
+
return await brain_client.get_alpha_pnl(alpha_id)
|
|
1368
1472
|
except Exception as e:
|
|
1369
1473
|
return {"error": str(e)}
|
|
1370
1474
|
|
|
@@ -1376,8 +1480,8 @@ async def get_user_alphas(stage: str = "IS", limit: int = 100, offset: int = 0)
|
|
|
1376
1480
|
Args:
|
|
1377
1481
|
stage: Alpha stage ("IS" for in-sample, "OS" for out-of-sample)
|
|
1378
1482
|
limit: Maximum number of alphas to return
|
|
1379
|
-
offset: Offset for pagination
|
|
1380
|
-
|
|
1483
|
+
offset: Offset for pagination, if the desired total number is larger than 100. then it is useful, every page, the load max is 100 alphas.
|
|
1484
|
+
|
|
1381
1485
|
Returns:
|
|
1382
1486
|
User's alphas
|
|
1383
1487
|
"""
|
|
@@ -1745,11 +1849,10 @@ async def get_user_activities(user_id: str, grouping: Optional[str] = None) -> D
|
|
|
1745
1849
|
return {"error": str(e)}
|
|
1746
1850
|
|
|
1747
1851
|
@mcp.tool()
|
|
1748
|
-
async def get_pyramid_multipliers(
|
|
1749
|
-
category: Optional[str] = None) -> Dict[str, Any]:
|
|
1852
|
+
async def get_pyramid_multipliers() -> Dict[str, Any]:
|
|
1750
1853
|
"""Get current pyramid multipliers showing BRAIN's encouragement levels."""
|
|
1751
1854
|
try:
|
|
1752
|
-
return await brain_client.get_pyramid_multipliers(
|
|
1855
|
+
return await brain_client.get_pyramid_multipliers()
|
|
1753
1856
|
except Exception as e:
|
|
1754
1857
|
return {"error": str(e)}
|
|
1755
1858
|
|
|
@@ -13,7 +13,7 @@ def read_requirements():
|
|
|
13
13
|
|
|
14
14
|
setup(
|
|
15
15
|
name="cnhkmcp",
|
|
16
|
-
version="1.1.
|
|
16
|
+
version="1.1.8",
|
|
17
17
|
author="CNHK",
|
|
18
18
|
author_email="cnhk@example.com",
|
|
19
19
|
description="A comprehensive Model Context Protocol (MCP) server for quantitative trading platform integration",
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|