交易执行端,采用的量化交易软件为讯投QMT的Mini版本, 目前QMT仅支持在Windows运行,需要先联系证券公司的客户经理开通QMT(需支持极简模式/Mini),推荐国投证券、国金证券、国信证券的QMT。目前可支持讯投QMT的券商和对应的入金门槛条件参考:https://www.xuntou.net/forum.php?mod=viewthread&tid=232&extra=&user_code=e2M5nZ
除了 Mini-QMT 外,还需要安装如下基础执行程序:
(推荐)在windows电脑的用户名(即 %HomePath%\trader 中,如 C:\Users\用户名\trader)下面新建trader文件夹,将客户端压缩包在 trader 文件夹下解压,形成正确的路径%HomePath%\trader\GUI\安装.bat(例如:C:\Users\用户名\trader\GUI\安装.bat)
本程序配套Python 3.11.4试用,官方下载链接为:
https://www.python.org/ftp/python/3.11.4/python-3.11.4-amd64.exe
安装时,勾选加入 PATH,勾选为所有用户安装。这样安装目录会在在 C:\Python311
pip依赖安装已包括在 安装.bat中,若源失效,可以替换为可用源手动安装
pip install websockets==13.1 pyside6==6.8.0.2 – i https://mirrors.aliyun.com/pypi/simple
QMT 依赖需要将 xtquant 库(在%QMT Path%\bin\x64\Lib\site-packages\xtquant)复制到系统 python 库目录(C:\Python311\Lib\site-packages)下。例如:从 D:\QMT\bin\x64\Lib\site-packages\xtquant把xtquant文件夹复制到 C:\Python311\Lib\site-packages\中,形成C:\Python311\Lib\site-packages\xtquant 目录。
配置链接mini-qmt的文件confi.Ini(含qmt 的userdata_mini路径和证券账号信息),路径为 %HomePath%\trader\conf\conf.ini,相关参数值根据注释填写。
[qmt_a]
; enable - trader将会连接此QMT; disable - trader不会连接词QMT
status=enable
; 您的QMT安装目录,需要到userdata_mini
qmt_dir = D:\QMT\userdata_mini
; 您的券商账户
account_id = 1234567890
[global]
; 您的用户名
user = example_user
; 您的token,请在控制台页面查询填入
token = ut8x@f09
若有2个qmt账号,可参考如下注释进行配置
[qmt_a]
; enable - trader将会连接此QMT; disable - trader不会连接词QMT
status=enable
; 您的QMT安装目录,需要到userdata_mini
qmt_dir = D:\QMT\userdata_mini
; 您的券商账户
account_id = 1234567890
[qmt_b]
; enable - trader将会连接此QMT; disable - trader不会连接词QMT
status=enable
; 您的QMT安装目录,需要到userdata_mini
qmt_dir = D:\QMT2\userdata_mini
; 您的券商账户
account_id = 2234567890
[global]
; 您的用户名
user = example_user
; 您的token,请在控制台页面查询填入
token = ut8x@f09
双击打开mini-QMT(打开QMT的登录界面时选择极简模式),双击桌面上的快捷方式trader启动mini-qmt连接器,观察日志显示 链接QMT成功 和 信传用户登录成功,则代表执行端就绪。
策略端为在聚宽(www.joinquant.com)量化平台上运行的分钟级/tick级实时模拟策略,需要先在注册聚宽用户并开通实时分钟/Tick 级模拟交易服务,(备注:分钟级模拟策略可使用积分兑换,聚宽的 积分中心-积分商城)。
聚宽量化策略需要进行如下改写:
import requests
from requests.adapters import HTTPAdapter
原始的 initialize函数
def initialize(context):
…… #省略一些全局变量配置等内容
# 设置交易运行时间
run_daily(prepare_stock_list, '9:05')
run_daily(trade_afternoon, time="14:00", reference_security='399101.XSHE') # 下午检查交易
run_daily(stop_loss, time="10:00") # 止损函数
run_daily(close_account, "14:50") # 清仓后次日资金可转
run_weekly(weekly_adjustment, 2, '10:00') # 整体调整持仓
run_weekly(print_position_info, 5, time='15:10', reference_security='000300.XSHG')
在initialize 函数函数增加如下 after_code_change()函数
def after_code_change(context):
…… #与 initialize中的全局变量配置等内容相同
unschedule_all()
# 设置交易运行时间
run_daily(prepare_stock_list, '9:05')
run_daily(trade_afternoon, time="14:00", reference_security='399101.XSHE') # 下午检查交易
run_daily(stop_loss, time="10:00") # 止损函数
run_daily(close_account, "14:50") # 清仓后次日资金可转
run_weekly(weekly_adjustment, 2, '10:00') # 整体调整持仓
run_weekly(print_position_info, 5, time='15:10', reference_security='000300.XSHG')
#远程Linux服务器信号方案 (升级版)
def signal(stockid, name, direction, price, volume):
user, token = '用户名', '传输Token'
domain = 'https://api-v2.300668.xyz:16888'
data = {'stockid': stockid, 'name': name, 'direction': direction, 'price': price, 'volume': volume}
s1 = requests.Session()
s1.mount('https://', HTTPAdapter(max_retries=5))
try:
r1 = s1.post(f"{domain}/ss/s/strategy_a0", json=data, headers={'Auth': user + " " + token}, timeout=15)
print(r1, r1.text)
except requests.exceptions.RequestException as e:
print("RequestException ", e)
需要修改的部分包括:
user, token = '用户名', '传输Token'。其中用户名为注册信传官网时使用的英文用户名,传输Token为登录个人后台后,在主页显示的传输Token。
策略代号:"strategy_a0"。"strategy_a0" 代表本地执行端部分的 1 个 qmt 账号执行的一个策略,1个qmt账号最多可支持多个策略 (免费试用版只支持1个策略strategy_a0,普通会员支持5 个策略:strategy_a0 到 strategy_a4,高级会员可支持 10 个策略:strategy_a0 到 strategy_a9);另外,"strategy_b0"和"strategy_c0"代表另外 2 个qmt 账号执行的策略,目前免费试用版只支持 1 个qmt账号,普通会员可支持 2 个qmt账号(2个qmt账号分别对应signal函数中的strategy_a0 和strategy_b0,同时在客户端配置文件confi.ini中配置好 [qmt_a] 和[qmt_b]userdata_mini路径和证券账号信息),高级会员最多支持 5 个qmt账号。
原代码:
# 3-4买入模块
def buy_security(context, target_list, num):
# 调仓买入
position_count = len(context.portfolio.positions)
target_num = num
if target_num != 0:
value = context.portfolio.cash / target_num
for stock in target_list:
order_target_value(stock, value)
log.info("买入[{}]({}元)".format(stock, value))
if len(context.portfolio.positions) == g.stock_num:
break
# 1-7止盈止损
def stop_loss(context):
if g.run_stopless:
current_positions = context.portfolio.positions
for stock in current_positions.keys():
avg_cost = current_positions[stock].avg_cost
price = current_positions[stock].price
# 个股盈利止盈
if price >= avg_cost * 2:
order_target_value(stock, 0)
log.debug("收益100%止盈,卖出({})".format(stock))
# 个股止损
elif price < avg_cost * (1 - g.stoploss_limit):
order_target_value(stock, 0)
log.debug("收益止损,卖出({})".format(stock))
g.stoploss_list.append(stock)
if g.stoploss_strategy == 2 or g.stoploss_strategy == 3:
stock_df = get_price(security=get_index_stocks('399101.XSHE'),
end_date=context.previous_date, frequency='daily', fields=['close', 'open'], count=1, panel=False)
# 计算成分股平均涨跌,即指数涨跌幅
down_ratio = (1 - stock_df['close'] / stock_df['open']).mean()
# 市场大跌止损
if down_ratio >= g.stoploss_market:
g.stoploss_list.append(stock)
log.debug("大盘惨跌,平均降幅(:.2%)".format(down_ratio))
for stock in current_positions.keys():
order_target_value(stock, 0)
修改代码:
# 3-4买入模块
def buy_security(context, target_list, num):
# 调仓买入
position_count = len(context.portfolio.positions)
target_num = num
if target_num != 0:
value = context.portfolio.cash / target_num
for stock in target_list:
order=order_target_value(stock, value)
stock1 = stock.replace('XSHG','SH')
stock2 = stock1.replace('XSHE','SZ')
if order != None and order.filled > 0:
price = order.price
amount = order.amount
security_info = get_security_info(stock)
signal(stock2, security_info.display_name, 'buy', price, amount)
log.info("买入[{}]({}元)".format(stock, value))
if len(context.portfolio.positions) == g.stock_num:
break
# 1-7止盈止损
def stop_loss(context):
if g.run_stopless:
current_positions = context.portfolio.positions
for stock in current_positions.keys():
avg_cost = current_positions[stock].avg_cost
price = current_positions[stock].price
# 个股盈利止盈
if price >= avg_cost * 2:
order=order_target_value(stock, 0)
stock1 = stock.replace('XSHG','SH')
stock2 = stock1.replace('XSHE','SZ')
if order != None and order.status == OrderStatus.held:
price=order.price
amount=order.amount
security_info = get_security_info(stock)
signal(stock2, security_info.display_name, 'sell', price, amount)
log.debug("收益100%止盈,卖出({})".format(stock))
# 个股止损
elif price < avg_cost * (1 - g.stoploss_limit):
order=order_target_value(stock, 0)
stock1 = stock.replace('XSHG','SH')
stock2 = stock1.replace('XSHE','SZ')
if order != None and order.status == OrderStatus.held:
price=order.price
amount=order.amount
security_info = get_security_info(stock)
signal(stock2, security_info.display_name, 'sell', price, amount)
log.debug("收益止损,卖出({})".format(stock))
g.stoploss_list.append(stock)
if g.stoploss_strategy == 2 or g.stoploss_strategy == 3:
stock_df = get_price(security=get_index_stocks('399101.XSHE'),
end_date=context.previous_date, frequency='daily', fields=['close', 'open'], count=1, panel=False)
# 计算成分股平均涨跌,即指数涨跌幅
down_ratio = (1 - stock_df['close'] / stock_df['open']).mean()
# 市场大跌止损
if down_ratio >= g.stoploss_market:
g.stoploss_list.append(stock)
log.debug("大盘惨跌,平均降幅(:.2%)".format(down_ratio))
for stock in current_positions.keys():
order=order_target_value(stock, 0)
stock1 = stock.replace('XSHG','SH')
stock2 = stock1.replace('XSHE','SZ')
if order != None and order.status == OrderStatus.held:
price=order.price
amount=order.amount
security_info = get_security_info(stock)
signal(stock2, security_info.display_name, 'sell', price, amount)
原代码:
if num >=1:
if len(SS) > 0:
# 清空记录
num=3
min_values = sorted(SS)[:num]
min_indices = [SS.index(value) for value in min_values]
min_strings = [S[index] for index in min_indices]
cash = context.portfolio.cash/num
for ss in min_strings:
order_value(ss, cash)
feishu(stock,'buy')
log.debug("补跌最多的N支 Order %s" % (ss))
if ss not in g.bought_stocks:
g.bought_stocks[ss] = cash
def clear(context):#卖出补跌的仓位
print(g.bought_stocks)
if g.bought_stocks!={}:
for stock, amount in g.bought_stocks.items():
if stock in context.portfolio.positions:
order_value(stock, -amount) # 卖出股票至目标价值为0
log.info("卖出补跌股票: %s, 卖出金额: %s" % (stock, amount)
# 清空记录
g.bought_stocks.clear()
修改代码:
if num >=1:
if len(SS) > 0:
# 清空记录
num=3
min_values = sorted(SS)[:num]
min_indices = [SS.index(value) for value in min_values]
min_strings = [S[index] for index in min_indices]
cash = context.portfolio.cash/num
for ss in min_strings:
order=order_value(ss, cash)
ss1 = ss.replace('XSHG','SH')
ss2 = ss1.replace('XSHE','SZ')
if order != None and order.filled > 0:
price = order.price
amount = order.amount
security_info = get_security_info(ss)
signal(ss2, security_info.display_name, 'buy', price, amount)
log.info("买入[{}]({}元)".format(ss, cash))
feishu(stock,'buy')
log.debug("补跌最多的N支 Order %s" % (ss))
if ss not in g.bought_stocks:
g.bought_stocks[ss] = cash
def clear(context):#卖出补跌的仓位
print(g.bought_stocks)
if g.bought_stocks!={}:
for stock, amount in g.bought_stocks.items():
if stock in context.portfolio.positions:
order=order_value(stock, -amount) # 卖出股票至目标价值为0
stock1 = stock.replace('XSHG','SH')
stock2 = stock1.replace('XSHE','SZ')
log.info("卖出补跌股票: %s, 卖出金额: %s" % (stock, amount))
if order != None and order.status == OrderStatus.held:
price=order.price
amount=order.amount
security_info = get_security_info(stock)
signal(stock2, security_info.display_name, 'sell', price, amount)
log.info("卖出[g%s]"%(stock))
# 清空记录
g.bought_stocks.clear()
原代码:
# 尾盘处理
def end_trade(context):
current_data = get_current_data()
# 卖出未记录的股票(比如送股)
keys = [key for d in g.positions.values() if isinstance(d, dict) for key in d.keys()]
for stock in context.portfolio.positions:
if stock not in keys and stock != g.fill_stock and current_data[stock].last_price < current_data[stock].high_limit:
#print(order_target_value(stock, 0))
if order_target_value(stock, 0):
log.info(f"卖出{stock}因送股未记录在持仓中")
# 买入货币ETF
amount = int(context.portfolio.available_cash / current_data[g.fill_stock].last_price)
if amount >= 100:
order(g.fill_stock, amount)
# 自定义下单(涨跌停不交易)
def order_target_value_(self, security, value):
current_data = get_current_data()
# 检查标的是否停牌、涨停、跌停
if current_data[security].paused:
log.info(f"{security}: 今日停牌")
return False
# 检查是否涨停
if current_data[security].last_price == current_data[security].high_limit:
log.info(f"{security}: 当前涨停")
return False
# 检查是否跌停
if current_data[security].last_price == current_data[security].low_limit:
log.info(f"{security}: 当前跌停")
return False
# 获取当前标的的价格
price = current_data[security].last_price
# 获取当前策略的持仓数量
current_position = g.positions[self.index].get(security, 0)
# 计算目标持仓数量
target_position = (int(value / price) // 100) * 100 if price != 0 else 0
# 计算需要调整的数量
adjustment = target_position - current_position
# 检查是否当天买入卖出
closeable_amount = self.context.portfolio.positions[security].closeable_amount if security in self.context.portfolio.positions else 0
if adjustment < 0 and closeable_amount == 0:
log.info(f"{security}: 当天买入不可卖出")
return False
# 下单并更新持仓
if adjustment != 0:
o = order(security, adjustment)
if o:
# 更新持仓数量
amount = o.amount if o.is_buy else -o.amount
g.positions[self.index][security] = amount + current_position
# 如果目标持仓为零,移除该证券
if target_position == 0:
g.positions[self.index].pop(security, None)
# 更新持有列表
self.hold_list = list(g.positions[self.index].keys())
return True
return False
修改代码:
# 尾盘处理
def end_trade(context):
current_data = get_current_data()
# 卖出未记录的股票(比如送股)
keys = [key for d in g.positions.values() if isinstance(d, dict) for key in d.keys()]
for stock in context.portfolio.positions:
if stock not in keys and stock != g.fill_stock and current_data[stock].last_price < current_data[stock].high_limit:
#if order_target_value(stock, 0):
# log.info(f"卖出{stock}因送股未记录在持仓中")
order = order_target_value(stock, 0)
stock1 = stock.replace('XSHG','SH')
stock2 = stock1.replace('XSHE','SZ')
if order:
if order.status == OrderStatus.held and order.filled == order.amount:
price=order.price
amount=order.amount
security_info = get_security_info(stock)
signal(stock2, security_info.display_name, 'sell', price, amount)
log.info(f"卖出{stock}因送股未记录在持仓中")
# 买入货币ETF
amount = int(context.portfolio.available_cash / current_data[g.fill_stock].last_price)
if amount >= 100:
#order(g.fill_stock, amount)
order = order_target_value(g.fill_stock, context.portfolio.available_cash)
fill_stock1 = g.fill_stock.replace('XSHG','SH')
fill_stock2 = fill_stock1.replace('XSHE','SZ')
if order != None and order.filled > 0:
price=order.price
amount=order.amount
security_info = get_security_info(g.fill_stock)
signal(fill_stock2, security_info.display_name, 'buy', price, amount)
# 自定义下单(涨跌停不交易)
def order_target_value_(self, security, value):
current_data = get_current_data()
# 检查标的是否停牌、涨停、跌停
if current_data[security].paused:
log.info(f"{security}: 今日停牌")
return False
# 检查是否涨停
if current_data[security].last_price == current_data[security].high_limit:
log.info(f"{security}: 当前涨停")
return False
# 检查是否跌停
if current_data[security].last_price == current_data[security].low_limit:
log.info(f"{security}: 当前跌停")
return False
# 获取当前标的的价格
price = current_data[security].last_price
# 获取当前策略的持仓数量
current_position = g.positions[self.index].get(security, 0)
# 计算目标持仓数量
target_position = (int(value / price) // 100) * 100 if price != 0 else 0
# 计算需要调整的数量
adjustment = target_position - current_position
# 检查是否当天买入卖出
closeable_amount = self.context.portfolio.positions[security].closeable_amount if security in self.context.portfolio.positions else 0
if adjustment < 0 and closeable_amount == 0:
log.info(f"{security}: 当天买入不可卖出")
return False
# 下单并更新持仓
if adjustment != 0:
#o = order(security, adjustment)
o = order_target_value(security, value)
security1 = security.replace('XSHG','SH')
security2 = security1.replace('XSHE','SZ')
if o:
# 更新持仓数量
#amount = o.amount if o.is_buy else -o.amount
if adjustment>0:
if o != None and o.filled > 0:
price=o.price
amount=o.amount
security_info = get_security_info(security)
signal(security2, security_info.display_name, 'buy', price, amount)
else:
if o != None:
if o.status == OrderStatus.held and o.filled == o.amount:
price=o.price
amount=-o.amount
security_info = get_security_info(security)
signal(security2, security_info.display_name, 'sell', price, -amount)
g.positions[self.index][security] = amount + current_position
# 如果目标持仓为零,移除该证券
if target_position == 0:
g.positions[self.index].pop(security, None)
# 更新持有列表
self.hold_list = list(g.positions[self.index].keys())
return True
return False
原代码:
if len(qualified_stocks)!=0 and context.portfolio.available_cash/context.portfolio.total_value>0.3:
value = context.portfolio.available_cash / len(qualified_stocks)
for s in qualified_stocks:
# 下单
#由于关闭了错误日志,不加这一句,不足一手买入失败也会打印买入,造成日志不准确
if context.portfolio.available_cash/current_data[s].last_price>100:
order_value(s, value, MarketOrderStyle(current_data[s].day_open))
print('买入' + s)
print('———————————————————————————————————')
修改代码:
if len(qualified_stocks)!=0 and context.portfolio.available_cash/context.portfolio.total_value>0.3:
value = context.portfolio.available_cash / len(qualified_stocks)
for s in qualified_stocks:
# 下单
#由于关闭了错误日志,不加这一句,不足一手买入失败也会打印买入,造成日志不准确
if context.portfolio.available_cash/current_data[s].last_price>100:
order = order_value(s, value, MarketOrderStyle(current_data[s].day_open))
s1 = s.replace('XSHG','SH')
s2 = s1.replace('XSHE','SZ')
price = order.style._limit_price
amount =order.amount
security_info = get_security_info(s)
signal(s2, security_info.display_name,'buy',price, amount)
print('买入' + s)
print('———————————————————————————————————')
登录信传官网的账号,可在信号看板部分查看交易信号的展示,包含聚宽实时模拟盘发送的交易信号(含交易的股票代码、名称、方向、量、价等)和qmt的实际执行情况。