9月底,一创和聚宽各自在平台上公布一创聚宽实盘将在12月29日关闭,届时无法再提供实盘交易服务。
事件一出,一片哗然。这似乎是在意料之外,情理之中。这几年来,聚宽陆陆续续关停的东西已经很多,比如策略交易,比如归因分析(后续在用户的坚持下转为VIP功能),等等。在今年六七月,由于A股市场的低迷,国家对量化的打击,监管变严,似乎成为了压倒聚宽的最后一根稻草。毕竟在一创聚宽宣布关停之前,才刚准备说要上安全模块呢。
在后续三个月里,小木屋也不断寻找更好的量化平台,心里也期待着聚宽能够找到新的合作方(虽然好像成为一种奢望)。最终敲定了QMT的方案,至于原因嘛,哈哈,就是因为国信的QMT(国信iquant)可以记住密码和自动登录,方便与聚宽进行交互,无需每天在服务器上手动输入密码登录。而国金开放了miniQMT,方便与本地交互,特别是采用AI模型。miniQMT可以使用本地环境,不需要再安装环境,对程序员来说更加友好。
在使用QMT的过程中,当然也是血与泪啊。相对聚宽平台而言,QMT的接口更加难用。举个栗子, run_weekly(weekly_adjustment, 1, ‘9:30’)用于指定函数weekly_adjustment每周一早上9点30分运行一次且仅此一次,run_daily(prepare_stock_list, ‘9:05’)指定函数prepare_stock_list在每天的9点5分运行一次。但是QMT中只有run_time定时函数。run_time(“myHandlebar”,“3nSecond”,“2019-10-14 13:20:00”,“SH”) 该函数的含义为2019-10-14 13:20:00开始每3秒运行一次myHandlebar函数。在这情况下无法精准指定函数在什么时间运行,运行几次,必须通过时间判断和标志位flag判断,从而实现聚宽run_daily和run_weekly的功能。此外,QMT存在行情数据不准确,回测时间过久等问题,因此QMT的定位更多是作为一个下单工具使用。
吐槽归吐槽,但是QMT确实是拯救聚宽实盘的一个绝佳方案了。什么,你跟我说Ptrade?大部分Ptrade可是不支持外部连接数据库的。easytrader?这种不是跟证券直连的先不说延时速度怎样,光是时不时就断线和服务崩溃已经够小木屋喝一壶了,唯一的好处就是自由度确实非常高。
如果你让我首选,我会选miniQMT。毕竟miniQMT解决了下单延时问题和服务崩溃问题,自由度非常高,适合像小木屋这样的程序员,搭配supervisor-win监控程序运行,应该是一个不错的选择。但是miniQMT并不是所有证券公司都会开放的,毕竟涉及合规问题,而且这个方案小木屋还没尝试,以后搞定了再分享。
接下来,小木屋先讲解QMT的方案,从聚宽如何输出订单信息给数据库。在本文中,小木屋采用的是mysql数据库,且是服务器自带的数据库(为了省钱,如果读者们考虑实时性和稳定性也可以购买云数据库)。
读者们在自己的聚宽代码里修改,主要是这两大函数:
# 创建一个基类,用于声明数据模型
Base = declarative_base()
class JoinQuantTable(Base):
__tablename__ = 'joinquant_stock' # 设置数据库表名称
pk = Column(String(36), primary_key=True, default=str(uuid.uuid1())) # 唯一识别码,可以理解为订单号,区分不同的订单
code = Column(String) # 证券代码
tradetime = Column(DateTime, default=datetime.datetime.now()) # 交易时间
order_values = Column(Integer) # 下单数量,可以改名为amount
price = Column(Integer) # 下单价格
ordertype = Column(String) # 下单方向,买 或 卖
if_deal = Column(Boolean, default=False) # 是否已经成交
insertdate = Column(DateTime, default=datetime.datetime.now()) # 订单信息插入数据库的时间
在上述这段代码中,要特别注意的是tablename数据库表名,改成自己的。剩下的order_values、price、ordertype 是否需要这三个特征就看自己了。小木屋只需要持仓信息,因此只需要提前(9点15分,避免9点30分的高峰期)把持仓信息提前发给数据库,再交给迅投QMT处理,因此order_values、price、ordertype对我不重要,code是对的即可。
#5.2下单指令入库函数
def push_order_command(order_dict_list):
def format_code(code):
code = code.replace('.XSHE','.SZ')
code = code.replace('.XSHG','.SH')
return code
try:
# Define your database connection parameters
db_user = 'xxxxxx' # 修改为你的数据库用户名
db_password = 'xxxxx' # 修改为你的数据库密码
db_host = 'xxxx.x.xxx.xx' # 修改为你的数据库ip
db_name = 'xxxxxxx' # 修改为你的数据库名称
# Create an SQLAlchemy engine
engine = create_engine(f'mysql://{db_user}:{db_password}@{db_host}/{db_name}')
# 创建一个基类,用于声明数据模型
# Create a session
Session = sessionmaker(bind=engine)
session = Session()
for order_dict in order_dict_list:
pk = order_dict['pk'] # 唯一识别码,可以理解为订单号,区分不同的订单
code = format_code(order_dict['code']) # 证券代码
tradetime = order_dict['tradetime'] # 交易时间
order_values = order_dict['order_values'] # 下单数量,可以改名为amount
price = order_dict['price'] # 下单价格
ordertype = order_dict['ordertype'] # 下单方向,买 或 卖
if_deal = order_dict['if_deal'] # 是否已经成交
insertdate = order_dict['insertdate'] # 订单信息插入数据库的时间
# Create a new record
new_record = JoinQuantTable(
pk=pk,
code=code,
tradetime=tradetime,
order_values=order_values,
price=price,
ordertype=ordertype,
if_deal=if_deal,
insertdate=insertdate
)
# Add the record to the session
session.add(new_record)
# Commit the changes to the database
session.commit()
# Close the session
session.close()
except Exception as e:
print('数据库出错')
print(e)
这部分的内容根据上面的自己修改后的信息进行修改。db_user、db_password、db_host和db_name记得需要修改。由于order_dict_list是一个列表,因此把聚宽下单信息(字典)都整合进列表list中再传给数据库,这样可以减少不断打开和关闭数据库导致的延时。一个下单信息应该包含pk到insertdate共8个信息,当然,也可以自己修改。记得聚宽和QMT都要修改即可。
QMT部分的代码,先说一下踩过的坑:
(1)尽量用run_time函数,不要用handlebar,对于开盘就需要交易的朋友很重要。(2)数据库挂掉后,QMT代码也无法继续运行。(3)当购买很多股票时,委托了但无法成交。(4)没有设置标志位,导致买股票时多次委托。
迅投QMT部分的思路如下:
(1)采用run_time函数,确保代码能在9点30分能运行起来,防止handlebar会有延迟。(2)采用run_time是每3秒运行一次,确保留住时间给代码去读取数据库;在读取数据库的时候需要采用异常处理语句try-except,防止数据库挂掉或其他原因导致的QMT代码无法正常运行。(3)小木屋是设定了早上9点29分到下午15点30分运行代码,会一直轮询数据库,确保数据库读取后第一时间让QMT下单。9点30分5秒这一刻会进行下单操作,为了防止开盘波动较大且订单量较小。如果读者的资金量过大,可以考虑买5价和卖5价进行交易,虽然有一定滑点,但滑点不会很大且能确保交易成功;又或者进行拆单操作,相隔一定时间再继续下单。(4)小木屋在中午收盘和晚上收盘前几分钟删除数据库的所有信息,防止订单信息交叉错误。
def init(ContextInfo):
global position_flag, delete_flag, order_flag
ContextInfo.run_time("myHandlebar","3nSecond","2019-10-14 13:20:00","SH")
position_flag = False
delete_flag = True
order_flag = True
account = "xxxxxxxx"
ContextInfo.accID = str(account)
ContextInfo.set_account(ContextInfo.accID)
print('init')
从数据库获取订单信息和删除订单信息
def get_data(query_str):
today_date = datetime.today().date()
today_date = today_date.strftime('%Y-%m-%d')
host = "xxx.x.xxx.xx"
port = 3306
user = "xxxxxx"
password = "xxxxxxx"
database = 'xxxxx'
# 连接 MySQL 数据库
conn = pymysql.connect(host=host, port=port, user=user,
password=password, database=database,
charset='utf8')
cursor = conn.cursor()
# 执行 SQL 查询语句
cursor.execute(query_str)
# 获取查询结果
result = cursor.fetchall()
# 将查询结果转化为 Pandas dataframe 对象
res = pd.DataFrame([result[i] for i in range(len(result))], columns=[i[0] for i in cursor.description])
res['tradedate'] = res['tradetime'].apply(lambda x:x.strftime('%Y-%m-%d'))
res = res[res['tradedate'] == today_date]
target_stock = res['code'].tolist()
cursor.close()
conn.close()
return target_stock
def delete_data():
query_str = """DELETE FROM joinquant.joinquant_stock_shipan5S"""
host = "xxx.x.xxx.xx"
port = 3306
user = "xxxxxx"
password = "xxxx"
database = 'xxxxxx'
try:
# 连接 MySQL 数据库
conn = pymysql.connect(host=host, port=port, user=user,
password=password, database=database,
charset='utf8')
cursor = conn.cursor()
# 执行 SQL 查询语句
cursor.execute(query_str)
conn.commit()
cursor.close()
conn.close()
print('删除数据库中的所有数据')
except Exception as e:
print('错误:{}'.format(e))
return
核心运行代码,指定交易时间进行下单操作
def myHandlebar(ContextInfo):
global position_flag, delete_flag, order_flag
current_time = datetime.now().time()
# 设置起始和结束时间
morning_start_time = time(9, 30, 5)
morning_end_time = time(9, 32)
morning_delete_database_start = time(11, 29)
morning_delete_database_end = time(11, 30)
target_stock = []
buy_direction = 23
sell_direction = 24
SALE3 = 2
BUY3 = 8
lastest_price = 5
day_start_time = time(9, 29)
day_end_time = time(15, 30)
if day_start_time <= current_time and current_time <= day_end_time:
# print('handlebar:{}'.format(datetime.now()))
query_str = """select * from joinquant.joinquant_stock_shipan5S"""
try:
target_stock = get_data(query_str)
except Exception as e:
target_stock = []
print('发生错误,错误:{}'.format(e))
if len(target_stock) < 1:
return
if morning_start_time <= current_time and current_time <= morning_end_time:
position_flag = True
delete_flag = True
if order_flag == False:
return
position_info = get_trade_detail_data(ContextInfo.accID, 'stock', 'position')
postion_code = []
pistion_volumn = {}
if len(position_info) > 0:
for ele in position_info:
if ele.m_nVolume > 0:
code_num = code_prefix_dix(ele.m_strInstrumentID)
postion_code.append(code_num)
pistion_volumn[code_num] = ele.m_nVolume
lastest_postion_code = postion_code.copy()
acc_info = get_trade_detail_data(ContextInfo.accID, 'stock', 'account')
avail_balance = acc_info[0].m_dAvailable
order_num = 5
order_value = avail_balance / order_num
order_value = order_value / 1000 # 实盘的时候去掉
for code_ele in postion_code[::-1]:
if code_ele not in target_stock:
passorder(sell_direction, 1101, ContextInfo.accID, code_ele, BUY3, -1, pistion_volumn[code_ele], '', 1, '', ContextInfo)
del lastest_postion_code[postion_code.index(code_ele)]
print('postion_code:', postion_code)
acc_info = get_trade_detail_data(ContextInfo.accID, 'stock', 'account')
avail_balance = acc_info[0].m_dAvailable
order_num = len(target_stock) - len(lastest_postion_code)
if order_num != 0:
order_value = avail_balance / order_num
order_value = order_value / 1000 # 实盘的时候去掉
for code_ele_buy in target_stock:
if code_ele_buy not in lastest_postion_code:
passorder(buy_direction, 1102, ContextInfo.accID, code_ele_buy, SALE3, -1, 20000, '', 1, '', ContextInfo)
lastest_postion_code.append(code_ele_buy)
if len(lastest_postion_code) >= len(target_stock):
break
order_flag = False
elif morning_delete_database_start < current_time and current_time < morning_delete_database_end:
order_flag = True
if delete_flag == True:
delete_data()
delete_flag = False
喜欢小木屋分享的内容的话,请记得关注小木屋。完整代码可以关注小木屋后私信领取哦。
如果有需要开通QMT的也可以私信小木屋哦