实时监控MT5和BITMEX
import MetaTrader5 as mt5//用来连接MT5并与其交互
import time//用于时间延迟
from datetime import datetime# 用于处理日期和时间
import requests# 用于发送HTTP请求(BitMEX API)
import json# 用于处理JSON数据(BitMEX API)
from multiprocessing import Process, Queue, Event# 用于多进程编程
import os # 用于清空控制台
import csv # 用于CSV文件操作
import copy # 用于深拷贝current_data
# 配置MT5平台
PLATFORMS = {
"IC": {
"path": r"C:\Program Files\MetaTrader 5\terminal64.exe",
"symbols": ["BTCUSD", "DE40", "US30", "USTEC", "US500"]
},
"STARTRADE": {
"path": r"C:\Program Files\VantageFX Trader 5\terminal64.exe",
"symbols": ["BTCUSD", "GER40+", "DJ30+", "NAS100+", "SP500+"]
},
"vantage": {
"path": r"C:\Users\tls\Desktop\MetaTrader 5\terminal64.exe",
"symbols": ["BTCUSD", "GER40", "DJ30", "NAS100", "SP500"]
}
}
# 配置BitMEX平台
BITMEX_BASE_URL = "https://www.bitmex.com/api/v1"
BITMEX_SYMBOL = "XBTUSD" # BitMEX的比特币品种
# 标准化交易品种 ,用于表格显示顺序和CSV文件名
STANDARD_SYMBOLS = ["BTCUSD", "GER40", "DJ30", "NAS100", "SP500"]
# 显示的平台顺序
DISPLAY_PLATFORMS = ["IC", "vantage", "STARTRADE", "BitMEX"]
# 平台特定符号到标准化符号的映射
PLATFORM_SYMBOL_MAP = {
"IC": {
"BTCUSD": "BTCUSD",
"DE40": "GER40",
"US30": "DJ30",
"USTEC": "NAS100",
"US500": "SP500"
},
"STARTRADE": {
"BTCUSD": "BTCUSD",
"GER40+": "GER40",
"DJ30+": "DJ30",
"NAS100+": "NAS100",
"SP500+": "SP500"
},
"vantage": {
"BTCUSD": "BTCUSD",
"GER40": "GER40",
"DJ30": "DJ30",
"NAS100": "NAS100",
"SP500": "SP500"
}
}
# 辅助函数(移到类外部,以便多进程可以正确地序列化和调用)
def get_symbol_status(mt5_instance, symbol):
"""检查指定交易品种的状态"""
symbol_info = mt5_instance.symbol_info(symbol)
if symbol_info is None:
return f"找不到品种: {symbol}", False
try:
trade_mode = getattr(symbol_info, 'trade_mode', None)
if trade_mode == 0:
return f"{symbol} 当前不允许交易(停盘)", False
trade_flags = getattr(symbol_info, 'trade_flags', None)
if trade_flags == 0:
return f"{symbol} 当前不允许交易(停盘)", False
trade_allowed = getattr(symbol_info, 'trade_allowed', True)
if not trade_allowed:
return f"{symbol} 当前不允许交易(停盘)", False
return f"{symbol} 可交易", True
except Exception as e:
return f"检查 {symbol} 交易状态出错: {e}", False
def get_symbol_realtime_data(mt5_instance, symbol, platform_name):
"""获取指定交易品种的实时数据"""
tick = mt5_instance.symbol_info_tick(symbol)
if tick is None:
return (f"获取 {symbol} 行情失败,错误码: {mt5_instance.last_error()}",
None)
current_time = datetime.now()
try:
if hasattr(tick, 'time_msc'):
tick_time = datetime.fromtimestamp(tick.time_msc / 1000.0)
else:
tick_time = datetime.fromtimestamp(tick.time)
except Exception as e:
tick_time = current_time
# 仅当数据在10秒内更新时才认为是新鲜数据
if (current_time - tick_time).total_seconds() > 10:
return (f"{symbol} 数据可能过时(最后更新时间: {tick_time})",
None)
return (f"{symbol}: Bid: {tick.bid:.5f}, Ask: {tick.ask:.5f}, Spread: {(tick.ask - tick.bid):.5f}",
{
'platform': platform_name,
'original_symbol': symbol, # 存储原始符号名称
'bid': tick.bid,
'ask': tick.ask,
'time': tick_time # 原始tick时间
})
def monitor_single_mt5_platform(platform_name, config, data_queue, stop_event):
"""
在独立进程中初始化并监控单个MT5平台。
每个进程都会有自己独立的mt5实例。
"""
import MetaTrader5 as mt5 # 在子进程中重新导入,确保独立实例
if not mt5.initialize(path=config["path"]):
print(f"{platform_name} 初始化失败,错误码: {mt5.last_error()}")
return
terminal_info = mt5.terminal_info()
if not terminal_info.connected:
print(f"{platform_name} 未连接到交易服务器,正在尝试重新连接...")
mt5.shutdown()
if not mt5.initialize(path=config["path"]):
print(f"{platform_name} 重新连接失败,错误码: {mt5.last_error()}")
return
print(f"{platform_name} 连接成功!")
for symbol in config["symbols"]:
if not mt5.symbol_select(symbol, True):
print(f"{platform_name} 添加 {symbol} 到市场观察失败,错误码: {mt5.last_error()}")
else:
print(f"{platform_name} 已添加 {symbol} 到市场观察")
while not stop_event.is_set(): # 检查停止事件
for symbol in config["symbols"]:
data_str, data_dict = get_symbol_realtime_data(mt5, symbol, platform_name)
if data_dict:
data_queue.put(data_dict) # 将数据放入共享队列
time.sleep(2) # 每2秒轮询一次
mt5.shutdown()
print(f"{platform_name} MT5连接已关闭")
def bitmex_monitor_process(data_queue, stop_event):
"""BitMEX监控函数,在独立进程中运行"""
print("BitMEX 监测进程启动...")
while not stop_event.is_set():
try:
url = f'{BITMEX_BASE_URL}/instrument'
params = {
'symbol': BITMEX_SYMBOL,
'columns': 'symbol,lastPrice,bidPrice,askPrice,timestamp'
}
response = requests.get(url, params=params, timeout=5)
response.raise_for_status()
data = response.json()
if data and len(data) > 0:
ticker = data[0]
data_queue.put({
"platform": "BitMEX",
"original_symbol": BITMEX_SYMBOL, # 存储原始符号名称 (XBTUSD)
"bid": ticker.get('bidPrice'),
"ask": ticker.get('askPrice'),
"time": datetime.now() # BitMEX数据使用当前处理时间作为时间戳
})
else:
print("BitMEX API 返回空数据。")
except requests.exceptions.RequestException as e:
print(f"BitMEX 网络请求错误: {e}")
except json.JSONDecodeError as e:
print(f"BitMEX JSON解析错误: {e}")
except Exception as e:
print(f"BitMEX 监测线程发生错误: {e}")
time.sleep(1) # BitMEX每秒轮询一次
def clear_console():
"""清空控制台屏幕"""
os.system('cls' if os.name == 'nt' else 'clear')
# 定义列宽常量
TIME_COL_WIDTH = 20 # "YYYY-MM-DD HH:MM:SS"
SYMBOL_COL_WIDTH = 10 # "BTCUSD"
DATA_PAIR_WIDTH = 25 # "bid.XXXX ask.XXXX" (e.g., "105607.00 105607.10")
def print_table(current_data):
"""
打印格式化的表格数据。
current_data 结构:
{
"STANDARD_SYMBOL": {
"PLATFORM_NAME": {"bid": value, "ask": value},
...
},
...
}
"""
clear_console()
current_time_str = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
# 构建表头
header_parts = [
f"{'时间':<{TIME_COL_WIDTH}}",
f"{'品种':<{SYMBOL_COL_WIDTH}}"
]
for p in DISPLAY_PLATFORMS:
header_parts.append(f"{p:<{DATA_PAIR_WIDTH}}")
header_line = " | ".join(header_parts)
print(header_line)
print("-" * len(header_line))
# 打印每行数据
for std_symbol in STANDARD_SYMBOLS:
row_parts = [
f"{current_time_str:<{TIME_COL_WIDTH}}",
f"{std_symbol:<{SYMBOL_COL_WIDTH}}"
]
for platform in DISPLAY_PLATFORMS:
bid = current_data[std_symbol][platform]["bid"]
ask = current_data[std_symbol][platform]["ask"]
if bid is not None and ask is not None:
# 格式化输出 bid 和 ask,用空格分隔
if std_symbol == "BTCUSD": # BTCUSD可能需要更多小数位
data_str = f"{bid:.2f} {ask:.2f}"
else: # 其他品种可能小数位较少
data_str = f"{bid:.4f} {ask:.4f}"
row_parts.append(f"{data_str:<{DATA_PAIR_WIDTH}}")
else:
row_parts.append(f"{'None None':<{DATA_PAIR_WIDTH}}") # 如果没有数据,显示None None
print(" | ".join(row_parts))
# 全局变量用于CSV文件和写入器
csv_files = {}
csv_writers = {}
# 全局变量用于存储每秒的快照,等待批量写入CSV
snapshot_buffer = []
def initialize_csv_files():
"""初始化CSV文件和写入器,如果文件是新创建的则写入表头。"""
for std_symbol in STANDARD_SYMBOLS:
filename = f"{std_symbol}.csv"
# 检查文件是否存在且不为空,以避免重复写入表头
file_exists = os.path.exists(filename) and os.path.getsize(filename) > 0
# 'a' 模式表示追加写入,newline='' 防止空行
file = open(filename, 'a', newline='', encoding='utf-8')
writer = csv.writer(file)
if not file_exists:
# 写入CSV文件的表头,与控制台输出格式一致
header = ["Timestamp", "Symbol"]
for platform in DISPLAY_PLATFORMS:
header.append(f"{platform}_Bid")
header.append(f"{platform}_Ask")
writer.writerow(header)
csv_files[std_symbol] = file
csv_writers[std_symbol] = writer
def save_buffered_snapshots_to_csv():
"""
将所有累积的快照数据从缓冲区保存到各自的CSV文件中。
然后清空缓冲区。
"""
global snapshot_buffer # 声明使用全局变量
for snapshot_record in snapshot_buffer:
timestamp_str = snapshot_record['timestamp']
data_snapshot = snapshot_record['data']
for std_symbol in STANDARD_SYMBOLS:
row_data = [timestamp_str, std_symbol] # 时间戳和品种名称
for platform in DISPLAY_PLATFORMS:
bid = data_snapshot[std_symbol][platform]["bid"]
ask = data_snapshot[std_symbol][platform]["ask"]
# 格式化 bid/ask 用于CSV。如果为None,则写入空字符串。
if bid is not None and ask is not None:
if std_symbol == "BTCUSD": # BTCUSD可能需要更多小数位
row_data.append(f"{bid:.2f}")
row_data.append(f"{ask:.2f}")
else: # 其他品种可能小数位较少
row_data.append(f"{bid:.4f}")
row_data.append(f"{ask:.4f}")
else:
row_data.append("") # Bid
row_data.append("") # Ask
csv_writers[std_symbol].writerow(row_data)
csv_files[std_symbol].flush() # 确保数据立即写入磁盘
snapshot_buffer.clear() # 保存后清空缓冲区
def close_csv_files():
"""关闭所有打开的CSV文件。"""
for file in csv_files.values():
file.close()
if __name__ == "__main__":
# 创建共享队列和停止事件
mt5_data_queue = Queue()
bitmex_data_queue = Queue()
stop_event = Event()
processes = []
# 初始化 current_data 结构,用于控制台显示最新数据
current_data = {}
for std_symbol in STANDARD_SYMBOLS:
current_data[std_symbol] = {}
for platform in DISPLAY_PLATFORMS:
current_data[std_symbol][platform] = {"bid": None, "ask": None}
# 初始化CSV文件
initialize_csv_files()
# 启动MT5监控进程
for platform_name, config in PLATFORMS.items():
p = Process(target=monitor_single_mt5_platform, args=(platform_name, config, mt5_data_queue, stop_event))
p.daemon = True # 设置为守护进程,主进程退出时子进程也会退出
processes.append(p)
p.start()
# 启动BitMEX监控进程
bitmex_p = Process(target=bitmex_monitor_process, args=(bitmex_data_queue, stop_event))
bitmex_p.daemon = True
processes.append(bitmex_p)
bitmex_p.start()
print("所有监控进程已启动。按 Ctrl+C 停止监控。")
last_save_time = time.time() # 记录上次保存数据的时间
try:
while True:
# 处理MT5数据队列
while not mt5_data_queue.empty():
data = mt5_data_queue.get()
platform = data["platform"]
original_symbol = data["original_symbol"]
bid = data["bid"]
ask = data["ask"]
# 查找对应的标准化符号
if platform in PLATFORM_SYMBOL_MAP and original_symbol in PLATFORM_SYMBOL_MAP[platform]:
std_symbol = PLATFORM_SYMBOL_MAP[platform][original_symbol]
current_data[std_symbol][platform]["bid"] = bid # 更新用于控制台显示
current_data[std_symbol][platform]["ask"] = ask # 更新用于控制台显示
# 处理BitMEX数据队列
while not bitmex_data_queue.empty():
data = bitmex_data_queue.get()
platform = data["platform"] # 应该是 "BitMEX"
original_symbol = data["original_symbol"] # 应该是 "XBTUSD"
bid = data["bid"]
ask = data["ask"]
if original_symbol == BITMEX_SYMBOL: # 只有XBTUSD对应BTCUSD
current_data["BTCUSD"][platform]["bid"] = bid
current_data["BTCUSD"][platform]["ask"] = ask
# BitMEX的其他品种(GER40, DJ30, NAS100, SP500)保持为None,不产生数据点
# 打印更新后的表格
print_table(current_data)
# 每秒捕获一次快照并添加到缓冲区
current_time_for_snapshot = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
snapshot_buffer.append({
'timestamp': current_time_for_snapshot,
'data': copy.deepcopy(current_data) # 深拷贝当前数据,防止后续修改影响已保存的快照
})
# 每10秒保存一次数据
if time.time() - last_save_time >= 10:
save_buffered_snapshots_to_csv() # 保存所有缓冲快照
last_save_time = time.time()
time.sleep(1) # 每秒更新一次显示和捕获快照
except KeyboardInterrupt:
print("\n接收到 Ctrl+C,正在停止所有监控进程...")
stop_event.set() # 设置停止事件,通知所有子进程停止
# 在退出前,保存所有剩余的缓冲快照
save_buffered_snapshots_to_csv()
for p in processes:
p.join(timeout=5) # 等待子进程结束,设置超时时间
finally:
close_csv_files() # 确保在程序退出时关闭所有CSV文件
print("所有监控进程已停止。程序退出。")