Python与MT5通讯采用socket方式来实现。我是第一次涉及到这方面的编程,完全不具备最基本的常识,通过查阅大量的资料,充实了基础知识,又一次武装了自己的大脑~~
文中两个实验程序的代码都有非常详细的注释,记录了在学习过程中查询到的每条指令的功能和用法。本文作为日后开发借鉴资料,同时也给各位程序员提供参考。
socket通讯采用TCPIP协议,需要两个参数,一个是服务器地址,一个是通讯端口。
通讯端口 port 的范围是 0-65535,其中 0-1023 是预留给系统和一些常用服务的,例如 80 是 HTTP 服务的端口,25 是 SMTP 服务的端口。1024-49151 是注册端口,可以被 IANA 分配给一些服务,但不建议用户自定义使用。49152-65535 是动态端口,可以被用户自定义使用。如果不指定端口,系统会自动分配一个可用的动态端口。
根据 Python 的 socket 文档,如果想使用系统自动分配的动态端口,可以在创建 socket 对象后,调用 bind 方法时传入 0 作为端口参数。例如:
self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
self.sock.bind((self.address, 0))
这样,系统会选择一个可用的动态端口,并绑定到 socket 对象上。如果想获取这个端口,可以调用 getsockname 方法,它会返回一个元组,包含地址和端口。例如:
self.address, self.port = self.sock.getsockname()
客户端要想获取这个端口,就需要服务器提供一种方式来通知客户端,例如通过广播或者其他服务。或者,客户端可以先连接到一个固定的端口,然后服务器再告诉客户端动态端口的信息。
以下实验的两个程序需要传输中英文混写字符串,要注意编码问题:
● py中接受发送命令中要指定“gb2312”而不是“utf-8”。发送字符串时,如果字符串是中英文混写,有 n 个中文字,要在字符串后面追加 n*2 个空格;如果第一个是中文字符,还需要在前面添加一个空格。全英文字符则不要做以上处理。
● mq5中接受信息命令要指定“CP_ACP”。
查看计算机当前支持的编码:打开PowerShell,输入 chcp
936 = GB2312 简体中文,我的电脑是这个配置。
65001 = UTF-8 Unicode 编程常用。
socketserver_test.py(服务器)
"""
描述:本程序是socket服务器,与MT5连接,交换数据
作者:老易 QQ:921795 手机微信:13974863868
版本:1.0
日期:2023/6/2
"""
import socket # 导入 socket 通讯模块
# 预定义服务器地址(本地)和端口
server_ip = "localhost" # 本地 IP 地址,也可以用 172.0.0.1
server_port = 56765 # 端口号
class SocketServer:
"""
socket服务器类
"""
def __init__(self, address='', port=0):
"""
构造方法,创建一个socket,初始化对象属性,例如地址、端口
:param address:
:param port:
"""
self.addr = None # 一个元组,表示客户端的地址,包括 IP 地址和端口号,例如 (‘127.0.0.1’, 12345)
self.conn = None # 一个 socket 对象,表示与客户端建立的连接,可以用来接收和发送数据
self.cummdata = '' # 一个字符串,表示累积接收到的客户端发送的数据,每次调用 recvmsg 方法时会清空并重新赋值
self.address = address # 服务器地址
self.port = port # 服务器端口
# 创建一个 socket,IPv4 = AF_INET,SOCK_STREAM = 使用 TCP 传输控制协议
self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
# 绑定 socket 对象到一个地址(host, port)
self.sock.bind((self.address, self.port))
def recvmsg(self):
"""
接收发送消息
:return:接收到的消息
"""
# 开始监听 TCP 连接请求
self.sock.listen(1)
"""
接受 TCP 客户端的连接,并返回一个新的 socket 对象和客户端的地址,分别赋值给 self.conn 和 self.addr。
这样,服务器就可以通过 self.conn 来与客户端进行通信,而 self.addr 可以用来显示客户端的信息。
"""
self.conn, self.addr = self.sock.accept()
print('连接到:', self.addr, self.conn)
self.cummdata = ''
while True:
# 接收TCP数据,返回一个字符串
data = self.conn.recv(10000) # 最大数据量是10000字节
# print(type(data), data)
# 将接收到的数据解码为 gb2312 而不是 utf-8 编码的字符串,并追加到 self.cummdata 变量中
self.cummdata += data.decode("gb2312")
# 判断 data 变量是否为空,如果为空,就跳出 while 循环,结束接收数据的过程
if not data:
break
# 发送TCP数据,返回发送的字节数
# 如果字符串是中英文混写,有 n 个中文字,要在字符串后面追加 n*2 个空格;如果第一个是中文字符,还需要在前面添加一个空格。
# 全英文字符则不要做以上处理。
sendstr = " 老易 is 老易 "
# bytes函数是Python的一个内置函数,可以对不同类型的数据进行字节化。encode函数只能对字符串进行编码,但是使用encode函数更常见和直观。
self.conn.send(sendstr.encode("utf-8"))
# self.conn.send(bytes(sendstr, "utf-8"))
print('这是从python发送出的信息', sendstr, len(sendstr))
# print('这是从mt5接收到的信息', self.cummdata)
return self.cummdata
def __del__(self):
"""
析构方法,用于在 SocketServer 类的实例对象被销毁时执行一些清理操作,例如关闭 socket 对象
:return:
"""
self.sock.close()
# 创建一个 SocketServer 类的实例对象,命名为 myserver,传入服务器地址和端口作为参数,初始化对象的属性和 socket 对象
myserver = SocketServer(server_ip, server_port)
# 开始一个无限循环,用于不断地接收客户端的连接请求和数据
while True:
"""
调用 myserver 对象的 recvmsg 方法,接收客户端发送的数据,并将其赋值给 msg 变量。
同时,向客户端发送一个字符串 ‘from python’ 作为回应
"""
msg = myserver.recvmsg()
if not msg:
print('没有收到信息')
else:
print('接收来自MT5的信息:', msg)
socket.recv 方法的参数表示要接收的最大数据量,单位是字节。这个参数的值可以根据实际情况进行调整,但是有一些因素需要考虑,例如:
● 网络层的数据包大小,例如以太网的数据包大小约为 1500 字节,TCP 的数据包大小约为 64K 字节。如果接收缓冲区的大小小于数据包的大小,可能会导致数据分片或丢失。
● 系统调用的开销,例如每次调用 socket.recv 都会涉及一些内核操作,如果接收缓冲区的大小过小,可能会导致频繁的系统调用,影响性能。
● 应用层的协议设计,例如如果发送方和接收方有约定好的消息格式和长度,那么接收缓冲区的大小应该与之匹配,以便正确地解析消息。
一般来说,接收缓冲区的大小应该选择一个相对较小的 2 的幂次方,例如 40963。这样可以与硬件和网络的实际情况相匹配,也可以避免出现小尺寸的剩余数据。如果想要接收更大量的数据,可以多次调用 socket.recv 方法,直到读取完整个消息或者遇到错误为止。
程序运行后,控制台显示如下:
socketclient_test.mq5(客户端MT5)
/*
描述:本程序是socket服务器,与MT5连接,交换数据
作者:老易 QQ:921795 手机微信:13974863868
版本:1.0
日期:2023/6/2
*/
int socket_handle; //socket句柄
// 预定义服务器地址(本地)和端口
string server_ip = "localhost"; // 本地 IP 地址,也可以用 172.0.0.1
int server_port = 56765; // 端口号
uint server_timeout = 1000; // 延时,毫秒
string str_send = ""; // 发送字符
string str_receive = ""; // 接收字符
int OnInit()
{
//--- 创建一个socket连接
socket_handle = SocketCreate(); // 创建成功,socket_handle=1
//--- 创建时间触发器,1秒
EventSetTimer(1);
//---
return(INIT_SUCCEEDED);
}
void OnTimer()
{
//--- 创建一个socket连接
socket_handle = SocketCreate(); // 创建成功,socket_handle>=1
//--- 连接socket服务器
if ( true
&& socket_handle!=INVALID_HANDLE
)
{
if ( true
&& SocketConnect(socket_handle, server_ip, server_port, server_timeout) // 每秒连接一次
)
{
str_send="123中文abc";
str_receive = StringSend(socket_handle, str_send) ? StringRecv(socket_handle, server_timeout) : "";
Comment(TerminalInfoInteger(TERMINAL_CODEPAGE));
Print("接收来自python的信息: ",str_receive);
}
}
//--- 销毁socket连接
SocketClose(socket_handle);
}
bool StringSend(int socket,string request)
{
/*
发送信息
*/
char req[];
int len=StringToCharArray(request,req)-1;
if(len<0) return(false);
return(SocketSend(socket,req,len)==len);
}
string StringRecv(int socket, uint timeout)
{
/*
接收信息
*/
// 定义一个缓冲区大小
char rsp[];
string result = "";
uint len;
uint timeout_check=GetTickCount()+timeout;
//--- 读取套接数据
do
{
len=SocketIsReadable(socket);
if(len)
{
int rsp_len=SocketRead(socket,rsp,len,timeout);
if(rsp_len>0)
{
result+=CharArrayToString(rsp,0,rsp_len,CP_UTF8);
}
}
}
while(GetTickCount()<timeout_check && !IsStopped());
//---
return(result);
}
void OnDeinit(const int reason)
{
//--- 销毁socket连接
SocketClose(socket_handle);
//--- 销毁时间触发器
EventKillTimer();
}
void OnTick()
{
//---
}
我发现MT5接收到python发送的字符,如果有中文,就会出现乱码。以下编写了一个函数来自动处理。
经过处理的字符被MT5接收后,str_receive变量获取的字符串已经自动将头尾空格去掉。
给字符串添加空格的函数
# 定义一个函数,接受一个中英文混写的字符串,返回一个前后加空格的字符串
def add_spaces(my_sct_string):
"""
:param my_sct_string: 字符串
:return: 整形后的字符串
"""
# 定义一个变量,存储中文字符和标点符号的个数
chinese_count = 0
# 定义一个常量,存储中文标点符号的范围
chinese_punctuation = u'\uFF01-\uFF5E\u0020-\u007E\u3000-\u303F'
# 遍历字符串中的每个字符
for char in my_sct_string:
# 如果是中文或者中文标点符号,就增加中文字符和标点符号的个数
if (char.isalpha() and not char.isascii()) or char in chinese_punctuation:
chinese_count += 1
# 如果字符串开头是中文或者中文标点符号,就在前面加一个空格
if (my_sct_string[0].isalpha() and not my_sct_string[0].isascii()) or my_sct_string[0] in chinese_punctuation:
my_sct_string = " " + my_sct_string
# 如果字符串中有中文或者中文标点符号,就在后面添加中文字符和标点符号的两倍
if chinese_count > 0:
my_sct_string += " " * (chinese_count * 2)
# 返回修改后的字符串
return my_sct_string
# 测试一下函数
sct_string = "你好!hello!你好"
print("原始字符串是:{}".format(sct_string))
print("修改后的字符串是:{}".format(add_spaces(sct_string)))
运行后的结果,输出字符反选部分显示了空格数量:
=== 实验报告结束 ===
有朋友问我干这个做什么用?借此机会多说两句。
国内股票期货量化系统已经有很多了,虽然他们都用Python编程,但技术规范各不相同。这就意味着你在一个平台编好的程序,换个平台就要重新写代码。
国内这些量化平台的可视化回测、可视化交易功能都很弱,不像MT5的回测功能,可以在指定时间段内逐个报价(tick)跑历史行情,这可以让我们大大缩短策略开发周期。
在国内量化平台上,你可以注册模拟、实盘账号,可以做交易。
我做这个的目的就是想从国内量化平台获取行情数据、账户数据,在MT5中呈现出来,并利用MT5强大的回测功能(可以多品种、多时间周期聚合)快速完成策略开发。
我经历过MT4到MT5伪升级,非常痛苦,同一家公司开发出来的量化系统,居然在语言上不能兼容。
从那以后,我就在想,但凡有机会,就要搞一套不被软件商捆绑的量化方案出来。做一套不被国内量化平台、不被国外量化平台例如MT5在编程语言上捆绑的系统。
我知道工作量有多大,靠一个人几乎不可能完成,但我有愚公移山的精神,慢慢挖,总有一天能成功。人多力量大,我也热切盼望这一路有更多的同志。
路漫漫其修远兮,吾将上下而求索。