python

实时监控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("所有监控进程已停止。程序退出。")

你可能感兴趣的:(python,数据库,开发语言)