futu-stock-mcp-server 0.1.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.
Potentially problematic release.
This version of futu-stock-mcp-server might be problematic. Click here for more details.
- futu_stock_mcp_server/__init__.py +0 -0
- futu_stock_mcp_server/server.py +1475 -0
- futu_stock_mcp_server-0.1.0.dist-info/METADATA +661 -0
- futu_stock_mcp_server-0.1.0.dist-info/RECORD +7 -0
- futu_stock_mcp_server-0.1.0.dist-info/WHEEL +4 -0
- futu_stock_mcp_server-0.1.0.dist-info/entry_points.txt +2 -0
- futu_stock_mcp_server-0.1.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,1475 @@
|
|
|
1
|
+
from contextlib import asynccontextmanager
|
|
2
|
+
from collections.abc import AsyncIterator
|
|
3
|
+
from typing import Dict, Any, List, Optional
|
|
4
|
+
from futu import OpenQuoteContext, OpenSecTradeContext, TrdMarket, SecurityFirm, RET_OK
|
|
5
|
+
import json
|
|
6
|
+
import asyncio
|
|
7
|
+
from loguru import logger
|
|
8
|
+
import os
|
|
9
|
+
import sys
|
|
10
|
+
from dotenv import load_dotenv
|
|
11
|
+
from mcp.server.fastmcp import FastMCP
|
|
12
|
+
from mcp.types import TextContent, PromptMessage
|
|
13
|
+
from mcp.server import Server
|
|
14
|
+
import atexit
|
|
15
|
+
import signal
|
|
16
|
+
import fcntl
|
|
17
|
+
import psutil
|
|
18
|
+
import time
|
|
19
|
+
from datetime import datetime
|
|
20
|
+
|
|
21
|
+
# Get the project root directory and add it to Python path
|
|
22
|
+
project_root = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
|
23
|
+
sys.path.insert(0, project_root)
|
|
24
|
+
|
|
25
|
+
# Load environment variables from project root
|
|
26
|
+
env_path = os.path.join(project_root, '.env')
|
|
27
|
+
load_dotenv(env_path)
|
|
28
|
+
|
|
29
|
+
# Configure logging
|
|
30
|
+
logger.remove() # Remove default handler
|
|
31
|
+
|
|
32
|
+
# Get the project root directory
|
|
33
|
+
log_dir = os.path.join(project_root, "logs")
|
|
34
|
+
os.makedirs(log_dir, exist_ok=True)
|
|
35
|
+
|
|
36
|
+
# Add file handler
|
|
37
|
+
logger.add(
|
|
38
|
+
os.path.join(log_dir, "futu_server.log"),
|
|
39
|
+
rotation="500 MB",
|
|
40
|
+
retention="10 days",
|
|
41
|
+
level="DEBUG",
|
|
42
|
+
format="{time:YYYY-MM-DD HH:mm:ss} | {level} | {message}"
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
# Add console handler
|
|
46
|
+
logger.add(
|
|
47
|
+
lambda msg: print(msg),
|
|
48
|
+
level="INFO",
|
|
49
|
+
format="{time:YYYY-MM-DD HH:mm:ss} | {level} | {message}"
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
logger.info(f"Starting server with log directory: {log_dir}")
|
|
53
|
+
|
|
54
|
+
# PID file path
|
|
55
|
+
PID_FILE = os.path.join(project_root, '.futu_mcp.pid')
|
|
56
|
+
LOCK_FILE = os.path.join(project_root, '.futu_mcp.lock')
|
|
57
|
+
|
|
58
|
+
# Global variables
|
|
59
|
+
quote_ctx = None
|
|
60
|
+
trade_ctx = None
|
|
61
|
+
lock_fd = None
|
|
62
|
+
_is_shutting_down = False
|
|
63
|
+
_is_trade_initialized = False
|
|
64
|
+
|
|
65
|
+
def is_process_running(pid):
|
|
66
|
+
"""Check if a process with given PID is running"""
|
|
67
|
+
try:
|
|
68
|
+
return psutil.pid_exists(pid)
|
|
69
|
+
except:
|
|
70
|
+
return False
|
|
71
|
+
|
|
72
|
+
def cleanup_stale_processes():
|
|
73
|
+
"""Clean up any stale Futu processes"""
|
|
74
|
+
global _is_shutting_down
|
|
75
|
+
if _is_shutting_down:
|
|
76
|
+
return
|
|
77
|
+
|
|
78
|
+
try:
|
|
79
|
+
# 只检查 PID 文件中的进程
|
|
80
|
+
if os.path.exists(PID_FILE):
|
|
81
|
+
try:
|
|
82
|
+
with open(PID_FILE, 'r') as f:
|
|
83
|
+
old_pid = int(f.read().strip())
|
|
84
|
+
if old_pid != os.getpid():
|
|
85
|
+
try:
|
|
86
|
+
old_proc = psutil.Process(old_pid)
|
|
87
|
+
if any('futu_stock_mcp_server' in cmd for cmd in old_proc.cmdline()):
|
|
88
|
+
logger.info(f"Found stale process {old_pid}")
|
|
89
|
+
old_proc.terminate()
|
|
90
|
+
try:
|
|
91
|
+
old_proc.wait(timeout=3)
|
|
92
|
+
except psutil.TimeoutExpired:
|
|
93
|
+
old_proc.kill()
|
|
94
|
+
except (psutil.NoSuchProcess, psutil.AccessDenied):
|
|
95
|
+
pass
|
|
96
|
+
except (IOError, ValueError):
|
|
97
|
+
pass
|
|
98
|
+
|
|
99
|
+
# 清理 PID 文件
|
|
100
|
+
try:
|
|
101
|
+
os.unlink(PID_FILE)
|
|
102
|
+
except OSError:
|
|
103
|
+
pass
|
|
104
|
+
|
|
105
|
+
# 清理锁文件
|
|
106
|
+
if os.path.exists(LOCK_FILE):
|
|
107
|
+
try:
|
|
108
|
+
os.unlink(LOCK_FILE)
|
|
109
|
+
except OSError:
|
|
110
|
+
pass
|
|
111
|
+
|
|
112
|
+
except Exception as e:
|
|
113
|
+
logger.error(f"Error cleaning up stale processes: {str(e)}")
|
|
114
|
+
|
|
115
|
+
def cleanup_connections():
|
|
116
|
+
"""Clean up Futu connections"""
|
|
117
|
+
global quote_ctx, trade_ctx
|
|
118
|
+
try:
|
|
119
|
+
if quote_ctx:
|
|
120
|
+
try:
|
|
121
|
+
quote_ctx.close()
|
|
122
|
+
logger.info("Successfully closed quote context")
|
|
123
|
+
except Exception as e:
|
|
124
|
+
logger.error(f"Error closing quote context: {str(e)}")
|
|
125
|
+
quote_ctx = None
|
|
126
|
+
|
|
127
|
+
if trade_ctx:
|
|
128
|
+
try:
|
|
129
|
+
trade_ctx.close()
|
|
130
|
+
logger.info("Successfully closed trade context")
|
|
131
|
+
except Exception as e:
|
|
132
|
+
logger.error(f"Error closing trade context: {str(e)}")
|
|
133
|
+
trade_ctx = None
|
|
134
|
+
|
|
135
|
+
# 等待连接完全关闭
|
|
136
|
+
time.sleep(1)
|
|
137
|
+
except Exception as e:
|
|
138
|
+
logger.error(f"Error during connection cleanup: {str(e)}")
|
|
139
|
+
|
|
140
|
+
def release_lock():
|
|
141
|
+
"""Release the process lock"""
|
|
142
|
+
global lock_fd
|
|
143
|
+
try:
|
|
144
|
+
if lock_fd is not None:
|
|
145
|
+
fcntl.flock(lock_fd, fcntl.LOCK_UN)
|
|
146
|
+
os.close(lock_fd)
|
|
147
|
+
lock_fd = None
|
|
148
|
+
if os.path.exists(LOCK_FILE):
|
|
149
|
+
os.unlink(LOCK_FILE)
|
|
150
|
+
if os.path.exists(PID_FILE):
|
|
151
|
+
os.unlink(PID_FILE)
|
|
152
|
+
except Exception as e:
|
|
153
|
+
logger.error(f"Error releasing lock: {str(e)}")
|
|
154
|
+
|
|
155
|
+
def cleanup_all():
|
|
156
|
+
"""Clean up all resources on exit"""
|
|
157
|
+
global _is_shutting_down
|
|
158
|
+
if _is_shutting_down:
|
|
159
|
+
return
|
|
160
|
+
_is_shutting_down = True
|
|
161
|
+
|
|
162
|
+
cleanup_connections()
|
|
163
|
+
release_lock()
|
|
164
|
+
cleanup_stale_processes()
|
|
165
|
+
|
|
166
|
+
def signal_handler(signum, frame):
|
|
167
|
+
"""Handle process signals"""
|
|
168
|
+
global _is_shutting_down
|
|
169
|
+
if _is_shutting_down:
|
|
170
|
+
return
|
|
171
|
+
|
|
172
|
+
# 只处理 SIGINT 和 SIGTERM
|
|
173
|
+
if signum not in (signal.SIGINT, signal.SIGTERM):
|
|
174
|
+
return
|
|
175
|
+
|
|
176
|
+
logger.info(f"Received signal {signum}, cleaning up...")
|
|
177
|
+
cleanup_all()
|
|
178
|
+
sys.exit(0)
|
|
179
|
+
|
|
180
|
+
# Register cleanup functions
|
|
181
|
+
atexit.register(cleanup_all)
|
|
182
|
+
signal.signal(signal.SIGINT, signal_handler)
|
|
183
|
+
signal.signal(signal.SIGTERM, signal_handler)
|
|
184
|
+
|
|
185
|
+
def acquire_lock():
|
|
186
|
+
"""Try to acquire the process lock"""
|
|
187
|
+
try:
|
|
188
|
+
# 先检查 PID 文件
|
|
189
|
+
if os.path.exists(PID_FILE):
|
|
190
|
+
try:
|
|
191
|
+
with open(PID_FILE, 'r') as f:
|
|
192
|
+
old_pid = int(f.read().strip())
|
|
193
|
+
if old_pid != os.getpid() and psutil.pid_exists(old_pid):
|
|
194
|
+
try:
|
|
195
|
+
old_proc = psutil.Process(old_pid)
|
|
196
|
+
if any('futu_stock_mcp_server' in cmd for cmd in old_proc.cmdline()):
|
|
197
|
+
logger.error(f"Another instance is already running (PID: {old_pid})")
|
|
198
|
+
return None
|
|
199
|
+
except (psutil.NoSuchProcess, psutil.AccessDenied):
|
|
200
|
+
pass
|
|
201
|
+
except (IOError, ValueError):
|
|
202
|
+
pass
|
|
203
|
+
|
|
204
|
+
# 创建锁文件
|
|
205
|
+
lock_fd = os.open(LOCK_FILE, os.O_CREAT | os.O_RDWR)
|
|
206
|
+
try:
|
|
207
|
+
fcntl.flock(lock_fd, fcntl.LOCK_EX | fcntl.LOCK_NB)
|
|
208
|
+
except IOError:
|
|
209
|
+
os.close(lock_fd)
|
|
210
|
+
return None
|
|
211
|
+
|
|
212
|
+
# 写入 PID 文件
|
|
213
|
+
with open(PID_FILE, 'w') as f:
|
|
214
|
+
f.write(str(os.getpid()))
|
|
215
|
+
|
|
216
|
+
return lock_fd
|
|
217
|
+
except Exception as e:
|
|
218
|
+
logger.error(f"Failed to acquire lock: {str(e)}")
|
|
219
|
+
if 'lock_fd' in locals():
|
|
220
|
+
try:
|
|
221
|
+
os.close(lock_fd)
|
|
222
|
+
except:
|
|
223
|
+
pass
|
|
224
|
+
return None
|
|
225
|
+
|
|
226
|
+
def init_quote_connection():
|
|
227
|
+
"""Initialize quote connection only"""
|
|
228
|
+
global quote_ctx
|
|
229
|
+
|
|
230
|
+
try:
|
|
231
|
+
# Check if OpenD is running by attempting to get global state
|
|
232
|
+
try:
|
|
233
|
+
temp_ctx = OpenQuoteContext(
|
|
234
|
+
host=os.getenv('FUTU_HOST', '127.0.0.1'),
|
|
235
|
+
port=int(os.getenv('FUTU_PORT', '11111'))
|
|
236
|
+
)
|
|
237
|
+
ret, _ = temp_ctx.get_global_state()
|
|
238
|
+
temp_ctx.close()
|
|
239
|
+
if ret != RET_OK:
|
|
240
|
+
logger.error("OpenD is not running or not accessible")
|
|
241
|
+
return False
|
|
242
|
+
except Exception as e:
|
|
243
|
+
logger.error(f"Failed to connect to OpenD: {str(e)}")
|
|
244
|
+
return False
|
|
245
|
+
|
|
246
|
+
# Initialize Futu connection
|
|
247
|
+
quote_ctx = OpenQuoteContext(
|
|
248
|
+
host=os.getenv('FUTU_HOST', '127.0.0.1'),
|
|
249
|
+
port=int(os.getenv('FUTU_PORT', '11111'))
|
|
250
|
+
)
|
|
251
|
+
logger.info("Successfully connected to Futu Quote API")
|
|
252
|
+
return True
|
|
253
|
+
|
|
254
|
+
except Exception as e:
|
|
255
|
+
logger.error(f"Failed to initialize quote connection: {str(e)}")
|
|
256
|
+
cleanup_connections()
|
|
257
|
+
return False
|
|
258
|
+
|
|
259
|
+
def init_trade_connection():
|
|
260
|
+
"""Initialize trade connection only"""
|
|
261
|
+
global trade_ctx, _is_trade_initialized
|
|
262
|
+
|
|
263
|
+
if _is_trade_initialized and trade_ctx:
|
|
264
|
+
return True
|
|
265
|
+
|
|
266
|
+
try:
|
|
267
|
+
# Initialize trade context with proper market access
|
|
268
|
+
trade_env = os.getenv('FUTU_TRADE_ENV', 'SIMULATE')
|
|
269
|
+
security_firm = getattr(SecurityFirm, os.getenv('FUTU_SECURITY_FIRM', 'FUTUSECURITIES'))
|
|
270
|
+
|
|
271
|
+
# 只支持港股和美股
|
|
272
|
+
market_map = {
|
|
273
|
+
'HK': 1, # TrdMarket.HK
|
|
274
|
+
'US': 2 # TrdMarket.US
|
|
275
|
+
}
|
|
276
|
+
trd_market = market_map.get(os.getenv('FUTU_TRD_MARKET', 'HK'), 1)
|
|
277
|
+
|
|
278
|
+
# 创建交易上下文
|
|
279
|
+
trade_ctx = OpenSecTradeContext(
|
|
280
|
+
filter_trdmarket=trd_market,
|
|
281
|
+
host=os.getenv('FUTU_HOST', '127.0.0.1'),
|
|
282
|
+
port=int(os.getenv('FUTU_PORT', '11111')),
|
|
283
|
+
security_firm=security_firm
|
|
284
|
+
)
|
|
285
|
+
|
|
286
|
+
# 等待连接就绪
|
|
287
|
+
time.sleep(1)
|
|
288
|
+
|
|
289
|
+
# 验证连接状态
|
|
290
|
+
if not trade_ctx:
|
|
291
|
+
raise Exception("Failed to create trade context")
|
|
292
|
+
|
|
293
|
+
# Set trade environment
|
|
294
|
+
if hasattr(trade_ctx, 'set_trade_env'):
|
|
295
|
+
ret, data = trade_ctx.set_trade_env(trade_env)
|
|
296
|
+
if ret != RET_OK:
|
|
297
|
+
logger.warning(f"Failed to set trade environment: {data}")
|
|
298
|
+
|
|
299
|
+
# Verify account access and permissions
|
|
300
|
+
ret, data = trade_ctx.get_acc_list()
|
|
301
|
+
if ret != RET_OK:
|
|
302
|
+
logger.warning(f"Failed to get account list: {data}")
|
|
303
|
+
cleanup_connections()
|
|
304
|
+
return False
|
|
305
|
+
|
|
306
|
+
if data is None or len(data) == 0:
|
|
307
|
+
logger.warning("No trading accounts available")
|
|
308
|
+
cleanup_connections()
|
|
309
|
+
return False
|
|
310
|
+
|
|
311
|
+
# Convert DataFrame to records if necessary
|
|
312
|
+
if hasattr(data, 'to_dict'):
|
|
313
|
+
accounts = data.to_dict('records')
|
|
314
|
+
else:
|
|
315
|
+
accounts = data
|
|
316
|
+
|
|
317
|
+
logger.info(f"Found {len(accounts)} trading account(s)")
|
|
318
|
+
|
|
319
|
+
# 检查账户状态
|
|
320
|
+
for acc in accounts:
|
|
321
|
+
if isinstance(acc, dict):
|
|
322
|
+
acc_id = acc.get('acc_id', 'Unknown')
|
|
323
|
+
acc_type = acc.get('acc_type', 'Unknown')
|
|
324
|
+
acc_state = acc.get('acc_state', 'Unknown')
|
|
325
|
+
trd_env = acc.get('trd_env', 'Unknown')
|
|
326
|
+
trd_market = acc.get('trd_market', 'Unknown')
|
|
327
|
+
else:
|
|
328
|
+
acc_id = getattr(acc, 'acc_id', 'Unknown')
|
|
329
|
+
acc_type = getattr(acc, 'acc_type', 'Unknown')
|
|
330
|
+
acc_state = getattr(acc, 'acc_state', 'Unknown')
|
|
331
|
+
trd_env = getattr(acc, 'trd_env', 'Unknown')
|
|
332
|
+
trd_market = getattr(acc, 'trd_market', 'Unknown')
|
|
333
|
+
|
|
334
|
+
logger.info(f"Account: {acc_id}, Type: {acc_type}, State: {acc_state}, Environment: {trd_env}, Market: {trd_market}")
|
|
335
|
+
|
|
336
|
+
_is_trade_initialized = True
|
|
337
|
+
logger.info(f"Successfully initialized trade connection (Trade Environment: {trade_env}, Security Firm: {security_firm}, Market: {trd_market})")
|
|
338
|
+
return True
|
|
339
|
+
|
|
340
|
+
except Exception as e:
|
|
341
|
+
logger.error(f"Failed to initialize trade connection: {str(e)}")
|
|
342
|
+
cleanup_connections()
|
|
343
|
+
_is_trade_initialized = False
|
|
344
|
+
return False
|
|
345
|
+
|
|
346
|
+
def init_futu_connection():
|
|
347
|
+
"""Initialize both quote and trade connections"""
|
|
348
|
+
return init_quote_connection()
|
|
349
|
+
|
|
350
|
+
@asynccontextmanager
|
|
351
|
+
async def lifespan(server: Server):
|
|
352
|
+
# Startup - only initialize quote connection
|
|
353
|
+
if not init_quote_connection():
|
|
354
|
+
logger.error("Failed to initialize quote connection")
|
|
355
|
+
raise Exception("Quote connection failed")
|
|
356
|
+
try:
|
|
357
|
+
yield
|
|
358
|
+
finally:
|
|
359
|
+
# Shutdown - ensure connections are closed
|
|
360
|
+
cleanup_all()
|
|
361
|
+
|
|
362
|
+
# Create MCP server instance
|
|
363
|
+
mcp = FastMCP("futu-stock-server", lifespan=lifespan)
|
|
364
|
+
|
|
365
|
+
def handle_return_data(ret: int, data: Any) -> Dict[str, Any]:
|
|
366
|
+
"""Helper function to handle return data from Futu API
|
|
367
|
+
|
|
368
|
+
Args:
|
|
369
|
+
ret: Return code from Futu API
|
|
370
|
+
data: Data returned from Futu API
|
|
371
|
+
|
|
372
|
+
Returns:
|
|
373
|
+
Dict containing either the data or error message
|
|
374
|
+
"""
|
|
375
|
+
if ret != RET_OK:
|
|
376
|
+
return {'error': str(data)}
|
|
377
|
+
|
|
378
|
+
# If data is already a dict, return it directly
|
|
379
|
+
if isinstance(data, dict):
|
|
380
|
+
return data
|
|
381
|
+
|
|
382
|
+
# If data has to_dict method, call it
|
|
383
|
+
if hasattr(data, 'to_dict'):
|
|
384
|
+
return data.to_dict()
|
|
385
|
+
|
|
386
|
+
# If data is a pandas DataFrame, convert to dict
|
|
387
|
+
if hasattr(data, 'to_dict') and callable(getattr(data, 'to_dict')):
|
|
388
|
+
return data.to_dict('records')
|
|
389
|
+
|
|
390
|
+
# For other types, try to convert to dict or return as is
|
|
391
|
+
try:
|
|
392
|
+
return dict(data)
|
|
393
|
+
except (TypeError, ValueError):
|
|
394
|
+
return {'data': data}
|
|
395
|
+
|
|
396
|
+
# Market Data Tools
|
|
397
|
+
@mcp.tool()
|
|
398
|
+
async def get_stock_quote(symbols: List[str]) -> Dict[str, Any]:
|
|
399
|
+
"""Get stock quote data for given symbols
|
|
400
|
+
|
|
401
|
+
Args:
|
|
402
|
+
symbols: List of stock codes, e.g. ["HK.00700", "US.AAPL", "SH.600519"]
|
|
403
|
+
Format: {market}.{code}
|
|
404
|
+
- HK: Hong Kong stocks
|
|
405
|
+
- US: US stocks
|
|
406
|
+
- SH: Shanghai stocks
|
|
407
|
+
- SZ: Shenzhen stocks
|
|
408
|
+
|
|
409
|
+
Returns:
|
|
410
|
+
Dict containing quote data including:
|
|
411
|
+
- quote_list: List of quote data entries, each containing:
|
|
412
|
+
- code: Stock code
|
|
413
|
+
- update_time: Update time (YYYY-MM-DD HH:mm:ss)
|
|
414
|
+
- last_price: Latest price
|
|
415
|
+
- open_price: Opening price
|
|
416
|
+
- high_price: Highest price
|
|
417
|
+
- low_price: Lowest price
|
|
418
|
+
- prev_close_price: Previous closing price
|
|
419
|
+
- volume: Trading volume
|
|
420
|
+
- turnover: Trading amount
|
|
421
|
+
- turnover_rate: Turnover rate
|
|
422
|
+
- amplitude: Price amplitude
|
|
423
|
+
- dark_status: Dark pool status (0: Normal)
|
|
424
|
+
- list_time: Listing date
|
|
425
|
+
- price_spread: Price spread
|
|
426
|
+
- stock_owner: Stock owner
|
|
427
|
+
- lot_size: Lot size
|
|
428
|
+
- sec_status: Security status
|
|
429
|
+
|
|
430
|
+
Raises:
|
|
431
|
+
- INVALID_PARAM: Invalid parameter
|
|
432
|
+
- INVALID_CODE: Invalid stock code format
|
|
433
|
+
- GET_STOCK_QUOTE_FAILED: Failed to get stock quote
|
|
434
|
+
|
|
435
|
+
Note:
|
|
436
|
+
- Stock quote contains latest market data
|
|
437
|
+
- Can request multiple stocks at once
|
|
438
|
+
- Does not include historical data
|
|
439
|
+
- Consider actual needs when selecting stocks
|
|
440
|
+
- Handle exceptions properly
|
|
441
|
+
"""
|
|
442
|
+
ret, data = quote_ctx.get_stock_quote(symbols)
|
|
443
|
+
if ret != RET_OK:
|
|
444
|
+
return {'error': str(data)}
|
|
445
|
+
|
|
446
|
+
# Convert DataFrame to dict if necessary
|
|
447
|
+
if hasattr(data, 'to_dict'):
|
|
448
|
+
result = {
|
|
449
|
+
'quote_list': data.to_dict('records')
|
|
450
|
+
}
|
|
451
|
+
else:
|
|
452
|
+
result = {
|
|
453
|
+
'quote_list': data
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
return result
|
|
457
|
+
|
|
458
|
+
@mcp.tool()
|
|
459
|
+
async def get_market_snapshot(symbols: List[str]) -> Dict[str, Any]:
|
|
460
|
+
"""Get market snapshot for given symbols
|
|
461
|
+
|
|
462
|
+
Args:
|
|
463
|
+
symbols: List of stock codes, e.g. ["HK.00700", "US.AAPL", "SH.600519"]
|
|
464
|
+
Format: {market}.{code}
|
|
465
|
+
- HK: Hong Kong stocks
|
|
466
|
+
- US: US stocks
|
|
467
|
+
- SH: Shanghai stocks
|
|
468
|
+
- SZ: Shenzhen stocks
|
|
469
|
+
|
|
470
|
+
Returns:
|
|
471
|
+
Dict containing snapshot data including:
|
|
472
|
+
- snapshot_list: List of snapshot data entries, each containing:
|
|
473
|
+
- code: Stock code
|
|
474
|
+
- update_time: Update time (YYYY-MM-DD HH:mm:ss)
|
|
475
|
+
- last_price: Latest price
|
|
476
|
+
- open_price: Opening price
|
|
477
|
+
- high_price: Highest price
|
|
478
|
+
- low_price: Lowest price
|
|
479
|
+
- prev_close_price: Previous closing price
|
|
480
|
+
- volume: Trading volume
|
|
481
|
+
- turnover: Trading amount
|
|
482
|
+
- turnover_rate: Turnover rate
|
|
483
|
+
- amplitude: Price amplitude
|
|
484
|
+
- dark_status: Dark pool status (0: Normal)
|
|
485
|
+
- list_time: Listing date
|
|
486
|
+
- price_spread: Price spread
|
|
487
|
+
- stock_owner: Stock owner
|
|
488
|
+
- lot_size: Lot size
|
|
489
|
+
- sec_status: Security status
|
|
490
|
+
- bid_price: List of bid prices
|
|
491
|
+
- bid_volume: List of bid volumes
|
|
492
|
+
- ask_price: List of ask prices
|
|
493
|
+
- ask_volume: List of ask volumes
|
|
494
|
+
|
|
495
|
+
Raises:
|
|
496
|
+
- INVALID_PARAM: Invalid parameter
|
|
497
|
+
- INVALID_CODE: Invalid stock code format
|
|
498
|
+
- GET_MARKET_SNAPSHOT_FAILED: Failed to get market snapshot
|
|
499
|
+
|
|
500
|
+
Note:
|
|
501
|
+
- Market snapshot contains latest market data
|
|
502
|
+
- Can request multiple stocks at once
|
|
503
|
+
- Does not include historical data
|
|
504
|
+
- Consider actual needs when selecting stocks
|
|
505
|
+
- Handle exceptions properly
|
|
506
|
+
"""
|
|
507
|
+
ret, data = quote_ctx.get_market_snapshot(symbols)
|
|
508
|
+
if ret != RET_OK:
|
|
509
|
+
return {'error': str(data)}
|
|
510
|
+
|
|
511
|
+
# Convert DataFrame to dict if necessary
|
|
512
|
+
if hasattr(data, 'to_dict'):
|
|
513
|
+
result = {
|
|
514
|
+
'snapshot_list': data.to_dict('records')
|
|
515
|
+
}
|
|
516
|
+
else:
|
|
517
|
+
result = {
|
|
518
|
+
'snapshot_list': data
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
return result
|
|
522
|
+
|
|
523
|
+
@mcp.tool()
|
|
524
|
+
async def get_cur_kline(symbol: str, ktype: str, count: int = 100) -> Dict[str, Any]:
|
|
525
|
+
"""Get current K-line data
|
|
526
|
+
|
|
527
|
+
Args:
|
|
528
|
+
symbol: Stock code, e.g. "HK.00700", "US.AAPL", "SH.600519"
|
|
529
|
+
Format: {market}.{code}
|
|
530
|
+
- HK: Hong Kong stocks
|
|
531
|
+
- US: US stocks
|
|
532
|
+
- SH: Shanghai stocks
|
|
533
|
+
- SZ: Shenzhen stocks
|
|
534
|
+
ktype: K-line type, options:
|
|
535
|
+
- "K_1M": 1 minute
|
|
536
|
+
- "K_5M": 5 minutes
|
|
537
|
+
- "K_15M": 15 minutes
|
|
538
|
+
- "K_30M": 30 minutes
|
|
539
|
+
- "K_60M": 60 minutes
|
|
540
|
+
- "K_DAY": Daily
|
|
541
|
+
- "K_WEEK": Weekly
|
|
542
|
+
- "K_MON": Monthly
|
|
543
|
+
- "K_QUARTER": Quarterly
|
|
544
|
+
- "K_YEAR": Yearly
|
|
545
|
+
count: Number of K-lines to return (default: 100)
|
|
546
|
+
Range: 1-1000
|
|
547
|
+
|
|
548
|
+
Returns:
|
|
549
|
+
Dict containing K-line data including:
|
|
550
|
+
- kline_list: List of K-line data entries, each containing:
|
|
551
|
+
- code: Stock code
|
|
552
|
+
- kline_type: K-line type
|
|
553
|
+
- update_time: Update time (YYYY-MM-DD HH:mm:ss)
|
|
554
|
+
- open_price: Opening price
|
|
555
|
+
- high_price: Highest price
|
|
556
|
+
- low_price: Lowest price
|
|
557
|
+
- close_price: Closing price
|
|
558
|
+
- volume: Trading volume
|
|
559
|
+
- turnover: Trading amount
|
|
560
|
+
- pe_ratio: Price-to-earnings ratio
|
|
561
|
+
- turnover_rate: Turnover rate
|
|
562
|
+
- timestamp: K-line time
|
|
563
|
+
- kline_status: K-line status (0: Normal)
|
|
564
|
+
|
|
565
|
+
Raises:
|
|
566
|
+
- INVALID_PARAM: Invalid parameter
|
|
567
|
+
- INVALID_CODE: Invalid stock code format
|
|
568
|
+
- INVALID_SUBTYPE: Invalid K-line type
|
|
569
|
+
- GET_CUR_KLINE_FAILED: Failed to get K-line data
|
|
570
|
+
|
|
571
|
+
Note:
|
|
572
|
+
- IMPORTANT: Must subscribe to the K-line data first using subscribe() with the corresponding K-line type
|
|
573
|
+
- K-line data contains latest market data
|
|
574
|
+
- Can request multiple stocks at once
|
|
575
|
+
- Different periods have different update frequencies
|
|
576
|
+
- Consider actual needs when selecting stocks and K-line types
|
|
577
|
+
- Handle exceptions properly
|
|
578
|
+
"""
|
|
579
|
+
ret, data = quote_ctx.get_cur_kline(
|
|
580
|
+
code=symbol,
|
|
581
|
+
ktype=ktype,
|
|
582
|
+
num=count
|
|
583
|
+
)
|
|
584
|
+
if ret != RET_OK:
|
|
585
|
+
return {'error': str(data)}
|
|
586
|
+
|
|
587
|
+
# Convert DataFrame to dict if necessary
|
|
588
|
+
if hasattr(data, 'to_dict'):
|
|
589
|
+
result = {
|
|
590
|
+
'kline_list': data.to_dict('records')
|
|
591
|
+
}
|
|
592
|
+
else:
|
|
593
|
+
result = {
|
|
594
|
+
'kline_list': data
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
return result
|
|
598
|
+
|
|
599
|
+
@mcp.tool()
|
|
600
|
+
async def get_history_kline(symbol: str, ktype: str, start: str, end: str, count: int = 100) -> Dict[str, Any]:
|
|
601
|
+
"""Get historical K-line data
|
|
602
|
+
|
|
603
|
+
Args:
|
|
604
|
+
symbol: Stock code, e.g. "HK.00700", "US.AAPL", "SH.600519"
|
|
605
|
+
Format: {market}.{code}
|
|
606
|
+
- HK: Hong Kong stocks
|
|
607
|
+
- US: US stocks
|
|
608
|
+
- SH: Shanghai stocks
|
|
609
|
+
- SZ: Shenzhen stocks
|
|
610
|
+
ktype: K-line type, options:
|
|
611
|
+
- "K_1M": 1 minute
|
|
612
|
+
- "K_3M": 3 minutes
|
|
613
|
+
- "K_5M": 5 minutes
|
|
614
|
+
- "K_15M": 15 minutes
|
|
615
|
+
- "K_30M": 30 minutes
|
|
616
|
+
- "K_60M": 60 minutes
|
|
617
|
+
- "K_DAY": Daily
|
|
618
|
+
- "K_WEEK": Weekly
|
|
619
|
+
- "K_MON": Monthly
|
|
620
|
+
start: Start date in format "YYYY-MM-DD"
|
|
621
|
+
end: End date in format "YYYY-MM-DD"
|
|
622
|
+
count: Number of K-lines to return (default: 100)
|
|
623
|
+
Range: 1-1000
|
|
624
|
+
|
|
625
|
+
Note:
|
|
626
|
+
- Limited to 30 stocks per 30 days
|
|
627
|
+
- Used quota will be automatically released after 30 days
|
|
628
|
+
- Different K-line types have different update frequencies
|
|
629
|
+
- Historical data availability varies by market and stock
|
|
630
|
+
|
|
631
|
+
Returns:
|
|
632
|
+
Dict containing K-line data including:
|
|
633
|
+
- code: Stock code
|
|
634
|
+
- kline_type: K-line type
|
|
635
|
+
- time_key: K-line time (YYYY-MM-DD HH:mm:ss)
|
|
636
|
+
- open: Opening price
|
|
637
|
+
- close: Closing price
|
|
638
|
+
- high: Highest price
|
|
639
|
+
- low: Lowest price
|
|
640
|
+
- volume: Trading volume
|
|
641
|
+
- turnover: Trading amount
|
|
642
|
+
- pe_ratio: Price-to-earnings ratio
|
|
643
|
+
- turnover_rate: Turnover rate
|
|
644
|
+
- change_rate: Price change rate
|
|
645
|
+
- last_close: Last closing price
|
|
646
|
+
|
|
647
|
+
Raises:
|
|
648
|
+
- INVALID_PARAM: Invalid parameter
|
|
649
|
+
- INVALID_CODE: Invalid stock code format
|
|
650
|
+
- INVALID_SUBTYPE: Invalid K-line type
|
|
651
|
+
- GET_HISTORY_KLINE_FAILED: Failed to get historical K-line data
|
|
652
|
+
"""
|
|
653
|
+
ret, data, page_req_key = quote_ctx.request_history_kline(
|
|
654
|
+
code=symbol,
|
|
655
|
+
start=start,
|
|
656
|
+
end=end,
|
|
657
|
+
ktype=ktype,
|
|
658
|
+
max_count=count
|
|
659
|
+
)
|
|
660
|
+
|
|
661
|
+
if ret != RET_OK:
|
|
662
|
+
return {'error': data}
|
|
663
|
+
|
|
664
|
+
result = data.to_dict()
|
|
665
|
+
|
|
666
|
+
# If there are more pages, continue fetching
|
|
667
|
+
while page_req_key is not None:
|
|
668
|
+
ret, data, page_req_key = quote_ctx.request_history_kline(
|
|
669
|
+
code=symbol,
|
|
670
|
+
start=start,
|
|
671
|
+
end=end,
|
|
672
|
+
ktype=ktype,
|
|
673
|
+
max_count=count,
|
|
674
|
+
page_req_key=page_req_key
|
|
675
|
+
)
|
|
676
|
+
if ret != RET_OK:
|
|
677
|
+
return {'error': data}
|
|
678
|
+
# Append new data to result
|
|
679
|
+
new_data = data.to_dict()
|
|
680
|
+
for key in result:
|
|
681
|
+
if isinstance(result[key], list):
|
|
682
|
+
result[key].extend(new_data[key])
|
|
683
|
+
|
|
684
|
+
return result
|
|
685
|
+
|
|
686
|
+
@mcp.tool()
|
|
687
|
+
async def get_rt_data(symbol: str) -> Dict[str, Any]:
|
|
688
|
+
"""Get real-time data
|
|
689
|
+
|
|
690
|
+
Args:
|
|
691
|
+
symbol: Stock code, e.g. "HK.00700", "US.AAPL", "SH.600519"
|
|
692
|
+
Format: {market}.{code}
|
|
693
|
+
- HK: Hong Kong stocks
|
|
694
|
+
- US: US stocks
|
|
695
|
+
- SH: Shanghai stocks
|
|
696
|
+
- SZ: Shenzhen stocks
|
|
697
|
+
|
|
698
|
+
Returns:
|
|
699
|
+
Dict containing real-time data including:
|
|
700
|
+
- rt_data_list: List of real-time data entries, each containing:
|
|
701
|
+
- code: Stock code
|
|
702
|
+
- time: Time (HH:mm:ss)
|
|
703
|
+
- price: Latest price
|
|
704
|
+
- volume: Trading volume
|
|
705
|
+
- turnover: Trading amount
|
|
706
|
+
- avg_price: Average price
|
|
707
|
+
- timestamp: Update time (YYYY-MM-DD HH:mm:ss)
|
|
708
|
+
- rt_data_status: Real-time data status (0: Normal)
|
|
709
|
+
|
|
710
|
+
Raises:
|
|
711
|
+
- INVALID_PARAM: Invalid parameter
|
|
712
|
+
- INVALID_CODE: Invalid stock code format
|
|
713
|
+
- GET_RT_DATA_FAILED: Failed to get real-time data
|
|
714
|
+
|
|
715
|
+
Note:
|
|
716
|
+
- IMPORTANT: Must subscribe to RT_DATA first using subscribe()
|
|
717
|
+
- Real-time data is updated frequently
|
|
718
|
+
- Contains latest data only, not historical data
|
|
719
|
+
- Update frequency varies by market and stock
|
|
720
|
+
- Consider using callbacks for real-time processing
|
|
721
|
+
"""
|
|
722
|
+
ret, data = quote_ctx.get_rt_data(symbol)
|
|
723
|
+
if ret != RET_OK:
|
|
724
|
+
return {'error': str(data)}
|
|
725
|
+
|
|
726
|
+
# Convert DataFrame to dict if necessary
|
|
727
|
+
if hasattr(data, 'to_dict'):
|
|
728
|
+
result = {
|
|
729
|
+
'rt_data_list': data.to_dict('records')
|
|
730
|
+
}
|
|
731
|
+
else:
|
|
732
|
+
result = {
|
|
733
|
+
'rt_data_list': data
|
|
734
|
+
}
|
|
735
|
+
|
|
736
|
+
return result
|
|
737
|
+
|
|
738
|
+
@mcp.tool()
|
|
739
|
+
async def get_ticker(symbol: str) -> Dict[str, Any]:
|
|
740
|
+
"""Get ticker data
|
|
741
|
+
|
|
742
|
+
Args:
|
|
743
|
+
symbol: Stock code, e.g. "HK.00700", "US.AAPL", "SH.600519"
|
|
744
|
+
Format: {market}.{code}
|
|
745
|
+
- HK: Hong Kong stocks
|
|
746
|
+
- US: US stocks
|
|
747
|
+
- SH: Shanghai stocks
|
|
748
|
+
- SZ: Shenzhen stocks
|
|
749
|
+
|
|
750
|
+
Returns:
|
|
751
|
+
Dict containing ticker data including:
|
|
752
|
+
- code: Stock code
|
|
753
|
+
- sequence: Sequence number
|
|
754
|
+
- price: Deal price
|
|
755
|
+
- volume: Deal volume
|
|
756
|
+
- turnover: Deal amount
|
|
757
|
+
- ticker_direction: Ticker direction
|
|
758
|
+
1: Bid order
|
|
759
|
+
2: Ask order
|
|
760
|
+
3: Neutral order
|
|
761
|
+
- ticker_type: Ticker type
|
|
762
|
+
1: Regular trade
|
|
763
|
+
2: Cancel trade
|
|
764
|
+
3: Trading at closing price
|
|
765
|
+
4: Off-exchange trade
|
|
766
|
+
5: After-hours trade
|
|
767
|
+
- timestamp: Deal time (YYYY-MM-DD HH:mm:ss)
|
|
768
|
+
- ticker_status: Ticker status (0: Normal)
|
|
769
|
+
|
|
770
|
+
Raises:
|
|
771
|
+
- INVALID_PARAM: Invalid parameter
|
|
772
|
+
- INVALID_CODE: Invalid stock code format
|
|
773
|
+
- GET_RT_TICKER_FAILED: Failed to get ticker data
|
|
774
|
+
|
|
775
|
+
Note:
|
|
776
|
+
- IMPORTANT: Must subscribe to TICKER first using subscribe()
|
|
777
|
+
- Ticker data is updated in real-time
|
|
778
|
+
- High update frequency, large data volume
|
|
779
|
+
- Update frequency varies by market and stock
|
|
780
|
+
- Consider using callbacks for real-time processing
|
|
781
|
+
"""
|
|
782
|
+
ret, data = quote_ctx.get_ticker(symbol)
|
|
783
|
+
return handle_return_data(ret, data)
|
|
784
|
+
|
|
785
|
+
@mcp.tool()
|
|
786
|
+
async def get_order_book(symbol: str) -> Dict[str, Any]:
|
|
787
|
+
"""Get order book data
|
|
788
|
+
|
|
789
|
+
Args:
|
|
790
|
+
symbol: Stock code, e.g. "HK.00700", "US.AAPL", "SH.600519"
|
|
791
|
+
Format: {market}.{code}
|
|
792
|
+
- HK: Hong Kong stocks
|
|
793
|
+
- US: US stocks
|
|
794
|
+
- SH: Shanghai stocks
|
|
795
|
+
- SZ: Shenzhen stocks
|
|
796
|
+
|
|
797
|
+
Returns:
|
|
798
|
+
Dict containing order book data including:
|
|
799
|
+
- code: Stock code
|
|
800
|
+
- update_time: Update time (YYYY-MM-DD HH:mm:ss)
|
|
801
|
+
- bid_price: List of bid prices (up to 10 levels)
|
|
802
|
+
- bid_volume: List of bid volumes (up to 10 levels)
|
|
803
|
+
- ask_price: List of ask prices (up to 10 levels)
|
|
804
|
+
- ask_volume: List of ask volumes (up to 10 levels)
|
|
805
|
+
|
|
806
|
+
Raises:
|
|
807
|
+
- INVALID_PARAM: Invalid parameter
|
|
808
|
+
- INVALID_CODE: Invalid stock code format
|
|
809
|
+
- GET_ORDER_BOOK_FAILED: Failed to get order book data
|
|
810
|
+
|
|
811
|
+
Note:
|
|
812
|
+
- IMPORTANT: Must subscribe to ORDER_BOOK first using subscribe()
|
|
813
|
+
- Order book data is updated in real-time
|
|
814
|
+
- Contains latest bid/ask information only
|
|
815
|
+
- Number of price levels may vary by market
|
|
816
|
+
- Update frequency varies by market and stock
|
|
817
|
+
"""
|
|
818
|
+
ret, data = quote_ctx.get_order_book(symbol)
|
|
819
|
+
return handle_return_data(ret, data)
|
|
820
|
+
|
|
821
|
+
@mcp.tool()
|
|
822
|
+
async def get_broker_queue(symbol: str) -> Dict[str, Any]:
|
|
823
|
+
"""Get broker queue data
|
|
824
|
+
|
|
825
|
+
Args:
|
|
826
|
+
symbol: Stock code, e.g. "HK.00700", "US.AAPL", "SH.600519"
|
|
827
|
+
Format: {market}.{code}
|
|
828
|
+
- HK: Hong Kong stocks
|
|
829
|
+
- US: US stocks
|
|
830
|
+
- SH: Shanghai stocks
|
|
831
|
+
- SZ: Shenzhen stocks
|
|
832
|
+
|
|
833
|
+
Returns:
|
|
834
|
+
Dict containing broker queue data including:
|
|
835
|
+
- code: Stock code
|
|
836
|
+
- update_time: Update time (YYYY-MM-DD HH:mm:ss)
|
|
837
|
+
- bid_broker_id: List of bid broker IDs
|
|
838
|
+
- bid_broker_name: List of bid broker names
|
|
839
|
+
- bid_broker_pos: List of bid broker positions
|
|
840
|
+
- ask_broker_id: List of ask broker IDs
|
|
841
|
+
- ask_broker_name: List of ask broker names
|
|
842
|
+
- ask_broker_pos: List of ask broker positions
|
|
843
|
+
- timestamp: Update timestamp
|
|
844
|
+
- broker_status: Broker queue status (0: Normal)
|
|
845
|
+
|
|
846
|
+
Raises:
|
|
847
|
+
- INVALID_PARAM: Invalid parameter
|
|
848
|
+
- INVALID_CODE: Invalid stock code format
|
|
849
|
+
- GET_BROKER_QUEUE_FAILED: Failed to get broker queue data
|
|
850
|
+
|
|
851
|
+
Note:
|
|
852
|
+
- IMPORTANT: Must subscribe to BROKER first using subscribe()
|
|
853
|
+
- Broker queue data is updated in real-time
|
|
854
|
+
- Shows broker information for both bid and ask sides
|
|
855
|
+
- Number of brokers may vary by market
|
|
856
|
+
- Update frequency varies by market and stock
|
|
857
|
+
- Mainly used for displaying broker trading activities
|
|
858
|
+
"""
|
|
859
|
+
ret, data = quote_ctx.get_broker_queue(symbol)
|
|
860
|
+
return handle_return_data(ret, data)
|
|
861
|
+
|
|
862
|
+
@mcp.tool()
|
|
863
|
+
async def subscribe(symbols: List[str], sub_types: List[str]) -> Dict[str, Any]:
|
|
864
|
+
"""Subscribe to real-time data
|
|
865
|
+
|
|
866
|
+
Args:
|
|
867
|
+
symbols: List of stock codes, e.g. ["HK.00700", "US.AAPL"]
|
|
868
|
+
Format: {market}.{code}
|
|
869
|
+
- HK: Hong Kong stocks
|
|
870
|
+
- US: US stocks
|
|
871
|
+
- SH: Shanghai stocks
|
|
872
|
+
- SZ: Shenzhen stocks
|
|
873
|
+
sub_types: List of subscription types, options:
|
|
874
|
+
- "QUOTE": Basic quote (price, volume, etc.)
|
|
875
|
+
- "ORDER_BOOK": Order book (bid/ask)
|
|
876
|
+
- "TICKER": Ticker (trades)
|
|
877
|
+
- "RT_DATA": Real-time data
|
|
878
|
+
- "BROKER": Broker queue
|
|
879
|
+
- "K_1M": 1-minute K-line
|
|
880
|
+
- "K_3M": 3-minute K-line
|
|
881
|
+
- "K_5M": 5-minute K-line
|
|
882
|
+
- "K_15M": 15-minute K-line
|
|
883
|
+
- "K_30M": 30-minute K-line
|
|
884
|
+
- "K_60M": 60-minute K-line
|
|
885
|
+
- "K_DAY": Daily K-line
|
|
886
|
+
- "K_WEEK": Weekly K-line
|
|
887
|
+
- "K_MON": Monthly K-line
|
|
888
|
+
- "K_QUARTER": Quarterly K-line
|
|
889
|
+
- "K_YEAR": Yearly K-line
|
|
890
|
+
|
|
891
|
+
Note:
|
|
892
|
+
- Maximum 100 symbols per request
|
|
893
|
+
- Maximum 5 subscription types per request
|
|
894
|
+
- Each socket can subscribe up to 500 symbols
|
|
895
|
+
- Data will be pushed through callbacks
|
|
896
|
+
- Consider unsubscribing when data is no longer needed
|
|
897
|
+
|
|
898
|
+
Returns:
|
|
899
|
+
Dict containing subscription result:
|
|
900
|
+
- status: "success" or error message
|
|
901
|
+
|
|
902
|
+
Raises:
|
|
903
|
+
- INVALID_PARAM: Invalid parameter
|
|
904
|
+
- INVALID_CODE: Invalid stock code format
|
|
905
|
+
- INVALID_SUBTYPE: Invalid subscription type
|
|
906
|
+
- SUBSCRIBE_FAILED: Failed to subscribe
|
|
907
|
+
"""
|
|
908
|
+
for symbol in symbols:
|
|
909
|
+
for sub_type in sub_types:
|
|
910
|
+
ret, data = quote_ctx.subscribe(symbol, sub_type)
|
|
911
|
+
if ret != RET_OK:
|
|
912
|
+
return {'error': data}
|
|
913
|
+
return {"status": "success"}
|
|
914
|
+
|
|
915
|
+
@mcp.tool()
|
|
916
|
+
async def unsubscribe(symbols: List[str], sub_types: List[str]) -> Dict[str, Any]:
|
|
917
|
+
"""Unsubscribe from real-time data
|
|
918
|
+
|
|
919
|
+
Args:
|
|
920
|
+
symbols: List of stock codes, e.g. ["HK.00700", "US.AAPL"]
|
|
921
|
+
Format: {market}.{code}
|
|
922
|
+
- HK: Hong Kong stocks
|
|
923
|
+
- US: US stocks
|
|
924
|
+
- SH: Shanghai stocks
|
|
925
|
+
- SZ: Shenzhen stocks
|
|
926
|
+
sub_types: List of subscription types, options:
|
|
927
|
+
- "QUOTE": Basic quote (price, volume, etc.)
|
|
928
|
+
- "ORDER_BOOK": Order book (bid/ask)
|
|
929
|
+
- "TICKER": Ticker (trades)
|
|
930
|
+
- "RT_DATA": Real-time data
|
|
931
|
+
- "BROKER": Broker queue
|
|
932
|
+
- "K_1M": 1-minute K-line
|
|
933
|
+
- "K_5M": 5-minute K-line
|
|
934
|
+
- "K_15M": 15-minute K-line
|
|
935
|
+
- "K_30M": 30-minute K-line
|
|
936
|
+
- "K_60M": 60-minute K-line
|
|
937
|
+
- "K_DAY": Daily K-line
|
|
938
|
+
- "K_WEEK": Weekly K-line
|
|
939
|
+
- "K_MON": Monthly K-line
|
|
940
|
+
|
|
941
|
+
Returns:
|
|
942
|
+
Dict containing unsubscription result:
|
|
943
|
+
- status: "success" or error message
|
|
944
|
+
|
|
945
|
+
Raises:
|
|
946
|
+
- INVALID_PARAM: Invalid parameter
|
|
947
|
+
- INVALID_CODE: Invalid stock code format
|
|
948
|
+
- INVALID_SUBTYPE: Invalid subscription type
|
|
949
|
+
- UNSUBSCRIBE_FAILED: Failed to unsubscribe
|
|
950
|
+
"""
|
|
951
|
+
for symbol in symbols:
|
|
952
|
+
for sub_type in sub_types:
|
|
953
|
+
ret, data = quote_ctx.unsubscribe(symbol, sub_type)
|
|
954
|
+
if ret != RET_OK:
|
|
955
|
+
return {'error': data}
|
|
956
|
+
return {"status": "success"}
|
|
957
|
+
|
|
958
|
+
# Derivatives Tools
|
|
959
|
+
@mcp.tool()
|
|
960
|
+
async def get_option_chain(symbol: str, start: str, end: str) -> Dict[str, Any]:
|
|
961
|
+
"""Get option chain data
|
|
962
|
+
|
|
963
|
+
Args:
|
|
964
|
+
symbol: Stock code, e.g. "HK.00700", "US.AAPL"
|
|
965
|
+
Format: {market}.{code}
|
|
966
|
+
- HK: Hong Kong stocks
|
|
967
|
+
- US: US stocks
|
|
968
|
+
start: Start date in format "YYYY-MM-DD"
|
|
969
|
+
end: End date in format "YYYY-MM-DD"
|
|
970
|
+
|
|
971
|
+
Returns:
|
|
972
|
+
Dict containing option chain data including:
|
|
973
|
+
- stock_code: Underlying stock code
|
|
974
|
+
- stock_name: Underlying stock name
|
|
975
|
+
- option_list: List of option contracts, each containing:
|
|
976
|
+
- option_code: Option code
|
|
977
|
+
- option_name: Option name
|
|
978
|
+
- option_type: Option type (CALL/PUT)
|
|
979
|
+
- strike_price: Strike price
|
|
980
|
+
- expiry_date: Expiry date
|
|
981
|
+
- last_price: Latest price
|
|
982
|
+
- volume: Trading volume
|
|
983
|
+
- open_interest: Open interest
|
|
984
|
+
- implied_volatility: Implied volatility
|
|
985
|
+
- delta: Delta value
|
|
986
|
+
- gamma: Gamma value
|
|
987
|
+
- theta: Theta value
|
|
988
|
+
- vega: Vega value
|
|
989
|
+
- update_time: Update time
|
|
990
|
+
|
|
991
|
+
Raises:
|
|
992
|
+
- INVALID_PARAM: Invalid parameter
|
|
993
|
+
- INVALID_MARKET: Invalid market code
|
|
994
|
+
- INVALID_STOCKCODE: Invalid stock code
|
|
995
|
+
- INVALID_EXPIRYDATE: Invalid expiry date
|
|
996
|
+
- GET_OPTION_CHAIN_FAILED: Failed to get option chain
|
|
997
|
+
|
|
998
|
+
Note:
|
|
999
|
+
- Option chain data is essential for options trading
|
|
1000
|
+
- Contains both call and put options
|
|
1001
|
+
- Includes Greeks for risk management
|
|
1002
|
+
- Data is updated during trading hours
|
|
1003
|
+
- Consider using with option expiration dates API
|
|
1004
|
+
"""
|
|
1005
|
+
ret, data = quote_ctx.get_option_chain(symbol, start, end)
|
|
1006
|
+
return data.to_dict() if ret == RET_OK else {'error': data}
|
|
1007
|
+
|
|
1008
|
+
@mcp.tool()
|
|
1009
|
+
async def get_option_expiration_date(symbol: str) -> Dict[str, Any]:
|
|
1010
|
+
"""Get option expiration dates
|
|
1011
|
+
|
|
1012
|
+
Args:
|
|
1013
|
+
symbol: Stock code, e.g. "HK.00700", "US.AAPL"
|
|
1014
|
+
Format: {market}.{code}
|
|
1015
|
+
- HK: Hong Kong stocks
|
|
1016
|
+
- US: US stocks
|
|
1017
|
+
|
|
1018
|
+
Returns:
|
|
1019
|
+
Dict containing expiration dates:
|
|
1020
|
+
- strike_time: List of expiration dates in format "YYYY-MM-DD"
|
|
1021
|
+
- option_expiry_info: Additional expiry information
|
|
1022
|
+
|
|
1023
|
+
Raises:
|
|
1024
|
+
- INVALID_PARAM: Invalid parameter
|
|
1025
|
+
- INVALID_MARKET: Invalid market code
|
|
1026
|
+
- INVALID_STOCKCODE: Invalid stock code
|
|
1027
|
+
- GET_OPTION_EXPIRATION_FAILED: Failed to get expiration dates
|
|
1028
|
+
|
|
1029
|
+
Note:
|
|
1030
|
+
- Use this API before querying option chain
|
|
1031
|
+
- Different stocks may have different expiry dates
|
|
1032
|
+
- Expiry dates are typically on monthly/weekly cycles
|
|
1033
|
+
- Not all stocks have listed options
|
|
1034
|
+
"""
|
|
1035
|
+
ret, data = quote_ctx.get_option_expiration_date(symbol)
|
|
1036
|
+
return data.to_dict() if ret == RET_OK else {'error': data}
|
|
1037
|
+
|
|
1038
|
+
@mcp.tool()
|
|
1039
|
+
async def get_option_condor(symbol: str, expiry: str, strike_price: float) -> Dict[str, Any]:
|
|
1040
|
+
"""Get option condor strategy data
|
|
1041
|
+
|
|
1042
|
+
Args:
|
|
1043
|
+
symbol: Stock code, e.g. "HK.00700", "US.AAPL"
|
|
1044
|
+
Format: {market}.{code}
|
|
1045
|
+
- HK: Hong Kong stocks
|
|
1046
|
+
- US: US stocks
|
|
1047
|
+
expiry: Option expiration date in format "YYYY-MM-DD"
|
|
1048
|
+
strike_price: Strike price of the option
|
|
1049
|
+
|
|
1050
|
+
Returns:
|
|
1051
|
+
Dict containing condor strategy data including:
|
|
1052
|
+
- strategy_name: Strategy name
|
|
1053
|
+
- option_list: List of options in the strategy
|
|
1054
|
+
- risk_metrics: Risk metrics for the strategy
|
|
1055
|
+
- profit_loss: Profit/loss analysis
|
|
1056
|
+
|
|
1057
|
+
Raises:
|
|
1058
|
+
- INVALID_PARAM: Invalid parameter
|
|
1059
|
+
- INVALID_MARKET: Invalid market code
|
|
1060
|
+
- INVALID_STOCKCODE: Invalid stock code
|
|
1061
|
+
- INVALID_EXPIRYDATE: Invalid expiry date
|
|
1062
|
+
- INVALID_STRIKEPRICE: Invalid strike price
|
|
1063
|
+
- GET_OPTION_CONDOR_FAILED: Failed to get condor data
|
|
1064
|
+
|
|
1065
|
+
Note:
|
|
1066
|
+
- Condor is a neutral options trading strategy
|
|
1067
|
+
- Involves four different strike prices
|
|
1068
|
+
- Limited risk and limited profit potential
|
|
1069
|
+
- Best used in low volatility environments
|
|
1070
|
+
"""
|
|
1071
|
+
ret, data = quote_ctx.get_option_condor(symbol, expiry, strike_price)
|
|
1072
|
+
return data.to_dict() if ret == RET_OK else {'error': data}
|
|
1073
|
+
|
|
1074
|
+
@mcp.tool()
|
|
1075
|
+
async def get_option_butterfly(symbol: str, expiry: str, strike_price: float) -> Dict[str, Any]:
|
|
1076
|
+
"""Get option butterfly strategy data
|
|
1077
|
+
|
|
1078
|
+
Args:
|
|
1079
|
+
symbol: Stock code, e.g. "HK.00700", "US.AAPL"
|
|
1080
|
+
Format: {market}.{code}
|
|
1081
|
+
- HK: Hong Kong stocks
|
|
1082
|
+
- US: US stocks
|
|
1083
|
+
expiry: Option expiration date in format "YYYY-MM-DD"
|
|
1084
|
+
strike_price: Strike price of the option
|
|
1085
|
+
|
|
1086
|
+
Returns:
|
|
1087
|
+
Dict containing butterfly strategy data including:
|
|
1088
|
+
- strategy_name: Strategy name
|
|
1089
|
+
- option_list: List of options in the strategy
|
|
1090
|
+
- risk_metrics: Risk metrics for the strategy
|
|
1091
|
+
- profit_loss: Profit/loss analysis
|
|
1092
|
+
|
|
1093
|
+
Raises:
|
|
1094
|
+
- INVALID_PARAM: Invalid parameter
|
|
1095
|
+
- INVALID_MARKET: Invalid market code
|
|
1096
|
+
- INVALID_STOCKCODE: Invalid stock code
|
|
1097
|
+
- INVALID_EXPIRYDATE: Invalid expiry date
|
|
1098
|
+
- INVALID_STRIKEPRICE: Invalid strike price
|
|
1099
|
+
- GET_OPTION_BUTTERFLY_FAILED: Failed to get butterfly data
|
|
1100
|
+
|
|
1101
|
+
Note:
|
|
1102
|
+
- Butterfly is a neutral options trading strategy
|
|
1103
|
+
- Involves three different strike prices
|
|
1104
|
+
- Limited risk and limited profit potential
|
|
1105
|
+
- Maximum profit at middle strike price
|
|
1106
|
+
- Best used when expecting low volatility
|
|
1107
|
+
"""
|
|
1108
|
+
ret, data = quote_ctx.get_option_butterfly(symbol, expiry, strike_price)
|
|
1109
|
+
return data.to_dict() if ret == RET_OK else {'error': data}
|
|
1110
|
+
|
|
1111
|
+
# Account Query Tools
|
|
1112
|
+
@mcp.tool()
|
|
1113
|
+
async def get_account_list() -> Dict[str, Any]:
|
|
1114
|
+
"""Get account list"""
|
|
1115
|
+
if not init_trade_connection():
|
|
1116
|
+
return {'error': 'Failed to initialize trade connection'}
|
|
1117
|
+
ret, data = trade_ctx.get_acc_list()
|
|
1118
|
+
return handle_return_data(ret, data)
|
|
1119
|
+
|
|
1120
|
+
@mcp.tool()
|
|
1121
|
+
async def get_funds() -> Dict[str, Any]:
|
|
1122
|
+
"""Get account funds information"""
|
|
1123
|
+
if not init_trade_connection():
|
|
1124
|
+
return {'error': 'Failed to initialize trade connection'}
|
|
1125
|
+
try:
|
|
1126
|
+
ret, data = trade_ctx.accinfo_query()
|
|
1127
|
+
if ret != RET_OK:
|
|
1128
|
+
return {'error': str(data)}
|
|
1129
|
+
|
|
1130
|
+
if data is None or data.empty:
|
|
1131
|
+
return {'error': 'No account information available'}
|
|
1132
|
+
|
|
1133
|
+
return handle_return_data(ret, data)
|
|
1134
|
+
except Exception as e:
|
|
1135
|
+
return {'error': f'Failed to get account funds: {str(e)}'}
|
|
1136
|
+
|
|
1137
|
+
@mcp.tool()
|
|
1138
|
+
async def get_positions() -> Dict[str, Any]:
|
|
1139
|
+
"""Get account positions"""
|
|
1140
|
+
if not init_trade_connection():
|
|
1141
|
+
return {'error': 'Failed to initialize trade connection'}
|
|
1142
|
+
ret, data = trade_ctx.position_list_query()
|
|
1143
|
+
return handle_return_data(ret, data)
|
|
1144
|
+
|
|
1145
|
+
@mcp.tool()
|
|
1146
|
+
async def get_max_power() -> Dict[str, Any]:
|
|
1147
|
+
"""Get maximum trading power for the account"""
|
|
1148
|
+
if not init_trade_connection():
|
|
1149
|
+
return {'error': 'Failed to initialize trade connection'}
|
|
1150
|
+
ret, data = trade_ctx.get_max_power()
|
|
1151
|
+
return handle_return_data(ret, data)
|
|
1152
|
+
|
|
1153
|
+
@mcp.tool()
|
|
1154
|
+
async def get_margin_ratio(symbol: str) -> Dict[str, Any]:
|
|
1155
|
+
"""Get margin ratio for a security"""
|
|
1156
|
+
if not init_trade_connection():
|
|
1157
|
+
return {'error': 'Failed to initialize trade connection'}
|
|
1158
|
+
ret, data = trade_ctx.get_margin_ratio(symbol)
|
|
1159
|
+
return handle_return_data(ret, data)
|
|
1160
|
+
|
|
1161
|
+
# Market Information Tools
|
|
1162
|
+
@mcp.tool()
|
|
1163
|
+
async def get_market_state(market: str) -> Dict[str, Any]:
|
|
1164
|
+
"""Get market state
|
|
1165
|
+
|
|
1166
|
+
Args:
|
|
1167
|
+
market: Market code, options:
|
|
1168
|
+
- "HK": Hong Kong market (includes pre-market, continuous trading, afternoon, closing auction)
|
|
1169
|
+
- "US": US market (includes pre-market, continuous trading, after-hours)
|
|
1170
|
+
- "SH": Shanghai market (includes pre-opening, morning, afternoon, closing auction)
|
|
1171
|
+
- "SZ": Shenzhen market (includes pre-opening, morning, afternoon, closing auction)
|
|
1172
|
+
|
|
1173
|
+
Returns:
|
|
1174
|
+
Dict containing market state information including:
|
|
1175
|
+
- market: Market code
|
|
1176
|
+
- market_state: Market state code
|
|
1177
|
+
- NONE: Market not available
|
|
1178
|
+
- AUCTION: Auction period
|
|
1179
|
+
- WAITING_OPEN: Waiting for market open
|
|
1180
|
+
- MORNING: Morning session
|
|
1181
|
+
- REST: Lunch break
|
|
1182
|
+
- AFTERNOON: Afternoon session
|
|
1183
|
+
- CLOSED: Market closed
|
|
1184
|
+
- PRE_MARKET_BEGIN: Pre-market begin
|
|
1185
|
+
- PRE_MARKET_END: Pre-market end
|
|
1186
|
+
- AFTER_HOURS_BEGIN: After-hours begin
|
|
1187
|
+
- AFTER_HOURS_END: After-hours end
|
|
1188
|
+
- market_state_desc: Description of market state
|
|
1189
|
+
- update_time: Update time (YYYY-MM-DD HH:mm:ss)
|
|
1190
|
+
|
|
1191
|
+
Raises:
|
|
1192
|
+
- INVALID_PARAM: Invalid parameter
|
|
1193
|
+
- INVALID_MARKET: Invalid market code
|
|
1194
|
+
- GET_MARKET_STATE_FAILED: Failed to get market state
|
|
1195
|
+
|
|
1196
|
+
Note:
|
|
1197
|
+
- Market state is updated in real-time
|
|
1198
|
+
- Different markets have different trading hours
|
|
1199
|
+
- Consider timezone differences
|
|
1200
|
+
- Market state affects trading operations
|
|
1201
|
+
- Recommended to check state before trading
|
|
1202
|
+
"""
|
|
1203
|
+
ret, data = quote_ctx.get_market_state(market)
|
|
1204
|
+
return data.to_dict() if ret == RET_OK else {'error': data}
|
|
1205
|
+
|
|
1206
|
+
@mcp.tool()
|
|
1207
|
+
async def get_security_info(market: str, code: str) -> Dict[str, Any]:
|
|
1208
|
+
"""Get security information
|
|
1209
|
+
|
|
1210
|
+
Args:
|
|
1211
|
+
market: Market code, options:
|
|
1212
|
+
- "HK": Hong Kong market
|
|
1213
|
+
- "US": US market
|
|
1214
|
+
- "SH": Shanghai market
|
|
1215
|
+
- "SZ": Shenzhen market
|
|
1216
|
+
code: Stock code without market prefix, e.g. "00700" for "HK.00700"
|
|
1217
|
+
|
|
1218
|
+
Returns:
|
|
1219
|
+
Dict containing security information including:
|
|
1220
|
+
- stock_code: Stock code
|
|
1221
|
+
- stock_name: Stock name
|
|
1222
|
+
- market: Market code
|
|
1223
|
+
- stock_type: Stock type (e.g., "STOCK", "ETF", "WARRANT")
|
|
1224
|
+
- stock_child_type: Stock subtype (e.g., "MAIN_BOARD", "GEM")
|
|
1225
|
+
- list_time: Listing date
|
|
1226
|
+
- delist_time: Delisting date (if applicable)
|
|
1227
|
+
- lot_size: Lot size
|
|
1228
|
+
- stock_owner: Company name
|
|
1229
|
+
- issue_price: IPO price
|
|
1230
|
+
- issue_size: IPO size
|
|
1231
|
+
- net_profit: Net profit
|
|
1232
|
+
- net_profit_growth: Net profit growth rate
|
|
1233
|
+
- revenue: Revenue
|
|
1234
|
+
- revenue_growth: Revenue growth rate
|
|
1235
|
+
- eps: Earnings per share
|
|
1236
|
+
- pe_ratio: Price-to-earnings ratio
|
|
1237
|
+
- pb_ratio: Price-to-book ratio
|
|
1238
|
+
- dividend_ratio: Dividend ratio
|
|
1239
|
+
- stock_derivatives: List of related derivatives
|
|
1240
|
+
|
|
1241
|
+
Raises:
|
|
1242
|
+
- INVALID_PARAM: Invalid parameter
|
|
1243
|
+
- INVALID_MARKET: Invalid market code
|
|
1244
|
+
- INVALID_STOCKCODE: Invalid stock code
|
|
1245
|
+
- GET_STOCK_BASICINFO_FAILED: Failed to get stock information
|
|
1246
|
+
|
|
1247
|
+
Note:
|
|
1248
|
+
- Contains static information about the security
|
|
1249
|
+
- Financial data may be delayed
|
|
1250
|
+
- Some fields may be empty for certain security types
|
|
1251
|
+
- Important for fundamental analysis
|
|
1252
|
+
"""
|
|
1253
|
+
ret, data = quote_ctx.get_security_info(market, code)
|
|
1254
|
+
return data.to_dict() if ret == RET_OK else {'error': data}
|
|
1255
|
+
|
|
1256
|
+
@mcp.tool()
|
|
1257
|
+
async def get_security_list(market: str) -> Dict[str, Any]:
|
|
1258
|
+
"""Get security list
|
|
1259
|
+
|
|
1260
|
+
Args:
|
|
1261
|
+
market: Market code, options:
|
|
1262
|
+
- "HK": Hong Kong market
|
|
1263
|
+
- "US": US market
|
|
1264
|
+
- "SH": Shanghai market
|
|
1265
|
+
- "SZ": Shenzhen market
|
|
1266
|
+
|
|
1267
|
+
Returns:
|
|
1268
|
+
Dict containing list of securities:
|
|
1269
|
+
- security_list: List of securities, each containing:
|
|
1270
|
+
- code: Security code
|
|
1271
|
+
- name: Security name
|
|
1272
|
+
- lot_size: Lot size
|
|
1273
|
+
- stock_type: Security type
|
|
1274
|
+
- list_time: Listing date
|
|
1275
|
+
- stock_id: Security ID
|
|
1276
|
+
- delisting: Whether delisted
|
|
1277
|
+
- main_contract: Whether it's the main contract (futures)
|
|
1278
|
+
- last_trade_time: Last trade time (futures/options)
|
|
1279
|
+
|
|
1280
|
+
Raises:
|
|
1281
|
+
- INVALID_PARAM: Invalid parameter
|
|
1282
|
+
- INVALID_MARKET: Invalid market code
|
|
1283
|
+
- GET_SECURITY_LIST_FAILED: Failed to get security list
|
|
1284
|
+
|
|
1285
|
+
Note:
|
|
1286
|
+
- Returns all securities in the specified market
|
|
1287
|
+
- Includes stocks, ETFs, warrants, etc.
|
|
1288
|
+
- Updated daily
|
|
1289
|
+
- Useful for market analysis and monitoring
|
|
1290
|
+
- Consider caching results for better performance
|
|
1291
|
+
"""
|
|
1292
|
+
ret, data = quote_ctx.get_security_list(market)
|
|
1293
|
+
return data.to_dict() if ret == RET_OK else {'error': data}
|
|
1294
|
+
|
|
1295
|
+
# Prompts
|
|
1296
|
+
@mcp.prompt()
|
|
1297
|
+
async def market_analysis(symbol: str) -> str:
|
|
1298
|
+
"""Create a market analysis prompt"""
|
|
1299
|
+
return f"Please analyze the market data for {symbol}"
|
|
1300
|
+
|
|
1301
|
+
@mcp.prompt()
|
|
1302
|
+
async def option_strategy(symbol: str, expiry: str) -> str:
|
|
1303
|
+
"""Create an option strategy analysis prompt"""
|
|
1304
|
+
return f"Please analyze option strategies for {symbol} expiring on {expiry}"
|
|
1305
|
+
|
|
1306
|
+
@mcp.tool()
|
|
1307
|
+
async def get_stock_filter(base_filters: List[Dict[str, Any]] = None,
|
|
1308
|
+
accumulate_filters: List[Dict[str, Any]] = None,
|
|
1309
|
+
financial_filters: List[Dict[str, Any]] = None,
|
|
1310
|
+
market: str = None,
|
|
1311
|
+
page: int = 1,
|
|
1312
|
+
page_size: int = 200) -> Dict[str, Any]:
|
|
1313
|
+
"""Get filtered stock list based on conditions
|
|
1314
|
+
|
|
1315
|
+
Args:
|
|
1316
|
+
base_filters: List of base filters with structure:
|
|
1317
|
+
{
|
|
1318
|
+
"field_name": int, # StockField enum value
|
|
1319
|
+
"filter_min": float, # Optional minimum value
|
|
1320
|
+
"filter_max": float, # Optional maximum value
|
|
1321
|
+
"is_no_filter": bool, # Optional, whether to skip filtering
|
|
1322
|
+
"sort_dir": int # Optional, sort direction (0: No sort, 1: Ascending, 2: Descending)
|
|
1323
|
+
}
|
|
1324
|
+
accumulate_filters: List of accumulate filters with structure:
|
|
1325
|
+
{
|
|
1326
|
+
"field_name": int, # AccumulateField enum value
|
|
1327
|
+
"filter_min": float,
|
|
1328
|
+
"filter_max": float,
|
|
1329
|
+
"is_no_filter": bool,
|
|
1330
|
+
"sort_dir": int, # 0: No sort, 1: Ascending, 2: Descending
|
|
1331
|
+
"days": int # Required, number of days to accumulate
|
|
1332
|
+
}
|
|
1333
|
+
financial_filters: List of financial filters with structure:
|
|
1334
|
+
{
|
|
1335
|
+
"field_name": int, # FinancialField enum value
|
|
1336
|
+
"filter_min": float,
|
|
1337
|
+
"filter_max": float,
|
|
1338
|
+
"is_no_filter": bool,
|
|
1339
|
+
"sort_dir": int, # 0: No sort, 1: Ascending, 2: Descending
|
|
1340
|
+
"quarter": int # Required, financial quarter
|
|
1341
|
+
}
|
|
1342
|
+
market: Market code, options:
|
|
1343
|
+
- "HK.Motherboard": Hong Kong Main Board
|
|
1344
|
+
- "HK.GEM": Hong Kong GEM
|
|
1345
|
+
- "HK.BK1911": H-Share Main Board
|
|
1346
|
+
- "HK.BK1912": H-Share GEM
|
|
1347
|
+
- "US.NYSE": NYSE
|
|
1348
|
+
- "US.AMEX": AMEX
|
|
1349
|
+
- "US.NASDAQ": NASDAQ
|
|
1350
|
+
- "SH.3000000": Shanghai Main Board
|
|
1351
|
+
- "SZ.3000001": Shenzhen Main Board
|
|
1352
|
+
- "SZ.3000004": Shenzhen ChiNext
|
|
1353
|
+
page: Page number, starting from 1 (default: 1)
|
|
1354
|
+
page_size: Number of results per page, max 200 (default: 200)
|
|
1355
|
+
"""
|
|
1356
|
+
# Create filter request
|
|
1357
|
+
req = {
|
|
1358
|
+
"begin": (page - 1) * page_size,
|
|
1359
|
+
"num": page_size
|
|
1360
|
+
}
|
|
1361
|
+
|
|
1362
|
+
# Add market filter if specified
|
|
1363
|
+
if market:
|
|
1364
|
+
req["plate"] = {"plate_code": market}
|
|
1365
|
+
|
|
1366
|
+
# Add base filters
|
|
1367
|
+
if base_filters:
|
|
1368
|
+
req["baseFilterList"] = []
|
|
1369
|
+
for f in base_filters:
|
|
1370
|
+
filter_item = {"fieldName": f["field_name"]}
|
|
1371
|
+
if "filter_min" in f:
|
|
1372
|
+
filter_item["filterMin"] = f["filter_min"]
|
|
1373
|
+
if "filter_max" in f:
|
|
1374
|
+
filter_item["filterMax"] = f["filter_max"]
|
|
1375
|
+
if "is_no_filter" in f:
|
|
1376
|
+
filter_item["isNoFilter"] = f["is_no_filter"]
|
|
1377
|
+
if "sort_dir" in f:
|
|
1378
|
+
filter_item["sortDir"] = f["sort_dir"]
|
|
1379
|
+
req["baseFilterList"].append(filter_item)
|
|
1380
|
+
|
|
1381
|
+
# Add accumulate filters
|
|
1382
|
+
if accumulate_filters:
|
|
1383
|
+
req["accumulateFilterList"] = []
|
|
1384
|
+
for f in accumulate_filters:
|
|
1385
|
+
filter_item = {
|
|
1386
|
+
"fieldName": f["field_name"],
|
|
1387
|
+
"days": f["days"]
|
|
1388
|
+
}
|
|
1389
|
+
if "filter_min" in f:
|
|
1390
|
+
filter_item["filterMin"] = f["filter_min"]
|
|
1391
|
+
if "filter_max" in f:
|
|
1392
|
+
filter_item["filterMax"] = f["filter_max"]
|
|
1393
|
+
if "is_no_filter" in f:
|
|
1394
|
+
filter_item["isNoFilter"] = f["is_no_filter"]
|
|
1395
|
+
if "sort_dir" in f:
|
|
1396
|
+
filter_item["sortDir"] = f["sort_dir"]
|
|
1397
|
+
req["accumulateFilterList"].append(filter_item)
|
|
1398
|
+
|
|
1399
|
+
# Add financial filters
|
|
1400
|
+
if financial_filters:
|
|
1401
|
+
req["financialFilterList"] = []
|
|
1402
|
+
for f in financial_filters:
|
|
1403
|
+
filter_item = {
|
|
1404
|
+
"fieldName": f["field_name"],
|
|
1405
|
+
"quarter": f["quarter"]
|
|
1406
|
+
}
|
|
1407
|
+
if "filter_min" in f:
|
|
1408
|
+
filter_item["filterMin"] = f["filter_min"]
|
|
1409
|
+
if "filter_max" in f:
|
|
1410
|
+
filter_item["filterMax"] = f["filter_max"]
|
|
1411
|
+
if "is_no_filter" in f:
|
|
1412
|
+
filter_item["isNoFilter"] = f["is_no_filter"]
|
|
1413
|
+
if "sort_dir" in f:
|
|
1414
|
+
filter_item["sortDir"] = f["sort_dir"]
|
|
1415
|
+
req["financialFilterList"].append(filter_item)
|
|
1416
|
+
|
|
1417
|
+
ret, data = quote_ctx.get_stock_filter(req)
|
|
1418
|
+
return data.to_dict() if ret == RET_OK else {'error': data}
|
|
1419
|
+
|
|
1420
|
+
@mcp.tool()
|
|
1421
|
+
async def get_current_time() -> Dict[str, Any]:
|
|
1422
|
+
"""Get current time information
|
|
1423
|
+
|
|
1424
|
+
Returns:
|
|
1425
|
+
Dict containing time information including:
|
|
1426
|
+
- timestamp: Unix timestamp in seconds
|
|
1427
|
+
- datetime: Formatted datetime string (YYYY-MM-DD HH:mm:ss)
|
|
1428
|
+
- date: Date string (YYYY-MM-DD)
|
|
1429
|
+
- time: Time string (HH:mm:ss)
|
|
1430
|
+
- timezone: Local timezone name
|
|
1431
|
+
"""
|
|
1432
|
+
now = datetime.now()
|
|
1433
|
+
return {
|
|
1434
|
+
'timestamp': int(now.timestamp()),
|
|
1435
|
+
'datetime': now.strftime('%Y-%m-%d %H:%M:%S'),
|
|
1436
|
+
'date': now.strftime('%Y-%m-%d'),
|
|
1437
|
+
'time': now.strftime('%H:%M:%S'),
|
|
1438
|
+
'timezone': datetime.now().astimezone().tzname()
|
|
1439
|
+
}
|
|
1440
|
+
|
|
1441
|
+
if __name__ == "__main__":
|
|
1442
|
+
try:
|
|
1443
|
+
# 清理旧的进程和文件
|
|
1444
|
+
cleanup_stale_processes()
|
|
1445
|
+
|
|
1446
|
+
# 获取锁
|
|
1447
|
+
lock_fd = acquire_lock()
|
|
1448
|
+
if lock_fd is None:
|
|
1449
|
+
logger.error("Failed to acquire lock. Another instance may be running.")
|
|
1450
|
+
sys.exit(1)
|
|
1451
|
+
|
|
1452
|
+
# 设置信号处理
|
|
1453
|
+
signal.signal(signal.SIGINT, signal_handler)
|
|
1454
|
+
signal.signal(signal.SIGTERM, signal_handler)
|
|
1455
|
+
|
|
1456
|
+
logger.info("Initializing Futu connection...")
|
|
1457
|
+
if init_futu_connection():
|
|
1458
|
+
logger.info("Successfully initialized Futu connection")
|
|
1459
|
+
logger.info("Starting MCP server in stdio mode...")
|
|
1460
|
+
try:
|
|
1461
|
+
mcp.run(transport='stdio')
|
|
1462
|
+
except KeyboardInterrupt:
|
|
1463
|
+
logger.info("Received keyboard interrupt, shutting down...")
|
|
1464
|
+
sys.exit(0)
|
|
1465
|
+
except Exception as e:
|
|
1466
|
+
logger.error(f"Error running server: {str(e)}")
|
|
1467
|
+
sys.exit(1)
|
|
1468
|
+
else:
|
|
1469
|
+
logger.error("Failed to initialize Futu connection. Server will not start.")
|
|
1470
|
+
sys.exit(1)
|
|
1471
|
+
except Exception as e:
|
|
1472
|
+
logger.error(f"Error starting server: {str(e)}")
|
|
1473
|
+
sys.exit(1)
|
|
1474
|
+
finally:
|
|
1475
|
+
cleanup_all()
|