chore: init qmt_bridge repo (HTTP+WS bridge, MCP endpoint, docs, references)

This commit is contained in:
Docker
2026-08-26 16:53:15 +08:00
commit 22a5b8ca04
210 changed files with 68176 additions and 0 deletions
File diff suppressed because one or more lines are too long
@@ -0,0 +1,343 @@
成交回报实时主推函数 | 迅投知识库
迅投知识库
首页
API API
投研新手教程
QMT新手教程
内置Python
XtQuant文档
VBA
数据字典 数据字典
快速开始
股票数据
行业概念数据
指数数据
期货数据
期权数据
场内基金数据
债券数据
常见问题
场景化示例
迅投因子
数据浏览器
策略迁移 策略迁移
聚宽策略
客服QQ: 810315303
联系方式: 18309226715
视频教程 视频教程
投研端教程
迅投学堂
在新窗口打开
投研平台
在新窗口打开
迅投社区
在新窗口打开
迅投官网
在新窗口打开
注册
登录
智能助手加载中...
首页
API API
投研新手教程
QMT新手教程
内置Python
XtQuant文档
VBA
数据字典 数据字典
快速开始
股票数据
行业概念数据
指数数据
期货数据
期权数据
场内基金数据
债券数据
常见问题
场景化示例
迅投因子
数据浏览器
策略迁移 策略迁移
聚宽策略
客服QQ: 810315303
联系方式: 18309226715
视频教程 视频教程
投研端教程
迅投学堂
在新窗口打开
投研平台
在新窗口打开
迅投社区
在新窗口打开
迅投官网
在新窗口打开
内置Python
快速开始
使用须知
界面操作
变量约定
数据结构
系统函数
行情函数
交易函数
成交回报实时主推函数
实时主推函数
account_callback - 资金账号状态变化主推
task_callback - 账号任务状态变化主推
order_callback - 账号委托状态变化主推
deal_callback - 账号成交状态变化主推
position_callback - 账号持仓状态变化主推
orderError_callback - 账号异常下单主推
其他主推函数
credit_account_callback - 查询信用账户明细回调
credit_opvolume_callback - 查询两融最大可下单量的回调
引用函数
绘图函数
枚举常量
完整示例
常见问题
# 实时主推函数
# account_callback - 资金账号状态变化主推
提示
仅在实盘运行模式下生效。
需要先在init里调用ContextInfo.set_account后生效。
用法: account_callback(ContextInfo, accountInfo)
释义: 当资金账号状态有变化时,这个函数被客户端调用
参数:
ContextInfo:特定对象
accountInfo 账号对象
在新窗口打开 或 信用账号对象
在新窗口打开
返回: 无
示例:
示例 返回值
#coding:gbk
def show_data ( data ):
tdata = {}
for ar in dir (data):
if ar[ : 2 ] != ' m_ ' : continue
try :
tdata[ar] = data. __getattribute__ (ar)
except :
tdata[ar] = ' <CanNotConvert> '
return tdata
def init ( ContextInfo ):
# 设置对应的资金账号
# 示例需要在策略交易界面运行
ContextInfo.set_account(account)
def after_init ( ContextInfo ):
# 在策略交易界面运行时,account的值会被赋值为策略配置中的账号,编辑器界面运行时,需要手动赋值
# 编译器界面里执行的下单函数不会产生实际委托
passorder( 23 , 1101 , account, " 000001.SZ " , 5 , 0 , 100 , " 示例 " , 2 , " 投资备注 " ,ContextInfo)
pass
def account_callback ( ContextInfo , accountInfo ):
print (show_data(accountInfo))
{ ' m_Enable ' : True , ' m_dAssetBalance ' : 9975010.001775814 , ' m_dAssureAsset ' : 9975010.001775814 , ' m_dAvailable ' : 9555238.805375814 , ' m_dBalance ' : 9975010.001775814 , ' m_dBuyWaitMoney ' : 0.0 , ' m_dCashIn ' : 0.0 , ' m_dCloseProfit ' : 0.0 , ' m_dCommission ' : 14.284000000000006 , ' m_dCredit ' : 0.0 , ' m_dCurrMargin ' : 0.0 , ' m_dDeposit ' : 0.0 , ' m_dEntrustAsset ' : 0.0 , ' m_dFetchBalance ' : 9556221.001775814 , ' m_dFrozenCash ' : 982.1964 , ' m_dFrozenCommission ' : 0.0 , ' m_dFrozenMargin ' : 9555238.805375814 , ' m_dFrozenRoyalty ' : 0.0 , ' m_dFundValue ' : 0.0 , ' m_dGoldFrozen ' : 0.0 , ' m_dGoldValue ' : 0.0 , ' m_dInitBalance ' : 0.0 , ' m_dInitCloseMoney ' : - 4249.283999999998 , ' m_dInstrumentValue ' : 418789.0 , ' m_dInstrumentValueRMB ' : 0.0 , ' m_dIntradayBalance ' : 0.0 , ' m_dIntradayFreedBalance ' : 0.0 , ' m_dLoanValue ' : 0.0 , ' m_dLongValue ' : 0.0 , ' m_dMargin ' : 0.0 , ' m_dMaxMarginRate ' : 0.0 , ' m_dMortgage ' : 0.0 , ' m_dNav ' : 0.0 , ' m_dNetValue ' : 0.0 , ' m_dOccupiedBalance ' : 0.0 , ' m_dPositionProfit ' : - 3441.6657800000025 , ' m_dPreBalance ' : 9556221.001775814 , ' m_dPreCredit ' : 0.0 , ' m_dPreMortgage ' : 0.0 , ' m_dPurchasingPower ' : 0.0 , ' m_dRawMargin ' : 0.0 , ' m_dRealRiskDegree ' : 0.0 , ' m_dRealUsedMargin ' : 0.0 , ' m_dReceiveInterestTotal ' : 0.0 , ' m_dRepurchaseValue ' : 0.0 , ' m_dRisk ' : 0.0 , ' m_dRoyalty ' : 0.0 , ' m_dSellWaitMoney ' : 0.0 , ' m_dShortValue ' : 0.0 , ' m_dStockValue ' : 418789.0 , ' m_dSubscribeFee ' : 0.0 , ' m_dTotalDebit ' : 0.0 , ' m_dWithdraw ' : 0.0 , ' m_nBrokerType ' : 2 , ' m_nDirection ' : 48 , ' m_strAccountID ' : ' 2000567 ' , ' m_strAccountKey ' : ' 2____11194____114911____49____2000567____ ' , ' m_strAccountRemark ' : '' , ' m_strBrokerName ' : '' , ' m_strMoneyType ' : '' , ' m_strOpenDate ' : '' , ' m_strStatus ' : ' 准备登录 ' , ' m_strTradingDate ' : ' 20240220 ' }
{ ' m_Enable ' : True , ' m_dAssetBalance ' : 9975010.001775814 , ' m_dAssureAsset ' : 9975010.001775814 , ' m_dAvailable ' : 9556221.001775814 , ' m_dBalance ' : 9975010.001775814 , ' m_dBuyWaitMoney ' : 0.0 , ' m_dCashIn ' : 0.0 , ' m_dCloseProfit ' : 0.0 , ' m_dCommission ' : 14.284000000000006 , ' m_dCredit ' : 0.0 , ' m_dCurrMargin ' : 0.0 , ' m_dDeposit ' : 0.0 , ' m_dEntrustAsset ' : 0.0 , ' m_dFetchBalance ' : 9556221.001775814 , ' m_dFrozenCash ' : 0.0 , ' m_dFrozenCommission ' : 0.0 , ' m_dFrozenMargin ' : 9556221.001775814 , ' m_dFrozenRoyalty ' : 0.0 , ' m_dFundValue ' : 0.0 , ' m_dGoldFrozen ' : 0.0 , ' m_dGoldValue ' : 0.0 , ' m_dInitBalance ' : 0.0 , ' m_dInitCloseMoney ' : - 4249.283999999998 , ' m_dInstrumentValue ' : 418789.0 , ' m_dInstrumentValueRMB ' : 0.0 , ' m_dIntradayBalance ' : 0.0 , ' m_dIntradayFreedBalance ' : 0.0 , ' m_dLoanValue ' : 0.0 , ' m_dLongValue ' : 0.0 , ' m_dMargin ' : 0.0 , ' m_dMaxMarginRate ' : 0.0 , ' m_dMortgage ' : 0.0 , ' m_dNav ' : 0.0 , ' m_dNetValue ' : 0.0 , ' m_dOccupiedBalance ' : 0.0 , ' m_dPositionProfit ' : - 3441.6657800000025 , ' m_dPreBalance ' : 9556221.001775814 , ' m_dPreCredit ' : 0.0 , ' m_dPreMortgage ' : 0.0 , ' m_dPurchasingPower ' : 0.0 , ' m_dRawMargin ' : 0.0 , ' m_dRealRiskDegree ' : 0.0 , ' m_dRealUsedMargin ' : 0.0 , ' m_dReceiveInterestTotal ' : 0.0 , ' m_dRepurchaseValue ' : 0.0 , ' m_dRisk ' : 0.0 , ' m_dRoyalty ' : 0.0 , ' m_dSellWaitMoney ' : 0.0 , ' m_dShortValue ' : 0.0 , ' m_dStockValue ' : 418789.0 , ' m_dSubscribeFee ' : 0.0 , ' m_dTotalDebit ' : 0.0 , ' m_dWithdraw ' : 0.0 , ' m_nBrokerType ' : 2 , ' m_nDirection ' : 48 , ' m_strAccountID ' : ' 2000567 ' , ' m_strAccountKey ' : ' 2____11194____114911____49____2000567____ ' , ' m_strAccountRemark ' : '' , ' m_strBrokerName ' : '' , ' m_strMoneyType ' : '' , ' m_strOpenDate ' : '' , ' m_strStatus ' : ' 准备登录 ' , ' m_strTradingDate ' : ' 20240220 ' }
{ ' m_Enable ' : True , ' m_dAssetBalance ' : 9975010.001775814 , ' m_dAssureAsset ' : 9975010.001775814 , ' m_dAvailable ' : 9555238.805375814 , ' m_dBalance ' : 9975010.001775814 , ' m_dBuyWaitMoney ' : 0.0 , ' m_dCashIn ' : 0.0 , ' m_dCloseProfit ' : 0.0 , ' m_dCommission ' : 14.480400000000007 , ' m_dCredit ' : 0.0 , ' m_dCurrMargin ' : 0.0 , ' m_dDeposit ' : 0.0 , ' m_dEntrustAsset ' : 0.0 , ' m_dFetchBalance ' : 9556221.001775814 , ' m_dFrozenCash ' : 0.0 , ' m_dFrozenCommission ' : 0.0 , ' m_dFrozenMargin ' : 9555238.805375814 , ' m_dFrozenRoyalty ' : 0.0 , ' m_dFundValue ' : 0.0 , ' m_dGoldFrozen ' : 0.0 , ' m_dGoldValue ' : 0.0 , ' m_dInitBalance ' : 0.0 , ' m_dInitCloseMoney ' : - 4249.283999999998 , ' m_dInstrumentValue ' : 419771.0 , ' m_dInstrumentValueRMB ' : 0.0 , ' m_dIntradayBalance ' : 0.0 , ' m_dIntradayFreedBalance ' : 0.0 , ' m_dLoanValue ' : 0.0 , ' m_dLongValue ' : 0.0 , ' m_dMargin ' : 0.0 , ' m_dMaxMarginRate ' : 0.0 , ' m_dMortgage ' : 0.0 , ' m_dNav ' : 0.0 , ' m_dNetValue ' : 0.0 , ' m_dOccupiedBalance ' : 0.0 , ' m_dPositionProfit ' : - 3441.6657800000025 , ' m_dPreBalance ' : 9556221.001775814 , ' m_dPreCredit ' : 0.0 , ' m_dPreMortgage ' : 0.0 , ' m_dPurchasingPower ' : 0.0 , ' m_dRawMargin ' : 0.0 , ' m_dRealRiskDegree ' : 0.0 , ' m_dRealUsedMargin ' : 0.0 , ' m_dReceiveInterestTotal ' : 0.0 , ' m_dRepurchaseValue ' : 0.0 , ' m_dRisk ' : 0.0 , ' m_dRoyalty ' : 0.0 , ' m_dSellWaitMoney ' : 0.0 , ' m_dShortValue ' : 0.0 , ' m_dStockValue ' : 419771.0 , ' m_dSubscribeFee ' : 0.0 , ' m_dTotalDebit ' : 0.0 , ' m_dWithdraw ' : 0.0 , ' m_nBrokerType ' : 2 , ' m_nDirection ' : 48 , ' m_strAccountID ' : ' 2000567 ' , ' m_strAccountKey ' : ' 2____11194____114911____49____2000567____ ' , ' m_strAccountRemark ' : '' , ' m_strBrokerName ' : '' , ' m_strMoneyType ' : '' , ' m_strOpenDate ' : '' , ' m_strStatus ' : ' 准备登录 ' , ' m_strTradingDate ' : ' 20240220 ' }
# task_callback - 账号任务状态变化主推
提示
仅在实盘运行模式下生效。
需要先在init里调用ContextInfo.set_account后生效。
用法: task_callback(ContextInfo, taskInfo)
释义: 当账号任务状态有变化时,这个函数被客户端调用
参数:
ContextInfo:特定对象
taskInfo 任务对象
在新窗口打开
返回: 无
示例:
示例 返回值
#coding:gbk
def show_data ( data ):
tdata = {}
for ar in dir (data):
if ar[ : 2 ] != ' m_ ' : continue
try :
tdata[ar] = data. __getattribute__ (ar)
except :
tdata[ar] = ' <CanNotConvert> '
return tdata
def init ( ContextInfo ):
# 设置对应的资金账号
# 示例需要在策略交易界面运行
ContextInfo.set_account(account)
def after_init ( ContextInfo ):
# 在策略交易界面运行时,account的值会被赋值为策略配置中的账号,编辑器界面运行时,需要手动赋值
# 编译器界面里执行的下单函数不会产生实际委托
passorder( 23 , 1101 , account, " 000001.SZ " , 5 , 0 , 100 , " 示例 " , 2 , " 投资备注 " ,ContextInfo)
pass
def task_callback ( ContextInfo , taskInfo ):
print (show_data(taskInfo))
{ ' m_3rdPartyTradeParam ' : '' , ' m_cancelTime ' : 2147483647 , ' m_dFixPrice ' : 9.82 , ' m_eOperationType ' : 18 , ' m_eOrderType ' : 0 , ' m_ePriceType ' : 5 , ' m_eStatus ' : 3 , ' m_endTime ' : 2147483647 , ' m_nBusinessNum ' : 0 , ' m_nGroupId ' : 11 , ' m_nNum ' : 100 , ' m_nTaskId ' : ' 11 ' , ' m_script ' : '' , ' m_startTime ' : 1708420476 , ' m_stockCode ' : ' 000001.SZ ' , ' m_strAccountID ' : ' 2000567 ' , ' m_strMsg ' : ' 9.8200全部委托! 报价行情已8076秒未更新! ' , ' m_strRemark ' : ' 投资备注 ' }
{ ' m_3rdPartyTradeParam ' : '' , ' m_cancelTime ' : 2147483647 , ' m_dFixPrice ' : 9.82 , ' m_eOperationType ' : 18 , ' m_eOrderType ' : 0 , ' m_ePriceType ' : 5 , ' m_eStatus ' : 3 , ' m_endTime ' : 2147483647 , ' m_nBusinessNum ' : 100 , ' m_nGroupId ' : 11 , ' m_nNum ' : 100 , ' m_nTaskId ' : ' 11 ' , ' m_script ' : '' , ' m_startTime ' : 1708420476 , ' m_stockCode ' : ' 000001.SZ ' , ' m_strAccountID ' : ' 2000567 ' , ' m_strMsg ' : ' 9.8200全部委托! 报价行情已8076秒未更新! ' , ' m_strRemark ' : ' 投资备注 ' }
{ ' m_3rdPartyTradeParam ' : '' , ' m_cancelTime ' : 2147483647 , ' m_dFixPrice ' : 9.82 , ' m_eOperationType ' : 18 , ' m_eOrderType ' : 0 , ' m_ePriceType ' : 5 , ' m_eStatus ' : 7 , ' m_endTime ' : 1708420476 , ' m_nBusinessNum ' : 100 , ' m_nGroupId ' : 11 , ' m_nNum ' : 100 , ' m_nTaskId ' : ' 11 ' , ' m_script ' : '' , ' m_startTime ' : 1708420476 , ' m_stockCode ' : ' 000001.SZ ' , ' m_strAccountID ' : ' 2000567 ' , ' m_strMsg ' : ' 任务完成 ' , ' m_strRemark ' : ' 投资备注 ' }
{ ' m_3rdPartyTradeParam ' : '' , ' m_cancelTime ' : 2147483647 , ' m_dFixPrice ' : 9.82 , ' m_eOperationType ' : 18 , ' m_eOrderType ' : 0 , ' m_ePriceType ' : 5 , ' m_eStatus ' : 7 , ' m_endTime ' : 1708420476 , ' m_nBusinessNum ' : 100 , ' m_nGroupId ' : 11 , ' m_nNum ' : 100 , ' m_nTaskId ' : ' 11 ' , ' m_script ' : '' , ' m_startTime ' : 1708420476 , ' m_stockCode ' : ' 000001.SZ ' , ' m_strAccountID ' : ' 2000567 ' , ' m_strMsg ' : ' 任务完成 ' , ' m_strRemark ' : ' 投资备注 ' }
# order_callback - 账号委托状态变化主推
提示
仅在实盘运行模式下生效。
需要先在init里调用ContextInfo.set_account后生效。
用法: order_callback(ContextInfo, orderInfo)
释义: 当账号委托状态有变化时,这个函数被客户端调用
参数:
ContextInfo:特定对象
orderInfo 委托
在新窗口打开
返回: 无
示例:
示例 返回值
#coding:gbk
def show_data ( data ):
tdata = {}
for ar in dir (data):
if ar[ : 2 ] != ' m_ ' : continue
try :
tdata[ar] = data. __getattribute__ (ar)
except :
tdata[ar] = ' <CanNotConvert> '
return tdata
def init ( ContextInfo ):
# 设置对应的资金账号
# 示例需要在策略交易界面运行
ContextInfo.set_account(account)
def after_init ( ContextInfo ):
# 在策略交易界面运行时,account的值会被赋值为策略配置中的账号,编辑器界面运行时,需要手动赋值
# 编译器界面里执行的下单函数不会产生实际委托
passorder( 23 , 1101 , account, " 000001.SZ " , 5 , 0 , 100 , " 示例 " , 2 , " 投资备注 " ,ContextInfo)
pass
def order_callback ( ContextInfo , orderInfo ):
print (show_data(orderInfo))
{ ' m_bEnable ' : True , ' m_dCancelAmount ' : 0.0 , ' m_dFrozenCommission ' : 0.21600000000000003 , ' m_dFrozenMargin ' : 1080.0 , ' m_dLimitPrice ' : 10.8 , ' m_dOrderPriceRMB ' : 0.0 , ' m_dReferenceRate ' : 0.0 , ' m_dShortOccupedMargin ' : 1.7976931348623157e+308 , ' m_dTradeAmount ' : 0.0 , ' m_dTradeAmountRMB ' : 0.0 , ' m_dTradedPrice ' : 0.0 , ' m_eCashgroupProp ' : 48 , ' m_eCoveredFlag ' : 0 , ' m_eEntrustType ' : 48 , ' m_nDirection ' : 48 , ' m_nErrorID ' : 2147483647 , ' m_nFrontID ' : - 1 , ' m_nGroupId ' : 2147483647 , ' m_nHedgeFlag ' : 49 , ' m_nOffsetFlag ' : 48 , ' m_nOpType ' : 23 , ' m_nOrderPriceType ' : 50 , ' m_nOrderStatus ' : 50 , ' m_nOrderStrategyType ' : - 946575058 , ' m_nOrderSubmitStatus ' : 51 , ' m_nRef ' : 1745879041 , ' m_nSessionID ' : - 1 , ' m_nStrategyID ' : 0 , ' m_nTaskId ' : 1 , ' m_nVolumeTotal ' : 100 , ' m_nVolumeTotalOriginal ' : 100 , ' m_nVolumeTraded ' : 0 , ' m_strAccountID ' : ' 2000567 ' , ' m_strAccountKey ' : ' 2____11194____114911____49____2000567____ ' , ' m_strAccountName ' : '' , ' m_strAccountRemark ' : '' , ' m_strBrokerName ' : '' , ' m_strCancelInfo ' : '' , ' m_strCompactNo ' : '' , ' m_strErrorMsg ' : '' , ' m_strExchangeID ' : ' SZ ' , ' m_strExchangeName ' : ' 深交所 ' , ' m_strInsertDate ' : ' 20240222 ' , ' m_strInsertTime ' : ' 091259 ' , ' m_strInstrumentID ' : ' 000001 ' , ' m_strInstrumentName ' : ' 平安银行 ' , ' m_strLocalInfo ' : '' , ' m_strOptName ' : ' 限价买入 ' , ' m_strOption ' : '' , ' m_strOrderParam ' : '' , ' m_strOrderRef ' : ' 8875341038780543374 ' , ' m_strOrderStrategyType ' : ' 函数下单 ' , ' m_strOrderSysID ' : ' 87 ' , ' m_strProductID ' : '' , ' m_strProductName ' : '' , ' m_strRemark ' : ' 投资备注 ' , ' m_strSource ' : ' 新建策略文件15 ' , ' m_strUnderCode ' : '' , ' m_strXTTrade ' : ' 本终端 ' , ' m_xtTag ' : ' <CanNotConvert> ' }
# deal_callback - 账号成交状态变化主推
提示
仅在实盘运行模式下生效。
需要先在init里调用ContextInfo.set_account后生效。
用法: deal_callback(ContextInfo, dealInfo)
释义: 当账号成交状态有变化时,这个函数被客户端调用
参数:
ContextInfo:特定对象
dealInfo 成交
在新窗口打开
返回: 无
示例:
示例 返回值
#coding:gbk
def show_data ( data ):
tdata = {}
for ar in dir (data):
if ar[ : 2 ] != ' m_ ' : continue
try :
tdata[ar] = data. __getattribute__ (ar)
except :
tdata[ar] = ' <CanNotConvert> '
return tdata
def init ( ContextInfo ):
# 设置对应的资金账号
# 示例需要在策略交易界面运行
ContextInfo.set_account(account)
def after_init ( ContextInfo ):
# 在策略交易界面运行时,account的值会被赋值为策略配置中的账号,编辑器界面运行时,需要手动赋值
# 编译器界面里执行的下单函数不会产生实际委托
passorder( 23 , 1101 , account, " 000001.SZ " , 5 , 0 , 100 , " 示例 " , 2 , " 投资备注 " ,ContextInfo)
pass
def deal_callback ( ContextInfo , dealInfo ):
print (show_data(dealInfo))
{ ' m_dCloseProfit ' : 0.0 , ' m_dComssion ' : 0.19640000000000002 , ' m_dOrderPriceRMB ' : 0.0 , ' m_dPrice ' : 9.82 , ' m_dPriceRMB ' : 0.0 , ' m_dReferenceRate ' : 0.0 , ' m_dTradeAmount ' : 982.0 , ' m_dTradeAmountRMB ' : 0.0 , ' m_eCoveredFlag ' : 48 , ' m_eEntrustType ' : 48 , ' m_eFutureTradeType ' : 48 , ' m_nCloseTodayVolume ' : 0 , ' m_nDirection ' : 48 , ' m_nGroupId ' : 2147483647 , ' m_nHedgeFlag ' : 49 , ' m_nOffsetFlag ' : 48 , ' m_nOrderPriceType ' : 50 , ' m_nOrderStrategyType ' : 0 , ' m_nRealOffsetFlag ' : - 1 , ' m_nRef ' : 1209008141 , ' m_nStrategyID ' : 0 , ' m_nTaskId ' : 14 , ' m_nVolume ' : 100 , ' m_strAccountID ' : ' 2000567 ' , ' m_strAccountKey ' : ' 2____11194____114911____49____2000567____ ' , ' m_strAccountRemark ' : '' , ' m_strCompactNo ' : '' , ' m_strExchangeID ' : ' SZ ' , ' m_strExchangeName ' : ' 深交所 ' , ' m_strInstrumentID ' : ' 000001 ' , ' m_strInstrumentName ' : ' 平安银行 ' , ' m_strLocalInfo ' : '' , ' m_strOperation ' : '' , ' m_strOptName ' : ' 限价买入 ' , ' m_strOrderRef ' : ' 8875341038780443523 ' , ' m_strOrderStrategyType ' : ' 函数下单 ' , ' m_strOrderSysID ' : ' 24500 ' , ' m_strProductID ' : '' , ' m_strProductName ' : '' , ' m_strRemark ' : ' 投资备注 ' , ' m_strSource ' : ' 新建策略文件15 ' , ' m_strTradeDate ' : ' 20240220 ' , ' m_strTradeID ' : ' 13 ' , ' m_strTradeTime ' : ' 172341 ' , ' m_strXTTrade ' : ' 本终端 ' , ' m_xtTag ' : ' <CanNotConvert> ' }
# position_callback - 账号持仓状态变化主推
提示
仅在实盘运行模式下生效。
需要先在init里调用ContextInfo.set_account后生效。
用法: position_callback(ContextInfo, positonInfo)
释义: 当账号持仓状态有变化时,这个函数被客户端调用
参数:
ContextInfo:特定对象
positonInfo 持仓
在新窗口打开
返回: 无
示例:
示例 返回值
#coding:gbk
def show_data ( data ):
tdata = {}
for ar in dir (data):
if ar[ : 2 ] != ' m_ ' : continue
try :
tdata[ar] = data. __getattribute__ (ar)
except :
tdata[ar] = ' <CanNotConvert> '
return tdata
def init ( ContextInfo ):
# 设置对应的资金账号
# 示例需要在策略交易界面运行
ContextInfo.set_account(account)
def after_init ( ContextInfo ):
# 在策略交易界面运行时,account的值会被赋值为策略配置中的账号,编辑器界面运行时,需要手动赋值
# 编译器界面里执行的下单函数不会产生实际委托
passorder( 23 , 1101 , account, " 000001.SZ " , 5 , 0 , 100 , " 示例 " , 2 , " 投资备注 " ,ContextInfo)
pass
def position_callback ( ContextInfo , positionInfo ):
print (show_data(positionInfo))
{ ' m_bIsToday ' : True , ' m_dAvgOpenPrice ' : 9.417861115 , ' m_dCloseAmount ' : 0.0 , ' m_dCloseProfit ' : 0.0 , ' m_dFloatProfit ' : 11.999999999999744 , ' m_dInstrumentValue ' : 19640.0 , ' m_dLastPrice ' : 9.82 , ' m_dLastSettlementPrice ' : 0.0 , ' m_dMargin ' : 0.0 , ' m_dMarketValue ' : 19640.0 , ' m_dOpenCost ' : 18835.72223 , ' m_dOpenPrice ' : 9.417861115 , ' m_dPositionCost ' : 18835.72223 , ' m_dPositionProfit ' : 804.2777700000006 , ' m_dProfitRate ' : 0.04269959814543308 , ' m_dRealUsedMargin ' : 0.0 , ' m_dRedemptionVolume ' : 0 , ' m_dReferenceRate ' : 0.0 , ' m_dRoyalty ' : 0.0 , ' m_dSettlementPrice ' : 9.82 , ' m_dSingleCost ' : 2.946 , ' m_dStaticHoldMargin ' : 1.7976931348623157e+308 , ' m_dStockLastPrice ' : 1.7976931348623157e+308 , ' m_dStructFundVol ' : 0 , ' m_dTotalCost ' : 5892.0 , ' m_eFutureTradeType ' : 48 , ' m_eSideFlag ' : 48 , ' m_nCanUseVolume ' : 1200 , ' m_nCidIncrease ' : 1953394499 , ' m_nCidIsDelist ' : 678126433 , ' m_nCidRateOfCurrentLine ' : 1667199589 , ' m_nCidRateOfTotalValue ' : 1651272801 , ' m_nCloseVolume ' : 0 , ' m_nCoveredVolume ' : 0 , ' m_nDirection ' : 48 , ' m_nEnableExerciseVolume ' : - 1 , ' m_nFrozenVolume ' : 0 , ' m_nHedgeFlag ' : 49 , ' m_nLegId ' : 0 , ' m_nOnRoadVolume ' : 800 , ' m_nOptCombUsedVolume ' : 0 , ' m_nPREnableVolume ' : 2000 , ' m_nSettledAmt ' : 0 , ' m_nStrategyID ' : 0 , ' m_nVolume ' : 2000 , ' m_nYesterdayVolume ' : 1200 , ' m_strAccountID ' : ' 2000567 ' , ' m_strAccountKey ' : ' 2____11194____114911____49____2000567____ ' , ' m_strComTradeID ' : '' , ' m_strExchangeID ' : ' SZ ' , ' m_strExchangeName ' : ' 深交所 ' , ' m_strExpireDate ' : '' , ' m_strInstrumentID ' : ' 000001 ' , ' m_strInstrumentName ' : ' 平安银行 ' , ' m_strOpenDate ' : '' , ' m_strProductID ' : '' , ' m_strProductName ' : '' , ' m_strStockHolder ' : '' , ' m_strTradeID ' : '' , ' m_strTradingDay ' : ' 20240220 ' , ' m_xtTag ' : None }
# orderError_callback - 账号异常下单主推
提示
仅在实盘运行模式下生效。
需要先在init里调用ContextInfo.set_account后生效。
用法: orderError_callback(ContextInfo,orderArgs,errMsg)
释义: 当账号下单异常时,这个函数被客户端调用
参数:
ContextInfo:特定对象
orderArgs 下单参数
在新窗口打开
errMsg:错误信息
返回: 无
示例:
示例 返回值
#coding:gbk
def show_data ( data ):
tdata = {}
for ar in dir (data):
if ar[ : 2 ] != ' m_ ' : continue
try :
tdata[ar] = data. __getattribute__ (ar)
except :
tdata[ar] = ' <CanNotConvert> '
return tdata
def init ( ContextInfo ):
# 设置对应的资金账号
# 示例需要在策略交易界面运行
ContextInfo.set_account(account)
def after_init ( ContextInfo ):
# 在策略交易界面运行时,account的值会被赋值为策略配置中的账号,编辑器界面运行时,需要手动赋值
# 编译器界面里执行的下单函数不会产生实际委托
passorder( 23 , 1101 , account, " 000001.SZ " , 11 , 0 , 100 , " 示例 " , 2 , " 投资备注 " ,ContextInfo)
pass
def orderError_callback ( ContextInfo , orderArgs , errMsg ):
print (show_data(orderArgs))
print (errMsg)
{ ' accountID ' : ' 2000567 ' , ' currentTime ' : 0 , ' formulaName ' : '' , ' modelPrice ' : 0.0 , ' modelVolume ' : 100.0 , ' opType ' : 23 , ' orderCode ' : ' SZ000001 ' , ' orderType ' : 1101 , ' prType ' : 11 , ' strategyName ' : ' 示例_&&&_投资备注 ' }
[函数交易] 函数: passorder, 证券 [ SZ000001 ] 指定价 无效, 无法下单!
# 其他主推函数
# credit_account_callback - 查询信用账户明细回调
用法: credit_account_callback(ContextInfo,seq,result)
释义: 查询信用账户明细回调
参数:
ContextInfo:策略模型全局对象
seq:query_credit_account时输入查询seq
result: 信用账户明细
在新窗口打开
# credit_opvolume_callback - 查询两融最大可下单量的回调
用法: credit_opvolume_callback(ContextInfo,accid,seq,ret,result)
释义: 查询两融最大可下单量的回调。
参数:
ContextInfo :策略模型全局对象
accid :查询的账号
seq : query_credit_opvolume 时输入查询 seq
ret :查询结果状态。正常返回: 1 ,正在查询中 -1 ,输入账号非法: -2 ,输入查询参数非法: -3 ,超时等服务器返回报错: -4
result :查询到的结果
示例 见 query_credit_opvolume
上次更新:
邀请注册送VIP优惠券
分享下方的内容给好友、QQ群、微信群,好友注册您即可获得VIP优惠券
玩转qmt,上迅投qmt知识库
登录后获取
交易函数 引用函数
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -0,0 +1,934 @@
数据结构 | 迅投知识库
迅投知识库
首页
API API
投研新手教程
QMT新手教程
内置Python
XtQuant文档
VBA
数据字典 数据字典
快速开始
股票数据
行业概念数据
指数数据
期货数据
期权数据
场内基金数据
债券数据
常见问题
场景化示例
迅投因子
数据浏览器
策略迁移 策略迁移
聚宽策略
客服QQ: 810315303
联系方式: 18309226715
视频教程 视频教程
投研端教程
迅投学堂
在新窗口打开
投研平台
在新窗口打开
迅投社区
在新窗口打开
迅投官网
在新窗口打开
注册
登录
智能助手加载中...
首页
API API
投研新手教程
QMT新手教程
内置Python
XtQuant文档
VBA
数据字典 数据字典
快速开始
股票数据
行业概念数据
指数数据
期货数据
期权数据
场内基金数据
债券数据
常见问题
场景化示例
迅投因子
数据浏览器
策略迁移 策略迁移
聚宽策略
客服QQ: 810315303
联系方式: 18309226715
视频教程 视频教程
投研端教程
迅投学堂
在新窗口打开
投研平台
在新窗口打开
迅投社区
在新窗口打开
迅投官网
在新窗口打开
内置Python
快速开始
使用须知
界面操作
变量约定
数据结构
数据类
Tick - Tick 对象
get_market_data_ex/get_full_tick返回对象:
get_market_data返回对象:
subscribe_quote/subscribe_whole_quote回调对象:
Bar - Bar对象
l2quote - Level2行情快照
l2quoteaux - Level2行情快照补充
l2order - Level2逐笔委托
l2transaction - Level2逐笔成交
l2transactioncount - Level2逐笔成交统计
l2orderqueue - Level2委买委卖队列
交易类
Account - 账户对象
Order - 委托对象
Deal - 成交对象
Position - 持仓对象
PositionStatistics - 持仓统计对象
CCreditAccountDetail - 信用账号对象(非查柜台)
CCreditDetail - 两融资金信息(查柜台)
CreditSloEnableAmount - 可融券明细对象
StkCompacts - 负债合约对象
StkSubjects - 担保标的对象
PassorderArguments - 下单函数参数对象
CTaskDetail - 任务对象
CLockPosition - 期权标的持仓
CStkOptCombPositionDetail - 期权组合持仓
entrustType - 委托类型
openInt - 证券状态(股票)
openInt - 证券状态(期货)
系统函数
行情函数
交易函数
成交回报实时主推函数
引用函数
绘图函数
枚举常量
完整示例
常见问题
# 数据类
# Tick - Tick 对象
行情快照数据
# get_market_data_ex/get_full_tick返回对象:
字段名 数据类型 含义
time int 时间戳
stime string 时间戳字符串形式
lastPrice float 最新价
open float 开盘价
high float 最高价
low float 最低价
lastClose float 前收盘价
amount float 成交总额
volume int 成交总量(手)
pvolume int 原始成交总量(未经过股手转换的成交总量)【不推荐使用】
stockStatus int 证券状态
openInt int 若是股票,则openInt含义为股票状态,非股票则是持仓量 openInt字段说明
在新窗口打开
transactionNum float 成交笔数(期货没有,单独计算)
lastSettlementPrice float 前结算(股票为0)
settlementPrice float 今结算(股票为0)
askPrice list[float] 多档委卖价
askVol list[int] 多档委卖量
bidPrice list[float] 多档委买价
bidVol list[int] 多档委买量
# get_market_data返回对象:
字段 数据类型 含义
timetag string 时间戳,格式为: %Y%m%d %H:%M:%S
lastPrice float 最新价
open float 开盘价
high float 最高价
low float 最低价
lastClose float 前收盘价
amount float 成交额
volume float 成交量(手)
pvolume float 原始成交量(股)【不推荐使用】
stockStatus int 作废 参考openInt
openInt float 若是股票,则openInt含义为股票状态,非股票则是持仓量 openInt字段说明
lastSettlementPrice float 昨结算价
pe float 对于股票是市盈率,对于ETF是iopv值
askPrice list 委卖价
bidPrice list 委买价
askVol list 委卖量
bidVol list 委买量
settlementPrice float 今结算价
# subscribe_quote/subscribe_whole_quote回调对象:
同 get_full_tick 返回结构
# Bar - Bar对象
bar数据是指各种频率的行情数据
字段 数据类型 含义
time int 时间
open float 开盘价
high float 最高价
low float 最低价
close float 收盘价
volume float 成交量
amount float 成交额
settelementPrice float 今结算
openInterest float 持仓量
preClose float 前收盘价
suspendFlag int 停牌  1停牌,0 不停牌
# l2quote - Level2行情快照
字段名 数据类型 解释
time int 时间戳
stime string 时间戳字符串形式
lastPrice float 最新价
open float 开盘价
high float 最高价
low float 最低价
amount float 成交额
volume int 成交总量
pvolume int 原始成交总量(未经过股手转换的成交总量)
stockStatus int 证券状态
openInt int 持仓量
transactionNum int 成交笔数(期货没有,单独计算)
lastClose float 前收盘价
lastSettlementPrice float 前结算(股票为0)
settlementPrice float 今结算(股票为0)
askPrice list[float] 多档委卖价
askVol list[int] 多档委卖量
bidPrice list[float] 多档委买价
bidVol list[int] 多档委买量
# l2quoteaux - Level2行情快照补充
字段名 数据类型 解释
time int 时间戳
stime string 时间戳字符串形式
avgBidPrice float 委买均价
totalBidQuantity int 委买总量
avgOffPrice float 委卖均价
totalOffQuantity int 委卖总量
withdrawBidQuantity int 买入撤单总量
withdrawBidAmount float 买入撤单总额
withdrawOffQuantity int 卖出撤单总量
withdrawOffAmount float 卖出撤单总额
# l2order - Level2逐笔委托
字段名 数据类型 解释
time int 时间戳
stime float 时间戳浮点数形式
price float 委托价
volume int 委托量
entrustNo int 委托号
entrustType int 委托类型
entrustDirection int 委托方向
提示
注:上交所的撤单信息在逐笔委托的委托方向,区分撤买撤卖
0 - 未知
1 - 买入
2 - 卖出
3 - 撤买(上交所)
4 - 撤卖(上交所)
# l2transaction - Level2逐笔成交
字段名 数据类型 解释
time int 时间戳
stime string 时间戳字符串形式
price float 成交价
volume int 成交量
amount float 成交额
tradeIndex int 成交记录号
buyNo int 买方委托号
sellNo int 卖方委托号
tradeType int 成交类型
tradeFlag int 成交标志
提示
深交所逐笔成交的撤单标志,没有方向
0 - 未知
1 - 外盘,主买
2 - 内盘,主卖
3 - 撤单
# l2transactioncount - Level2逐笔成交统计
字段名 数据类型 解释
time int 时间戳
bidNumber int 主买单总单数
offNumber int 主卖单总单数
ddx float 大单动向
ddy float 涨跌动因
ddz float 大单差分
netOrder int 净挂单量
netWithdraw int 净撤单量
withdrawBid int 总撤买量
withdrawOff int 总撤卖量
bidNumberDx int 主买单总单数增量
offNumberDx int 主卖单总单数增量
transactionNumber int 成交笔数增量
bidMostAmount float 主买特大单成交额
bidBigAmount float 主买大单成交额
bidMediumAmount float 主买中单成交额
bidSmallAmount float 主买小单成交额
bidTotalAmount float 主买累计成交额
offMostAmount float 主卖特大单成交额
offBigAmount float 主卖大单成交额
offMediumAmount float 主卖中单成交额
offSmallAmount float 主卖小单成交额
offTotalAmount float 主卖累计成交额
unactiveBidMostAmount float 被动买特大单成交额
unactiveBidBigAmount float 被动买大单成交额
unactiveBidMediumAmount float 被动买中单成交额
unactiveBidSmallAmount float 被动买小单成交额
unactiveBidTotalAmount float 被动买累计成交额
unactiveOffMostAmount float 被动卖特大单成交额
unactiveOffBigAmount float 被动卖大单成交额
unactiveOffMediumAmount float 被动卖中单成交额
unactiveOffSmallAmount float 被动卖小单成交额
unactiveOffTotalAmount float 被动卖累计成交额
netInflowMostAmount float 净流入超大单成交额(lv1数据不支持计算,返回为 0,如有需求可咨询高频资金流数据)
netInflowBigAmount float 净流入大单成交额(lv1数据不支持计算,返回为 0,如有需求可咨询高频资金流数据)
netInflowMediumAmount float 净流入中单成交额(lv1数据不支持计算,返回为 0,如有需求可咨询高频资金流数据)
netInflowSmallAmount float 净流入小单成交额(lv1数据不支持计算,返回为 0,如有需求可咨询高频资金流数据)
bidMostVolume int 主买特大单成交量
bidBigVolume int 主买大单成交量
bidMediumVolume int 主买中单成交量
bidSmallVolume int 主买小单成交量
bidTotalVolume int 主买累计成交量
offMostVolume int 主卖特大单成交量
offBigVolume int 主卖大单成交量
offMediumVolume int 主卖中单成交量
offSmallVolume int 主卖小单成交量
offTotalVolume int 主卖累计成交量
unactiveBidMostVolume int 被动买特大单成交量
unactiveBidBigVolume int 被动买大单成交量
unactiveBidMediumVolume int 被动买中单成交量
unactiveBidSmallVolume int 被动买小单成交量
unactiveBidTotalVolume int 被动买累计成交量
unactiveOffMostVolume int 被动卖特大单成交量
unactiveOffBigVolume int 被动卖大单成交量
unactiveOffMediumVolume int 被动卖中单成交量
unactiveOffSmallVolume int 被动卖小单成交量
unactiveOffTotalVolume int 被动卖累计成交量
netInflowMostVolume int 净流入超大单成交量
netInflowBigVolume int 净流入大单成交量
netInflowMediumVolume int 净流入中单成交量
netInflowSmallVolume int 净流入小单成交量
bidMostAmountDx float 主买特大单成交额增量
bidBigAmountDx float 主买大单成交额增量
bidMediumAmountDx float 主买中单成交额增量
bidSmallAmountDx float 主买小单成交额增量
bidTotalAmountDx float 主买累计成交额增量
offMostAmountDx float 主卖特大单成交额增量
offBigAmountDx float 主卖大单成交额增量
offMediumAmountDx float 主卖中单成交额增量
offSmallAmountDx float 主卖小单成交额增量
offTotalAmountDx float 主卖累计成交额增量
unactiveBidMostAmountDx float 被动买特大单成交额增量
unactiveBidBigAmountDx float 被动买大单成交额增量
unactiveBidMediumAmountDx float 被动买中单成交额增量
unactiveBidSmallAmountDx float 被动买小单成交额增量
unactiveBidTotalAmountDx float 被动买累计成交额增量
unactiveOffMostAmountDx float 被动卖特大单成交额增量
unactiveOffBigAmountDx float 被动卖大单成交额增量
unactiveOffMediumAmountDx float 被动卖中单成交额增量
unactiveOffSmallAmountDx float 被动卖小单成交额增量
unactiveOffTotalAmountDx float 被动卖累计成交额增量
netInflowMostAmountDx float 净流入超大单成交额增量
netInflowBigAmountDx float 净流入大单成交额增量
netInflowMediumAmountDx float 净流入中单成交额增量
netInflowSmallAmountDx float 净流入小单成交额增量
bidMostVolumeDx int 主买特大单成交量增量
bidBigVolumeDx int 主买大单成交量增量
bidMediumVolumeDx int 主买中单成交量增量
bidSmallVolumeDx int 主买小单成交量增量
bidTotalVolumeDx int 主买累计成交量增量
offMostVolumeDx int 主卖特大单成交量增量
offBigVolumeDx int 主卖大单成交量增量
offMediumVolumeDx int 主卖中单成交量增量
offSmallVolumeDx int 主卖小单成交量增量
offTotalVolumeDx int 主卖累计成交量增量
unactiveBidMostVolumeDx int 被动买特大单成交量增量
unactiveBidBigVolumeDx int 被动买大单成交量增量
unactiveBidMediumVolumeDx int 被动买中单成交量增量
unactiveBidSmallVolumeDx int 被动买小单成交量增量
unactiveBidTotalVolumeDx int 被动买累计成交量增量
unactiveOffMostVolumeDx int 被动卖特大单成交量增量
unactiveOffBigVolumeDx int 被动卖大单成交量增量
unactiveOffMediumVolumeDx int 被动卖中单成交量增量
unactiveOffSmallVolumeDx int 被动卖小单成交量增量
unactiveOffTotalVolumeDx int 被动卖累计成交量增量
netInflowMostVolumeDx int 净流入超大单成交量增量
netInflowBigVolumeDx int 净流入大单成交量增量
netInflowMediumVolumeDx int 净流入中单成交量增量
netInflowSmallVolumeDx int 净流入小单成交量增量
# l2orderqueue - Level2委买委卖队列
# 交易类
# Account - 账户对象
字段名 数据类型 解释
m_strAccountID str 资金账号,用于识别不同的资金账户
m_nBrokerType int 账号类型,表示账号的具体种类
m_dMaxMarginRate float 保证金比率,通常用于期货账号
m_dFrozenMargin float 冻结保证金,指投资者在交易中被冻结的保证金金额
m_dFrozenCash float 冻结金额,指投资者在交易中被冻结的资金金额
m_dFrozenCommission float 冻结手续费,指投资者在交易中被冻结的手续费金额
m_dRisk float 风险度,指投资者账户的风险程度
m_dNav float 单位净值,用于表示基金的净值
m_dPreBalance float 期初权益,指期初时账户的资金金额
m_dBalance float 总资产,表示账户的总资金金额
m_dAvailable float 可用金额,指账户中可用于交易和提取的资金金额
m_dCommission float 手续费 (旧版本为 m_dComission)
m_dPositionProfit float 持仓盈亏,指当前持有的证券或期货合约的盈亏金额
m_dCloseProfit float 平仓盈亏,在期货交易中表示已经平仓的交易的盈亏金额
m_dCashIn float 出入金净值,表示账户中出入金的净额
m_dCurrMargin float 当前使用的保证金金额
m_dInitBalance float 初始权益,指账户初始时的权益金额
m_strStatus str 状态,表示账户的当前状态
m_dInitCloseMoney float 期初平仓盈亏,指账户初始时的平仓盈亏金额
m_dInstrumentValue float 总市值,表示持有的证券或期货合约的总市值
m_dDeposit float 入金,指账户中的入金金额
m_dWithdraw float 出金,指账户中的出金金额
m_dPreCredit float 上次信用额度,用于表示上次的信用额度
m_dPreMortgage float 上次质押,指上次的质押金额
m_dMortgage float 质押,指当前的质押金额
m_dCredit float 信用额度,表示账户的信用额度
m_dAssetBalance float 证券初始资金,表示股票账户的初始资金
m_strOpenDate str 起始日期,表示账户的起始日期
m_dFetchBalance float 可取金额,指账户中可取出的金额
m_strTradingDate str 交易日,表示当前的交易日期
m_dStockValue float 股票总市值,表示股票账户中持有的股票的总市值
m_dLoanValue float 债券总市值,表示账户中持有的债券的总市值
m_dFundValue float 基金总市值,包括ETF和封闭式基金在内的基金的总市值
m_dRepurchaseValue float 回购总市值,表示账户中持有的所有回购交易的总市值
m_dLongValue float 多单总市值,指现货账户中多单持仓的总市值
m_dShortValue float 空单总市值,指现货账户中空单持仓的总市值
m_dNetValue float 净持仓总市值,指现货账户中多单总市值减去空单总市值的差额
m_dAssureAsset float 净资产,表示账户的净资产金额
m_dTotalDebit float 总负债,表示账户的总负债金额
m_dEntrustAsset float 可信资产,用于校对账户资金的准确性
m_dInstrumentValueRMB float 总市值(人民币),指沪港通账户中的持仓证券的总市值
m_dSubscribeFee float 申购费,指申购基金时支付的费用
m_dGoldValue float 库存市值,表示黄金现货账户中黄金库存的市值
m_dGoldFrozen float 现货冻结,表示黄金现货账户中被冻结的黄金金额
m_dMargin float 占用保证金,用于维持保证金
m_strMoneyType str 币种,表示账户的资金所使用的货币种类
m_dPurchasingPower float 购买力,指账户可用于购买投资品的金额
m_dRawMargin float 原始保证金,指期货账户中的原始保证金金额
m_dBuyWaitMoney float 买入待交收金额(元),指账户中买入股票但尚未交收的金额
m_dSellWaitMoney float 卖出待交收金额(元),指账户中卖出股票但尚未交收的金额
m_dReceiveInterestTotal float 本期间应计利息,指账户本期间内应计的利息金额
m_dRoyalty float 权利金收支,指期货期权交易中的权利金收支金额
m_dFrozenRoyalty float 冻结权利金,指期货期权交易中被冻结的权利金金额
m_dRealUsedMargin float 实时占用保证金,用于股票期权交易中表示实时占用的保证金金额
m_dRealRiskDegree float 实时风险度,用于股票期权交易中表示实时的风险度
# Order - 委托对象
字段 数据类型 解释
m_strAccountID str 资金账号,账号,账号,资金账号
m_strExchangeID str 证券市场
m_strExchangeName str 交易市场
m_strProductID str 品种代码
m_strProductName str 品种名称
m_strInstrumentID str 证券代码
m_strInstrumentName str 证券名称,合约名称
m_nRef int 订单编号
m_strOrderRef str 内部委托号,下单引用等于股票的内部委托号
m_nOrderPriceType int EBrokerPriceType 类型,例如市价单、限价单
在新窗口打开
m_nDirection int EEntrustBS 类型,操作,多空,期货多空,股票买卖永远是 48,其他的 dir 同理
m_nOffsetFlag int EOffset_Flag_Type类型,买卖/开平,用此字段区分股票买卖,期货开、平仓,期权买卖等
m_nHedgeFlag int EHedge_Flag_Type 类型,投保
m_dLimitPrice float 委托价格,限价单的限价,即报价
m_nVolumeTotalOriginal int 委托数量,最初的委托数量
m_nOrderSubmitStatus int EEntrustSubmitStatus 类型,报单状态,提交状态,股票中不需要报单状态
m_strOrderSysID str 合同编号,委托号
m_nOrderStatus int EEntrustStatus,委托状态
m_nVolumeTraded int 成交数量,已成交量
m_nVolumeTotal int 委托剩余量,当前总委托量,股票中表示总委托量减去成交量
m_nErrorID int 状态ID
m_strErrorMsg str 状态信息
m_nTaskId int 任务号
m_dFrozenMargin float 冻结金额,冻结保证金
m_dFrozenCommission float 冻结手续费
m_strInsertDate str 委托日期,报单日期
m_strInsertTime str 委托时间
m_dTradedPrice float 成交均价(股票)
m_dCancelAmount float 已撤数量
m_strOptName str 买卖标记,展示委托属性的中文
m_dTradeAmount float 成交金额,期货的计算方式为均价乘以数量乘以合约乘数
m_eEntrustType int EEntrustTypes,委托类别
m_strCancelInfo str 废单原因
m_strUnderCode str 标的证券代码
m_eCoveredFlag int 备兑标记,'0’表示非备兑,'1’表示备兑
m_dOrderPriceRMB float 委托价格(人民币),目前用于港股通
m_dTradeAmountRMB float 成交金额(人民币),目前用于港股通
m_dReferenceRate float 汇率,目前用于港股通
m_strCompactNo str 合约编号
m_eCashgroupProp int EXTCompactBrushSource类型,头寸来源
m_dShortOccupedMargin float 预估在途占用保证金,用于期权
m_strXTTrade str 是否是迅投交易
m_strAccountKey str 账号key,唯一区别不同账号的key
m_strRemark str 投资备注
# Deal - 成交对象
字段 数据类型 解释
m_strAccountID str 资金账号
m_strExchangeID str 证券市场
m_strExchangeName str 交易市场
m_strProductID str 品种代码
m_strProductName str 品种名称
m_strInstrumentID str 证券代码
m_strInstrumentName str 证券名称
m_strTradeID str 成交编号
m_strOrderRef str 下单引用,等于股票的内部委托号
m_strOrderSysID str 合同编号,报单编号,委托号
m_nDirection int EEntrustBS,买卖方向 对于股票该值始终是48
在新窗口打开
m_nOffsetFlag int EOffset_Flag_Type,买卖/开平,用此字段区分股票买卖,期货开、平仓,期权买卖等
在新窗口打开
m_nHedgeFlag int EHedge_Flag_Type 类型,投保
在新窗口打开
m_dPrice float 成交均价
m_nVolume int 成交量,期货单位手,股票做到股
m_strTradeDate str 成交日期
m_strTradeTime str 成交时间
m_dCommission float 手续费 (旧版本为 m_dComission )
m_dTradeAmount float 成交额,期货 = 均价 * 量 * 合约乘数
m_nTaskId int 任务号
m_nOrderPriceType int EBrokerPriceType 类型,例如市价单、限价单
在新窗口打开
m_strOptName str 买卖标记,展示委托属性的中文
m_eEntrustType int EEntrustTypes,委托类别
在新窗口打开
m_eFutureTradeType int EFutureTradeType 类型,成交类型
在新窗口打开
m_nRealOffsetFlag int EOffset_Flag_Type 类型,实际开平,主要是区分平今和平昨
在新窗口打开
m_eCoveredFlag int ECoveredFlag类型,备兑标记 '0' - 非备兑,'1' - 备兑
m_nCloseTodayVolume int 平今量,不显示
m_dOrderPriceRMB float 委托价格(人民币),目前用于港股通
m_dPriceRMB float 成交价格(人民币),目前用于港股通
m_dTradeAmountRMB float 成交金额(人民币),目前用于港股通
m_dReferenceRate float 汇率,目前用于港股通
m_strXTTrade str 是否是迅投交易
m_strCompactNo str 合约编号
m_dCloseProfit float 平仓盈亏,目前用于外盘
m_strRemark str 投资备注
m_strAccountKey str 账号key,唯一区别不同账号的key
m_nRef int 订单编号
# Position - 持仓对象
字段名 数据类型 含义
m_strAccountID string 资金账号
m_strExchangeID string 证券市场
m_strExchangeName string 市场名称
m_strProductID string 品种代码
m_strProductName string 品种名称
m_strInstrumentID string 证券代码
m_strInstrumentName string 证券名称
m_nHedgeFlag int EHedge_Flag_Type 类型,投保 ,股票不适用
在新窗口打开
m_nDirection int EEntrustBS,买卖方向 对于股票该值始终是48
在新窗口打开
m_strOpenDate string 开仓日期 股票此字段无效
m_strTradeID string 成交号,最初开仓位的成交
m_nVolume int 当前拥股/持仓量
m_dOpenPrice float 持仓成本 ;持仓成本 = (总买入金额 - 总卖出金额) / 剩余数量
m_strTradingDay string 在实盘运行中是当前交易日,在回测中是股票最后交易过的日期
m_dMargin float 使用的保证金,历史的直接用ctp的,新的自己用成本价 存量 系数算,股票不适用
m_dOpenCost float 开仓成本,等于成本价*第一次建仓的量,后续减持会影响,不算手续费,股票不适用
m_dSettlementPrice float 最新结算价/当前价
m_nCloseVolume int 平仓量(对于股票不适用)
m_dCloseAmount float 平仓额(对于股票不适用)
m_dFloatProfit float 浮动盈亏
m_dCloseProfit float 平仓盈亏(对于股票不适用)
m_dMarketValue float 市值/合约价值
m_dPositionCost float 持仓成本(对于股票不适用)
m_dPositionProfit float 持仓盈亏(对于股票不适用)
m_dLastSettlementPrice float 最新结算价(对于股票不适用)
m_dInstrumentValue float 合约价值(对于股票不适用)
m_bIsToday bool 是否今仓
m_strStockHolder string 股东账号
m_nFrozenVolume int 冻结数量
m_nCanUseVolume int 可用数量
m_nOnRoadVolume int 在途股份
m_nYesterdayVolume int 昨夜拥股
m_dLastPrice float 最新价/当前价
m_dAvgOpenPrice float 开仓均价(对于股票不适用)
m_dProfitRate float 盈亏比例
m_eFutureTradeType int EFutureTradeType 类型,成交类型
在新窗口打开
m_strExpireDate string 到期日(针对逆回购)
m_strComTradeID string 组合成交号
m_nLegId int 组合序号
m_dTotalCost float 累计成本(自定义,股票信用用到)
m_dSingleCost float 单股成本(自定义,股票信用用
m_nCoveredVolume int 备兑数量,用于个股期权
m_eSideFlag int 持仓类型 ,用于个股期权,标记 '0' - 权利,'1' - 义务,'2' - '备兑'
m_dReferenceRate float 汇率,目前用于港股通
m_dStructFundVol float 分级基金可用(可分拆或可合并)
m_dRedemptionVolume float 分级基金可赎回量
m_nPREnableVolume int 申赎可用量(记录当日申购赎回的股票或基金数量)
m_dRealUsedMargin float 实时占用保证金,用于期权
m_dRoyalty float 权利金
m_dStockLastPrice float 标的证券最新价,用于期权
m_dStaticHoldMargin float 静态持仓占用保证金,用于期权
m_nOptCombUsedVolume int 期权组合占用数量
m_nEnableExerciseVolume int 能够行使的数量,用于个股期权
m_strAccountKey string 账号key,唯一区别不同账号的key
# PositionStatistics - 持仓统计对象
字段名 数据类型 描述
m_strAccountID string 账号
m_strExchangeID string 市场代码
m_strExchangeName string 市场名称
m_strProductID string 品种代码
m_strInstrumentID string 合约代码
m_strInstrumentName string 合约名称
m_nDirection int 多空
m_nHedgeFlag int 投保
m_nPosition int 持仓
m_nYestodayPosition int 昨仓
m_nTodayPosition int 今仓
m_nCanCloseVol int 可平
m_dPositionCost float 持仓成本
m_dAvgPrice float 持仓均价
m_dPositionProfit float 持仓盈亏
m_dFloatProfit float 浮动盈亏
m_dOpenPrice float 开仓均价
m_dUsedMargin float 已使用保证金
m_dUsedCommission float 已使用的手续费
m_dFrozenMargin float 冻结保证金
m_dFrozenCommission float 冻结手续费
m_dInstrumentValue float 市值,合约价值
m_nOpenTimes int 开仓次数
m_nOpenVolume int 总开仓量 中间平仓不减
m_nCancelTimes int 撤单次数
m_dLastPrice float 最新价
m_dRiseRatio float 当日涨幅
m_strProductName string 产品名称
m_dRoyalty float 权利金市值
m_strExpireDate string 到期日
m_dAssestWeight float 资产占比
m_dIncreaseBySettlement float 当日涨幅(结)
m_dMarginRatio float 保证金占比
m_dFloatProfitDivideByUsedMargin float 浮盈比例(保证金)
m_dFloatProfitDivideByBalance float 浮盈比例(动态权益)
m_dTodayProfitLoss float 当日盈亏(结)
m_nYestodayInitPosition int 昨日持仓
m_dFrozenRoyalty float 冻结权利金
m_dTodayCloseProfitLoss float 当日盈亏(收)
m_dCloseProfit float 平仓盈亏
m_strFtProductName string 品种名称
m_dOpenCost float 开仓成本
# CCreditAccountDetail - 信用账号对象(非查柜台)
字段名 数据类型 解释
m_strAccountID str 资金账号
m_nBrokerType int 账号类型,1-期货账号,2-股票账号,3-信用账号,5-期货期权账号,6-股票期权账号,7-沪港通账号,11-深港通账号
m_strAccountKey str 唯一区别不同账号的key
m_dMaxMarginRate float 保证金比率,股票的保证金率等于1
m_dFrozenMargin float 冻结保证金,外源性,股票的保证金就是冻结资金,股票不适用
m_dFrozenCash float 冻结金额,内外源冻结保证金和手续费四个的和
m_dFrozenCommission float 冻结手续费,外源性冻结资金源
m_dRisk float 风险度,冻结资金/可用资金
m_dNav float 单位净值
m_dPreBalance float 期初权益,也叫静态权益,股票不适用
m_dBalance float 总资产,动态权益,即市值
m_dAvailable float 可用金额
m_dCommission float 手续费(旧版本为 m_dComission)
m_dPositionProfit float 持仓盈亏
m_dCloseProfit float 平仓盈亏,股票不适用
m_dCashIn float 出入金净值
m_dCurrMargin float 当前使用的保证金,股票不适用
m_dInitBalance float 初始权益
m_strStatus str 状态
m_dInitCloseMoney float 期初平仓盈亏,初始平仓盈亏
m_dInstrumentValue float 总市值,合约价值,合约价值
m_dDeposit float 入金
m_dWithdraw float 出金
m_dPreCredit float 上次信用额度,股票不适用
m_dPreMortgage float 上次质押,股票不适用
m_dMortgage float 质押,股票不适用
m_dCredit float 信用额度,股票不适用
m_dAssetBalance float 证券初始资金,股票不适用
m_strOpenDate str 起始日期股票不适用
m_dFetchBalance float 可取金额
m_strTradingDate str 交易日
m_dStockValue float 股票总市值,期货没有
m_dLoanValue float 债券总市值,期货没有
m_dFundValue float 基金总市值,包括 ETF 和封闭式基金,期货没有
m_dRepurchaseValue float 回购总市值,所有回购,期货没有
m_dLongValue float 多单总市值,现货没有
m_dShortValue float 单总市值,现货没有
m_dNetValue float 净持仓总市值,净持仓市值 = 多 - 空
m_dAssureAsset float 净资产
m_dEntrustAsset float 可信资产,用于校对
m_dInstrumentValueRMB float 总市值(人民币),沪港通
m_dSubscribeFee float 申购费,申购费
m_dGoldValue float 库存市值,黄金现货库存市值
m_dGoldFrozen float 现货冻结,黄金现货冻结
m_dMargin float 占用保证金,维持保证金
m_strMoneyType str 币种
m_dPurchasingPower float 购买力,盈透购买力
m_dRawMargin float 原始保证金
m_dBuyWaitMoney float 买入待交收金额(元),买入待交收
m_dSellWaitMoney float 卖出待交收金额(元),卖出待交收
m_dReceiveInterestTotal float 本期间应计利息
m_dRoyalty float 权利金收支,期货期权用
m_dFrozenRoyalty float 冻结权利金,期货期权用
m_dRealUsedMargin float 实时占用保证金,用于股票期权
m_dRealRiskDegree float 实时风险度
m_dPerAssurescaleValue float 个人维持担保比例
m_dEnableBailBalance float 可用保证金
m_dUsedBailBalance float 已用保证金
m_dAssureEnbuyBalance float 可买担保品资金
m_dFinEnbuyBalance float 可买标的券资金
m_dSloEnrepaidBalance float 可还券资金
m_dFinEnrepaidBalance float 可还款资金
m_dFinMaxQuota float 融资授信额度
m_dFinEnableQuota float 融资可用额度
m_dFinUsedQuota float 融资已用额度
m_dFinUsedBail float 融资已用保证金额
m_dFinCompactBalance float 融资合约金额
m_dFinCompactFare float 融资合约费用
m_dFinCompactInterest float 融资合约利息
m_dFinMarketValue float 融资市值
m_dFinIncome float 融资合约盈亏
m_dSloMaxQuota float 融券授信额度
m_dSloEnableQuota float 融券可用额度
m_dSloUsedQuota float 融券已用额度
m_dSloUsedBail float 融券已用保证金额
m_dSloCompactBalance float 融券合约金额
m_dSloCompactFare float 融券合约费用
m_dSloCompactInterest float 融券合约利息
m_dSloMarketValue float 融券市值
m_dSloIncome float 融券合约盈亏
m_dOtherFare float 其它费用
m_dUnderlyMarketValue float 标的证券市值
m_dFinEnableBalance float 可融资金额
m_dDiffEnableBailBalance float 可用保证金调整值
m_dBuySecuRepayFrozenMargin float 买券还券冻结资金
m_dBuySecuRepayFrozenCommission float 买券还券冻结手续费
m_dSpecialEnableBalance float 专项可融金额
m_dEncumberedAssets float 担保资产
m_dSloSellBalance float 融券卖出资金
m_dDiffAssureEnbuyBalance float 可买担保品资金调整值
m_dDiffFinEnbuyBalance float 可买标的券资金调整值
m_dDiffFinEnrepaidBalance float 可还款资金调整值
m_dOtherRealCompactBalance float 其他负债合约金额
m_dOtherFinCompactInterest float 其他负债合约利息金额
m_dUsedSloSellBalance float 已用融券卖出资金
m_dFetchAssetBalance float 可提出资产总额
m_dTotalEnableQuota float 可用总信用额度
m_dTotalUsedQuota float 已用总信用额度
m_dDebtProfit float 负债总浮盈
m_dDebtLoss float 负债总浮亏
m_nContractEndDate int 合同到期日期
m_dFinDebt float 融资负债
m_dFinProfitAmortized float 融资浮盈折算
m_dSloProfit float 融券浮盈
m_dSloProfitAmortized float 融券浮盈折算
m_dFinLoss float 融资浮亏
m_dSloLoss float 融券浮亏
# CCreditDetail - 两融资金信息(查柜台)
字段名 数据类型 解释
m_dPerAssurescaleValue float 维持担保比例
m_dBalance float 总资产
m_dTotalDebt float 总负债
m_dAssureAsset float 净资产
m_dMarketValue float 总市值
m_dEnableBailBalance float 可用保证金
m_dAvailable float 可用资金
m_dFinDebt float 融资负债
m_dFinDealAvl float 融资本金
m_dFinFee float 融资息费
m_dSloDebt float 融券负债
m_dSloMarketValue float 融券市值
m_dSloFee float 融券息费
m_dOtherFare float 其它费用
m_dFinMaxQuota float 融资授信额度
m_dFinEnableQuota float 融资可用额度
m_dFinUsedQuota float 融资冻结额度
m_dSloMaxQuota float 融券授信额度
m_dSloEnableQuota float 融券可用额度
m_dSloUsedQuota float 融券冻结额度
m_dSloSellBalance float 融券卖出资金
m_dUsedSloSellBalance float 已用融券卖出资金
m_dSurplusSloSellBalance float 剩余融券卖出资金
m_dStockValue float 股票市值
m_dFundValue float 基金市值
error string 错误信息
# CreditSloEnableAmount - 可融券明细对象
提示
由于字段m_dSloRatio、m_dSloStatus提供来源和取担保品明细 get_assure_contract 重复,字段在2021年9月移除,后续用担保品明细接口获取,具体见 担保标的对象字段说明
在新窗口打开
字段名 数据类型 解释
m_nPlatformID int 平台号
m_strBrokerID string 经纪公司编号
m_strBrokerName string 经纪公司
m_strAccountID string 资金账号
m_strExchangeID string 交易所
m_strInstrumentID string 证券代码
m_nEnableAmount int 融券可融数量
m_eQuerySloType enum EXTSloTypeQueryMode
在新窗口打开 ,查询类型
# StkCompacts - 负债合约对象
字段名 数据类型 解释
m_strAccountID string 资金账号,账号,账号,资金账号
m_strExchangeID string 交易所
m_strInstrumentID string 证券代码
m_strExchangeName string 交易所名称
m_strInstrumentName string 股票名称
m_nOpenDate int 合约开仓日期
m_strCompactId string 合约编号
m_dCrdtRatio float 融资融券保证金比例
m_strEntrustNo string 委托编号
m_dEntrustPrice float 委托价格
m_nEntrustVol int 委托数量
m_nBusinessVol int 合约开仓数量
m_dBusinessBalance float 合约开仓金额
m_dBusinessFare float 合约开仓费用
m_eCompactType enum EXTCompactType
在新窗口打开 ,合约类型
m_eCompactStatus enum EXTCompactStatus
在新窗口打开 ,合约状态
m_dRealCompactBalance float 未还合约金额
m_nRealCompactVol int 未还合约数量
m_dRealCompactFare float 未还合约费用
m_dRealCompactInterest float 未还合约利息
m_dRepaidInterest float 已还利息
m_nRepaidVol int 已还数量
m_dRepaidBalance float 已还金额
m_dCompactInterest float 合约总利息
m_dUsedBailBalance float 占用保证金
m_dYearRate float 合约年利率
m_nRetEndDate int 归还截止日
m_strDateClear string 了结日期
m_strPositionStr string 定位串
m_dPrice float 最新价
m_nOpenTime int 合约开仓时间
m_nCancelVol int 合约撤单数量
m_eCashgroupProp enum EXTCompactBrushSource
在新窗口打开 ,头寸来源
m_dUnRepayBalance float 负债金额
m_nRepayPriority int 偿还优先级
m_dRealDefaultInterest float 未还罚息
m_dOtherRealCompactBalance float 其他负债合约金额
m_dOtherRealCompactInterest float 其他负债合约利息金额
# StkSubjects - 担保标的对象
字段名 数据类型 解释
m_nPlatformID int 平台号//目前主要用于区别不同的行情,根据此来选择对应行情
m_strBrokerID string 经纪公司编号
m_strBrokerName string 经纪公司名称
m_strExchangeID string 交易所
m_strInstrumentID string 证券代码
m_dSloRatio float 融券保证金比例
m_eSloStatus enum EXTSubjectsStatus
在新窗口打开 ,融券状态
m_dFinRatio float 融资保证金比例
m_eFinStatus enum EXTSubjectsStatus
在新窗口打开 ,融资状态
m_strAccountID string 资金账号
m_eCreditFundCtl enum EXTCreditFundCtl
在新窗口打开 ,融资交易控制
m_eCreditStkCtl enum EXTCreditStkCtl
在新窗口打开 ,融券交易控制
m_eAssureStatus enum EXTSubjectsStatus
在新窗口打开 ,是否可做担保
m_dAssureRatio float 担保品折算比例
# PassorderArguments - 下单函数参数对象
字段名 数据类型 解释
opType int passorder的opType参数
orderType int passorder的orderType参数
accountID string 资金账号
orderCode string 交易代码
prType int passorder的prType,价格类型
modelPrice float 下单价格
modelVolume int 下单量(手数或股数)
strategyName string 策略名 _ &&& _ 投资备注
# CTaskDetail - 任务对象
字段名 数据类型 解释
m_nTaskId int 任务号
m_eStatus enum 任务状态 ETaskStatus类型,见ETaskStatus说明
在新窗口打开
m_strMsg string 任务状态消息
m_startTime int 任务开始时间, 时间戳类型
m_endTime int 任务结束时间, 时间戳类型
m_cancelTime int 任务取消时间
m_nBusinessNum int 已成交量
m_nGroupId int 组合Id
m_stockCode string 下单代码(不针对组合下单)
m_strAccountID string 下单用户(单用户下单)
m_eOperationType enum 下单操作: 开平、多空……EOperationType类型, 见EOperationType说明
在新窗口打开
m_eOrderType enum 算法交易、普通交易 EOrderType类型, 见EOrderType说明
在新窗口打开
m_ePriceType enum 报价方式:对手、最新…… EPriceType类型见EPriceType说明
在新窗口打开
m_dFixPrice float 委托价
m_nNum int 委托量
m_strRemark string 投资备注
# CLockPosition - 期权标的持仓
字段名 数据类型 解释
m_strAccountID string 账号名
m_strExchangeID string 交易所
m_strExchangeName string 交易所名
m_strInstrumentID string 标的代码
m_strInstrumentName string 标的名称
m_totalVol int 总持仓量
m_lockVol int 可用锁定量
m_unlockVol int 未锁定量
m_coveredVol int 备兑量
m_nOnRoadcoveredVol int 在途备兑量
# CStkOptCombPositionDetail - 期权组合持仓
字段名 数据类型 解释
m_strAccountID string 账号名
m_strExchangeID string 交易所
m_strExchangeName string 交易所名
m_strContractAccount string 合约账号
m_strCombID string 组合编号
m_strCombCode string 组合策略编码
m_strCombCodeName string 组合策略名称
m_nVolume int 持仓量
m_nFrozenVolume int 冻结数量
m_nCanUseVolume int 可用数量
m_strFirstCode string 合约一
m_eFirstCodeType enum 合约一类型 认购:48,认沽:49
m_strFirstCodeName string 合约一名称
m_eFirstCodePosType enum 合约一持仓类型 认购:48,义务:49,备兑:50
m_nFirstCodeAmt int 合约一数量
m_strSecondCode string 合约二
m_eSecondCodeType enum 合约二类型 认购:48,认沽:49
m_strSecondCodeName string 合约二名称
m_eSecondCodePosType enum 合约二持仓类型 权利:48,义务:49,备兑:50
m_nSecondCodeAmt int 合约二数量
m_dCombBailBalance float 占用保证金
# entrustType - 委托类型
0 - 未知
1 - 正常交易业务
2 - 即时成交剩余撤销
3 - ETF基金申报
4 - 最优五档即时成交剩余撤销
5 - 全额成交或撤销
6 - 本方最优价格
7 - 对手方最优价格
# openInt - 证券状态(股票)
编码 状态
0,10 默认为未知
1 停牌
11 开盘前S
12 集合竞价时段C
13 连续交易T
14 休市B
15 闭市E
16 波动性中断V,例如(10006742.SHO)50ETF沽9月2300在2024/08/28 10:15:34 - 2024/08/28 10:18:34 触发熔断临时停牌,此时的openInt值为16
17 临时停牌P
18 收盘集合竞价U
19 盘中集合竞价M
20 暂停交易至闭市N
21 获取字段异常
22 盘后固定价格行情
23 盘后固定价格行情完毕
# openInt - 证券状态(期货)
编码 状态
0 默认为未知
1 开盘前S
2 集合竞价时段C
3 连续交易T
4 休市B
5 闭市E
上次更新:
邀请注册送VIP优惠券
分享下方的内容给好友、QQ群、微信群,好友注册您即可获得VIP优惠券
玩转qmt,上迅投qmt知识库
登录后获取
变量约定 系统函数
File diff suppressed because one or more lines are too long
@@ -0,0 +1,826 @@
枚举常量 | 迅投知识库
迅投知识库
首页
API API
投研新手教程
QMT新手教程
内置Python
XtQuant文档
VBA
数据字典 数据字典
快速开始
股票数据
行业概念数据
指数数据
期货数据
期权数据
场内基金数据
债券数据
常见问题
场景化示例
迅投因子
数据浏览器
策略迁移 策略迁移
聚宽策略
客服QQ: 810315303
联系方式: 18309226715
视频教程 视频教程
投研端教程
迅投学堂
在新窗口打开
投研平台
在新窗口打开
迅投社区
在新窗口打开
迅投官网
在新窗口打开
注册
登录
智能助手加载中...
首页
API API
投研新手教程
QMT新手教程
内置Python
XtQuant文档
VBA
数据字典 数据字典
快速开始
股票数据
行业概念数据
指数数据
期货数据
期权数据
场内基金数据
债券数据
常见问题
场景化示例
迅投因子
数据浏览器
策略迁移 策略迁移
聚宽策略
客服QQ: 810315303
联系方式: 18309226715
视频教程 视频教程
投研端教程
迅投学堂
在新窗口打开
投研平台
在新窗口打开
迅投社区
在新窗口打开
迅投官网
在新窗口打开
内置Python
快速开始
使用须知
界面操作
变量约定
数据结构
系统函数
行情函数
交易函数
成交回报实时主推函数
引用函数
绘图函数
枚举常量
opType - 操作类型
期货/股指期权/商品期权 - 六键
期货/股指期权/商品期权 - 四键
期货/股指期权/商品期权 - 两键
股票/ETF/可转债买卖
融资融券
组合交易
ETF期权交易
ETF申赎交易
专项两融
可转债转股/回售
orderType - 下单方式
单股交易
单股交易(账号组)
组合交易(单账号)
组合交易(账号组)
prType - 下单选价类型
volume - 下单数量
单股下单时
组合下单时
quicktrade - 快速下单
enum_ - 对象属性状态字段释义
enum_EEntrustBS - 买卖方向
EEntrustSubmitStatus - 报单状态
enum_EEntrustTypes - 委托类型
enum_EEntrustStatus - 委托状态
enum_EHedge_Flag_Type - 投保类型
enum_EFutureTradeType - 成交类型
enum_EBrokerPriceType - 价格类型
enum_EOffset_Flag_Type - 操作类型
enum_EXTSubjectsStatus - 融资融券状态
enum_EXTCreditFundCtl - 融资交易控制
enum_EXTCreditStkCtl - 融券交易控制
enum_EXTSloTypeQueryMode - 查询类型
enum_EXTCompactType - 合约类型
enum_EXTCompactStatus - 合约状态
enum_EXTCompactBrushSource - 头寸来源
enum_EXTSpecialAssure - 是否可以用融券资金买入
enum_EOperationType - 下单操作类型/主要交易类型
enum_EOrderType - 算法交易、普通交易类型
enum_EPriceType - 价格类型
enum_ETaskStatus - 任务状态
完整示例
常见问题
# opType - 操作类型
# 期货/股指期权/商品期权 - 六键
数值 描述
0 开多
1 平昨多
2 平今多
3 开空
4 平昨空
5 平今空
# 期货/股指期权/商品期权 - 四键
数值 描述
6 平多, 优先平今
7 平多, 优先平昨
8 平空, 优先平今
9 平空, 优先平昨
# 期货/股指期权/商品期权 - 两键
数值 描述
10 卖出, 如有多仓, 优先平仓, 优先平今, 如有余量, 再开空
11 卖出, 如有多仓, 优先平仓, 优先平昨, 如有余量, 再开空
12 买入, 如有空仓, 优先平仓, 优先平今, 如有余量, 再开多
13 买入, 如有空仓, 优先平仓, 优先平昨, 如有余量, 再开多
14 买入, 不优先平仓
15 卖出, 不优先平仓
# 股票/ETF/可转债买卖
数值 描述
23 股票/ETF/可转债买入,或沪港通、深港通股票买入
24 股票/ETF/可转债卖出,或沪港通、深港通股票卖出
# 融资融券
数值 描述
27 融资买入
28 融券卖出
29 买券还券
30 直接还券
31 卖券还款
32 直接还款
33 担保品买入
34 担保品卖出
# 组合交易
数值 描述
25 组合买入,或沪港通、深港通的组合买入
26 组合卖出,或沪港通、深港通的组合卖出
27 融资买入
28 融券卖出
29 买券还券
31 卖券还款
33 担保品买入
34 担保品卖出
35 普通账号一键买卖
36 信用账号一键买卖
40 期货组合开多
43 期货组合开空
46 期货组合平多, 优先平今
47 期货组合平多, 优先平昨
48 期货组合平空, 优先平今
49 期货组合平空, 优先平昨
# ETF期权交易
数值 描述
50 买入开仓
51 卖出平仓
52 卖出开仓
53 买入平仓
54 备兑开仓
55 备兑平仓
56 认购行权
57 认沽行权
58 证券锁定
59 证券解锁
# ETF申赎交易
数值 描述
60 申购
61 赎回
# 专项两融
数值 描述
70 专项融资买入
71 专项融券卖出
72 专项买券还券
73 专项直接还券
74 专项卖券还款
75 专项直接还款
# 可转债转股/回售
数值 描述
80 普通账户转股
81 普通账户回售
82 信用账户转股
83 信用账户回售
# orderType - 下单方式
提示
注意
一、期货不支持 1102 和 1202
二、对所有账号组的操作相当于对账号组里的每个账号做一样的操作,如:
passorder(23, 1202, 'testS', '000001.SZ', 5, -1, 50000, ContextInfo) ,意思就是对账号组 testS 里的所有账号都以最新价开仓买入 50000 元市值的 000001.SZ 平安银行;
passorder (60,1101,"test",'510050. SH', 5,-1,1, ContextInfo) 意思就是账号 test 申购 1 个单位 ( 900000股 )的 华夏上证50ETF (只申购不买入成分股)。
# 单股交易
数值 描述
1101 单股、单账号、普通、股/手方式下单
1102 单股、单账号、普通、金额(元)方式下单(只支持股票)
1113 单股、单账号、总资产、比例 [0 ~ 1] 方式下单
1123 单股、单账号、可用、比例[0 ~ 1]方式下单
# 单股交易(账号组)
数值 描述
1201 单股、账号组(无权重)、普通、股/手方式下单
1202 单股、账号组(无权重)、普通、金额(元)方式下单(只支持股票)
1213 单股、账号组(无权重)、总资产、比例 [0 ~ 1] 方式下单
1223 单股、账号组(无权重)、可用、比例 [0 ~ 1] 方式下单
# 组合交易(单账号)
数值 描述
2101 组合、单账号、普通、按组合股票数量(篮子中股票设定的数量)方式下单 > 对应 volume 的单位为篮子的份
2102 组合、单账号、普通、按组合股票权重(篮子中股票设定的权重)方式下单 > 对应 volume 的单位为元
2103 组合、单账号、普通、按账号可用方式下单 > (底层篮子股票怎么分配?答:按可用资金比例后按篮子中股票权重分配,如用户没填权重则按相等权重分配)只对股票篮子支持
# 组合交易(账号组)
数值 描述
2201 组合、账号组(无权重)、普通、按组合股票数量方式下单
2202 组合、账号组(无权重)、普通、按组合股票权重方式下单
2203 组合、账号组(无权重)、普通、按账号可用方式下单只对股票篮子支持
# prType - 下单选价类型
关于使用市价指令的说明
对于上交所(42,43,44,45
当 prType 选择 市价类型 时时, price 为保护限价,范围为(0 - 9999)表示投资者能够接受的最高买入价或最低卖出价,即买入申报的成交价格和转限价的价格不高于保护限价,卖出申报的成交价格和转限价的价格不低于保护限价,当 price 指定为 0 时,保护限价为对应的涨跌停价
融券卖出不允许使用市价指令
集合竞价阶段不允许使用市价指令
对于深交所(44,45,46,47,48
市价申报只适用于有价格涨跌幅限制证券。
集合竞价阶段不允许使用市价指令
对于北交所(42,43,44,45)
当 prType 选择 市价类型 时时, price 为保护限价,范围为(0 - 9999)表示投资者能够接受的最高买入价或最低卖出价,即买入申报的成交价格和转限价的价格不高于保护限价,卖出申报的成交价格和转限价的价格不低于保护限价,当 price 指定为 0 时,保护限价为对应的涨跌停价
融券卖出不允许使用市价指令
集合竞价阶段不允许使用市价指令
数值 描述
-1 无效(只对于algo_passorder起作用)
0 卖5价
1 卖4价
2 卖3价
3 卖2价
4 卖1价
5 最新价
6 买1价
7 买2价(组合不支持)
8 买3价(组合不支持)
9 买4价(组合不支持)
10 买5价(组合不支持)
11 指定价(只对单股情况支持,对组合交易不支持)
12 涨跌停价(对手方最远端价格)
13 挂单价(本方一档价格)
14 对手价(对方一档价格)
18 市价最优价[郑商所][期货] (不支持模拟交易中使用)
19 市价即成剩撤[大商所][期货] (不支持模拟交易中使用)
20 市价全额成交或撤[大商所][期货] (不支持模拟交易中使用)
21 市价最优一档即成剩撤[中金所][期货] (不支持模拟交易中使用)
22 市价最优五档即成剩撤[中金所][期货] (不支持模拟交易中使用)
23 市价最优一档即成剩转[中金所][期货] (不支持模拟交易中使用)
24 市价最优五档即成剩转[中金所][期货] (不支持模拟交易中使用)
26 限价即时全部成交否则撤单[上交所[期权]] [深交所[期权]] (不支持模拟交易中使用)
27 市价即成剩撤[上交所][期权] (不支持模拟交易中使用)
28 市价即全成否则撤[上交所][期权] (不支持模拟交易中使用)
29 市价剩转限价[上交所][期权] (不支持模拟交易中使用)
42 最优五档即时成交剩余撤销申报[上交所[股票]][北交所[股票]] (不支持模拟交易中使用)
43 最优五档即时成交剩转限价申报[上交所[股票]][北交所[股票]] (不支持模拟交易中使用)
44 对手方最优价格委托[上交所[股票]][深交所[股票][北交所[股票]][期权]] (不支持模拟交易中使用)
45 本方最优价格委托[上交所[股票]][深交所[股票][北交所[股票]][期权]] (不支持模拟交易中使用)
46 即时成交剩余撤销委托[深交所][股票][期权] (不支持模拟交易中使用)
47 最优五档即时成交剩余撤销委托[深交所][股票][期权] (不支持模拟交易中使用)
48 全额成交或撤销委托[深交所][股票][期权] (不支持模拟交易中使用)
49 盘后定价
# volume - 下单数量
提示
根据 orderType 值最后一位确定 volume 的单位
# 单股下单时
数值 描述
1 股 / 手 (股票: 股,股票期权: 张,期货: 手,可转债: 张,基金:份)
2 金额(元)
3 比例(%
# 组合下单时
数值 描述
1 按组合股票数量(份)
2 按组合股票权重(元)
3 按账号可用(%
# quicktrade - 快速下单
数值 描述
0 否
1 是
2 是
提示
passorder 是对最后一根K线完全走完后生成的模型信号在下一根K线的第一个 tick 数据来时触发下单交易;
采用 quickTrade 参数设置为 1 时,非历史 bar 上执行时( ContextInfo.is_last_bar() 为 True ),只要策略模型中调用到就触发下单交易。
quickTrade 参数设置为 2 时,不判断 bar 状态,只要策略模型中调用到就触发下单交易,历史 bar 上也能触发下单,请谨慎使用。
# enum_ - 对象属性状态字段释义
# enum_EEntrustBS - 买卖方向
变量 数值 描述
ENTRUST_BUY 48 买入,多
ENTRUST_SELL 49 卖出,空
ENTRUST_PLEDGE_IN 81 质押入库
ENTRUST_PLEDGE_OUT 66 质押出库
# EEntrustSubmitStatus - 报单状态
数值 描述
48 已经提交
49 撤单已经提交
50 修改已经提交
51 已经接受
52 报单已经被拒绝
53 撤单已经被拒绝
54 改单已经被拒绝
# enum_EEntrustTypes - 委托类型
变量名称 数值 描述
ENTRUST_BUY_SELL 48 买卖
ENTRUST_QUERY 49 查询
ENTRUST_CANCE 50 撤单
ENTRUST_APPEND 51 补单
ENTRUST_COMFIRM 52 确认
ENTRUST_BIG 53 大宗
ENTRUST_FIN 54 融资委托
ENTRUST_SLO 55 融券委托
ENTRUST_CLOSE 56 信用平仓
ENTRUST_CREDIT_NORMAL 57 信用普通委托
ENTRUST_CANCEL_OPEN 58 撤单补单
ENTRUST_TYPE_OPTION_EXERCISE 59 行权
ENTRUST_TYPE_OPTION_SECU_LOCK 60 锁定
ENTRUST_TYPE_OPTION_SECU_UNLOCK 61 解锁
ENTRUST_QUOTATION_REPURCHASE 62 报价回购
ENTRUST_TYPE_OPTION_ABANDON 63 放弃行权
ENTRUST_AGREEMENT_REPURCHASE 64 协议回购
ENTRUST_TYPE_OPTION_COMB_EXERCISE 65 组合行权
ENTRUST_TYPE_OPTION_BUILD_COMB_STRATEGY 66 构建组合策略持仓
ENTRUST_TYPE_OPTION_RELEASE_COMB_STRATEGY 67 解除组合策略持仓
ENTRUST_TYPE_LMT_LOAN 68 转融通出借
ENTRUST_TYPE_LMT_LOAN_DEFER 69 转融通出借展期
ENTRUST_TYPE_LMT_LOAN_FINISH_AHEAD 70 转融通出借提前了结
ENTRUST_CROSS_MARKET_IN 71 跨市场场内
ENTRUST_CROSS_MARKET_OUT 72 跨市场场外
# enum_EEntrustStatus - 委托状态
变量名称 数值 描述
ENTRUST_STATUS_WAIT_REPORTING 49 待报
ENTRUST_STATUS_REPORTED 50 已报(已报出到柜台,待成交)
ENTRUST_STATUS_REPORTED_CANCEL 51 已报待撤(对已报状态的委托撤单吗,等待柜台处理撤单请求)
ENTRUST_STATUS_PARTSUCC_CANCEL 52 部成待撤(已报到柜台,已有部分成交,已发出对剩余部分的撤单,待柜台处理撤单请求)
ENTRUST_STATUS_PART_CANCEL 53 部撤(已报到柜台,已有部分成交,剩余部分已撤)
ENTRUST_STATUS_CANCELED 54 已撤
ENTRUST_STATUS_PART_SUCC 55 部成(已报到柜台,已有部分成交)
ENTRUST_STATUS_SUCCEEDED 56 已成
ENTRUST_STATUS_JUNK 57 废单(不符合报单条件,委托被打回,相关信息再委托的废单原因字段查看)
# enum_EHedge_Flag_Type - 投保类型
变量名称 数值 描述
HEDGE_FLAG_SPECULATION 49 投机
HEDGE_FLAG_ARBITRAGE 50 套利
HEDGE_FLAG_HEDGE 51 套保
# enum_EFutureTradeType - 成交类型
变量名称 数值 描述
FUTRUE_TRADE_TYPE_COMMON 48 普通成交
FUTURE_TRADE_TYPE_OPTIONSEXECUTION 49 期权成交
FUTURE_TRADE_TYPE_OTC 50 OTC 成交
FUTURE_TRADE_TYPE_EFPDIRVED 51 期转现衍生成交
FUTURE_TRADE_TYPE_COMBINATION_DERIVED 52 组合衍生成交
# enum_EBrokerPriceType - 价格类型
变量名称 数值 描述
BROKER_PRICE_ANY 49 市价
BROKER_PRICE_LIMIT 50 限价
BROKER_PRICE_BEST 51 最优价
BROKER_PRICE_PROP_ALLOTMENT 52 配股
BROKER_PRICE_PROP_REFER 53 转托
BROKER_PRICE_PROP_SUBSCRIBE 54 申购
BROKER_PRICE_PROP_BUYBACK 55 回购
BROKER_PRICE_PROP_PLACING 56 配售
BROKER_PRICE_PROP_DECIDE 57 指定
BROKER_PRICE_PROP_EQUITY 58 转股
BROKER_PRICE_PROP_SELLBACK 59 回售
BROKER_PRICE_PROP_DIVIDEND 60 股息
BROKER_PRICE_PROP_SHENZHEN_PLACING 68 深圳配售确认
BROKER_PRICE_PROP_CANCEL_PLACING 69 配售放弃
BROKER_PRICE_PROP_WDZY 70 无冻质押
BROKER_PRICE_PROP_DJZY 71 冻结质押
BROKER_PRICE_PROP_WDJY 72 无冻解押
BROKER_PRICE_PROP_JDJY 73 解冻解押
BROKER_PRICE_PROP_ETF 81 ETF申购
BROKER_PRICE_PROP_VOTE 75 投票
BROKER_PRICE_PROP_YYSGYS 92 要约收购预售
BROKER_PRICE_PROP_YSYYJC 77 预售要约解除
BROKER_PRICE_PROP_FUND_DEVIDEND 78 基金设红
BROKER_PRICE_PROP_FUND_ENTRUST 79 基金申赎
BROKER_PRICE_PROP_CROSS_MARKET 80 跨市转托
BROKER_PRICE_PROP_EXERCIS 83 权证行权
BROKER_PRICE_PROP_PEER_PRICE_FIRST 84 对手方最优价格
BROKER_PRICE_PROP_L5_FIRST_LIMITPX 85 最优五档即时成交剩余转限价
BROKER_PRICE_PROP_MIME_PRICE_FIRST 86 本方最优价格
BROKER_PRICE_PROP_INSTBUSI_RESTCANCEL 87 即时成交剩余撤销
BROKER_PRICE_PROP_L5_FIRST_CANCEL 88 最优五档即时成交剩余撤销
BROKER_PRICE_PROP_FULL_REAL_CANCEL 89 全额成交并撤单
BROKER_PRICE_PROP_DIRECT_SECU_REPAY 101 直接还券
BROKER_PRICE_PROP_FUND_CHAIHE 90 基金拆合
BROKER_PRICE_PROP_DEBT_CONVERSION 91 债转股
BROKER_PRICE_BID_LIMIT 92 港股通竞价限价
BROKER_PRICE_ENHANCED_LIMIT 93 港股通增强限价
BROKER_PRICE_RETAIL_LIMIT 94 港股通零股限价
BROKER_PRICE_PROP_INCREASE_SHARE 'j' 增发
BROKER_PRICE_PROP_COLLATERAL_TRANSFER 107 担保品划转
BROKER_PRICE_PROP_NEEQ_PRICING 'w' 定价(全国股转 - 挂牌公司交易 - 协议转让)
BROKER_PRICE_PROP_NEEQ_MATCH_CONFIRM 'x' 成交确认(全国股转 - 挂牌公司交易 - 协议转让)
BROKER_PRICE_PROP_NEEQ_MUTUAL_MATCH_CONFIRM 'y' 互报成交确认(全国股转 - 挂牌公司交易 - 协议转让)
BROKER_PRICE_PROP_NEEQ_LIMIT 'z' 限价(用于挂牌公司交易 - 做市转让 - 限价买卖和两网及退市交易-限价买卖)
# enum_EOffset_Flag_Type - 操作类型
变量名称 数值 描述
EOFF_THOST_FTDC_OF_INVALID -1 无效操作
EOFF_THOST_FTDC_OF_Open 48 买入,开仓
EOFF_THOST_FTDC_OF_Close 49 卖出,平仓
EOFF_THOST_FTDC_OF_ForceClose 50 强平
EOFF_THOST_FTDC_OF_CloseToday 51 平今
EOFF_THOST_FTDC_OF_CloseYesterday 52 平昨
EOFF_THOST_FTDC_OF_ForceOff 53 强减
EOFF_THOST_FTDC_OF_LocalForceClose 54 本地强平
EOFF_THOST_FTDC_OF_PLEDGE_IN 81 质押入库
EOFF_THOST_FTDC_OF_PLEDGE_OUT 66 质押出库
EOFF_THOST_FTDC_OF_ALLOTMENT 67 股票配股
# enum_EXTSubjectsStatus - 融资融券状态
变量名称 数值 描述
SUBJECTS_STATUS_NORMAL 48 正常
SUBJECTS_STATUS_PAUSE 49 暂停
SUBJECTS_STATUS_NOT 50 作废
# enum_EXTCreditFundCtl - 融资交易控制
变量名称 数值 描述
FUND_CTL_ONLY_FIN_BUY 48 只允许融资买入
FUND_CTL_ONLY_SELL_CASH_REPAY 49 只允许卖券还款
FUND_CTL_ALL 50 既允许融资买入又允许卖券还款
FUND_CTL_NONE 51 既不允许融资买入又不允许卖券还款
# enum_EXTCreditStkCtl - 融券交易控制
变量名称 数值 描述
STK_CTL_ONLY_SLO_SELL 48 只允许融券卖出
STK_CTL_ONLY_BUY_SECU_REPAY 49 只允许买券还券
STK_CTL_ALL 50 既允许融券卖出又允许买券还券
STK_CTL_NONE 51 既不允许融券卖出又不允许买券还券
# enum_EXTSloTypeQueryMode - 查询类型
变量名称 数值 描述
XT_SLOTYPE_QUERYMODE_NOMARL 48 普通
XT_SLOTYPE_QUERYMODE_SPECIAL 49 专项
# enum_EXTCompactType - 合约类型
变量名称 数值 描述
COMPACT_TYPE_ALL 32 不限制
COMPACT_TYPE_FIN 48 融资
COMPACT_TYPE_SLO 49 融券
# enum_EXTCompactStatus - 合约状态
变量名称 数值 描述
COMPACT_STATUS_ALL 32 不限制
COMPACT_STATUS_UNDONE 48 未归还
COMPACT_STATUS_PART_DONE 49 部分归还
COMPACT_STATUS_DONE 50 已归还
COMPACT_STATUS_DONE_BY_SELF 51 自行了结
COMPACT_STATUS_DONE_BY_HAND 52 手工了结
COMPACT_STATUS_NOT_DEBT 53 未形成负债
COMPACT_STATUS_EXPIRY 54 合约已过期
# enum_EXTCompactBrushSource - 头寸来源
变量名称 数值 描述
XT_COMPACT_BRUSH_SOURCE_ALL 32 不限制
XT_COMPACT_BRUSH_SOURCE_NORMAL 48 普通头寸
XT_COMPACT_BRUSH_SOURCE_SPECIAL 49 专项头寸
# enum_EXTSpecialAssure - 是否可以用融券资金买入
变量名称 数值 描述
ASSURE_USE_SLO_CASH_DISABLE 48 担保品买入不允许使用融券资金
ASSURE_USE_SLO_CASH_ENABLE 49 担保品买入允许使用融券资金
# enum_EOperationType - 下单操作类型/主要交易类型
变量名称 数值 描述
OPT_OPEN_LONG 0 开多
OPT_CLOSE_LONG_HISTORY 1 平昨多
OPT_CLOSE_LONG_TODAY 2 平今多
OPT_OPEN_SHORT 3 开空
OPT_CLOSE_SHORT_HISTORY 4 平昨空
OPT_CLOSE_SHORT_TODAY 5 平今空
OPT_CLOSE_LONG_TODAY_FIRST 6 优先平今多
OPT_CLOSE_LONG_HISTORY_FIRST 7 优先平昨多
OPT_CLOSE_SHORT_TODAY_FIRST 8 平空优先平今
OPT_CLOSE_SHORT_HISTORY_FIRST 9 平空优先平昨
OPT_CLOSE_LONG_TODAY_HISTORY_THEN_OPEN_SHORT 10 卖出优先平今
OPT_CLOSE_LONG_HISTORY_TODAY_THEN_OPEN_SHORT 11 卖出优先平昨
OPT_CLOSE_SHORT_TODAY_HISTORY_THEN_OPEN_LONG 12 买入优先平今
OPT_CLOSE_SHORT_HISTORY_TODAY_THEN_OPEN_LONG 13 买入优先平昨
OPT_CLOSE_LONG 14 平多
OPT_CLOSE_SHORT 15 平空
OPT_OPEN 16 开仓
OPT_CLOSE 17 平仓
OPT_BUY 18 买入
OPT_SELL 19 卖出
OPT_FIN_BUY 20 融资买入
OPT_SLO_SELL 21 融券卖出
OPT_BUY_SECU_REPAY 22 买券还券
OPT_DIRECT_SECU_REPAY 23 直接还券
OPT_SELL_CASH_REPAY 24 卖券还款
OPT_DIRECT_CASH_REPAY 25 直接还款
OPT_FUND_SUBSCRIBE 26 基金申购
OPT_FUND_REDEMPTION 27 基金赎回
OPT_FUND_MERGE 28 基金合并
OPT_FUND_SPLIT 29 基金分拆
OPT_PLEDGE_IN 30 质押入库
OPT_PLEDGE_OUT 31 质押出库
OPT_OPTION_BUY_OPEN 32 买入开仓(个股期权交易)
OPT_OPTION_SELL_CLOSE 33 卖出平仓(个股期权交易)
OPT_OPTION_SELL_OPEN 34 卖出开仓(个股期权交易)
OPT_OPTION_BUY_CLOSE 35 买入平仓(个股期权交易)
OPT_OPTION_COVERED_OPEN 36 备兑开仓(个股期权交易)
OPT_OPTION_COVERED_CLOSE 37 备兑平仓(个股期权交易)
OPT_OPTION_CALL_EXERCISE 38 认购行权(个股期权交易)
OPT_OPTION_PUT_EXERCISE 39 认沽行权(个股期权交易)
OPT_OPTION_SECU_LOCK 40 证券锁定(个股期权交易)
OPT_OPTION_SECU_UNLOCK 41 证券解锁(个股期权交易)
OPT_N3B_PRICE_BUY 42 协议转让-定价买入
OPT_N3B_PRICE_SELL 43 协议转让-定价卖出
OPT_N3B_CONFIRM_BUY 44 协议转让-成交确认买入
OPT_N3B_CONFIRM_SELL 45 协议转让-成交确认卖出
OPT_N3B_REPORT_CONFIRM_BUY 46 协议转让-互报成交确认买入
OPT_N3B_REPORT_CONFIRM_SELL 47 协议转让-互报成交确认卖出
OPT_N3B_LIMIT_PRICE_BUY 48 全国股转-限价买入
OPT_N3B_LIMIT_PRICE_SELL 49 全国股转-限价卖出
OPT_FUTURE_OPTION_EXERCISE 50 期货期权行权
OPT_CONVERT_BONDS 51 可转债转股
OPT_SELL_BACK_BONDS 52 可转债回售
OPT_STK_ALLOTMENT 53 股票配股
OPT_STK_INCREASE_SHARE 54 股票增发
OPT_COLLATERAL_TRANSFER_IN 55 担保品划入
OPT_COLLATERAL_TRANSFER_OUT 56 担保品划出
OPT_BLOCK_INTENTION_BUY 57 意向申报买入
OPT_BLOCK_INTENTION_SELL 58 意向申报卖出
OPT_BLOCK_PRICE_BUY 59 定价申报买入
OPT_BLOCK_PRICE_SELL 60 定价申报卖出
OPT_BLOCK_CONFIRM_BUY 61 成交申报买入
OPT_BLOCK_CONFIRM_SELL 62 成交申报卖出
OPT_BLOCK_CLOSE_PRICE_BUY 63 盘后定价买入
OPT_BLOCK_CLOSE_PRICE_SELL 64 盘后定价卖出
OPT_GOLD_PRICE_DELIVERY_BUY 65 黄金交割买
OPT_GOLD_PRICE_DELIVERY_SELL 66 黄金交割卖
OPT_GOLD_PRICE_MIDDLE_BUY 67 黄金中立仓买
OPT_GOLD_PRICE_MIDDLE_SELL 68 黄金中立仓卖
OPT_COMPOSE_ONEKEY_BUYSELL 69 组合交易一键买卖
OPT_COMPOSE_GGT_BUY 70 组合交易港股通买入
OPT_COMPOSE_GGT_SELL 71 组合交易港股通卖出
OPT_ODD_SELL 72 零股卖出
OPT_ETF_STOCK_BUY 73 成份股买入
OPT_ETF_STOCK_SELL 74 成份股卖出
OPT_OTC_FUND_SUBSCRIBE 200 场外基金认购
OPT_OTC_FUND_PURCHASE 201 场外基金申购
OPT_OTC_FUND_REDEMPTION 202 场外基金赎回
OPT_OTC_FUND_CONVERT 203 场外基金转换
OPT_OTC_FUND_BONUS_TYPE_UPDATE 204 场外基金分红方式变更
OPT_OTC_CONTRACTUAL_DEPOSIT 205 场外协议存款
OPT_OTC_NON_CONTRACTUAL_DEPOSIT 206 场外非协议存款
OPT_OTC_CONTRACTUAL_DEPOSIT_ASK 207 场外协议存款询价
OPT_OTC_NON_CONTRACTUAL_DEPOSIT_ASK 208 场外非协议存款询价
OPT_OTC_NON_CONTRACTUAL_DEPOSIT_CUR 209 场外非协议活期存款
OPT_OTC_DRAW_DEPOSIT 210 场外存单支取
OPT_OTC_STOCK_INQUIRY 230 网下询价
OPT_OTC_STOCK_PURCHASE 231 网下申购
OPT_OPTION_NS_DEPOSIT 1001 场外转账入金
OPT_OPTION_NS_WITHDRAW 1002 场外转账出金
OPT_OPTION_NS_INOUT 1003 场外互转
OPT_ETF_PURCHASE 1004 ETF申购
OPT_ETF_REDEMPTION 1005 ETF赎回
OPT_OUTER_BUY 1006 外盘买入
OPT_OUTER_SELL 1007 外盘卖出
OPT_OUTER_CAN_CLOSE_BUY 1008 外盘可平买仓
OPT_OUTER_CAN_CLOSE_SELL 1009 外盘可平卖仓
OPT_SLO_SELL_SPECIAL 1010 专项融券卖出
OPT_BUY_SECU_REPAY_SPECIAL 1011 专项买券还券
OPT_DIRECT_SECU_REPAY_SPECIAL 1012 专项直接还券
OPT_NEEQ_O3B_LIMIT_PRICE_BUY 1013 全国股转-两网及退市交易-限价买入
OPT_NEEQ_O3B_LIMIT_PRICE_SELL 1014 全国股转-两网及退市交易-限价卖出
OPT_IBANK_BOND_BUY 1015 投行债券买入
OPT_IBANK_BOND_SELL 1016 投行债券卖出
OPT_IBANK_FUND_REPURCHASE 1017 质押式融资回购
OPT_IBANK_BOND_REPURCHASE 1018 质押式融券回购
OPT_IBANK_BOND_REPAY 1019 质押式融资购回
OPT_IBANK_FUND_RETRIEVE 1020 质押式融券购回
OPT_INTEREST_FEE 1021 融券息费
OPT_FIN_BUY_SPECIAL 1022 专项融资买入
OPT_SELL_CASH_REPAY_SPECIAL 1023 专项卖券还款
OPT_DIRECT_CASH_REPAY_SPECIAL 1024 专项直接还款
OPT_FUND_PRICE_BUY 1025 货币基金申购
OPT_FUND_PRICE_SELL 1026 货币基金赎回
OPT_N3B_CALL_AUCTION_BUY 1027 协议转让-集合竞价买入
OPT_N3B_CALL_AUCTION_SELL 1028 协议转让-集合竞价卖出
OPT_N3B_AFTER_HOURS_BUY 1029 全国股转-盘后协议买入
OPT_N3B_AFTER_HOURS_SELL 1030 全国股转-盘后协议卖出
OPT_ETF_HEDGE 1031 ETF套利
OPT_QUOTATION_REPURCHASE_BUY 1032 报价回购买入
OPT_QUOTATION_REPURCHASE_STOP 1033 报价回购终止续做
OPT_QUOTATION_REPURCHASE_BEFORE 1034 报价回购提前购回
OPT_QUOTATION_REPURCHASE_RESERVATION 1035 报价回购购回预约
OPT_QUOTATION_REPURCHASE_CANCEL 1036 报价回购取消预约
OPT_BLOCK_CONFIRM_MATCH_BUY 1037 成交申报配对买入
OPT_BLOCK_CONFIRM_MATCH_SELL 1038 成交申报配对卖出
OPT_FUTURE_OPTION_ABANDON 1039 期货期权放弃行权
OPT_ONEKEY_TRANSFER 1040 一键划转
OPT_ONEKEY_TRANSFER_IN 1041 一键划入
OPT_ONEKEY_TRANSFER_OUT 1042 一键划出
OPT_AFTER_FIX_BUY 1043 盘后定价买入
OPT_AFTER_FIX_SELL 1044 盘后定价卖
OPT_AGREEMENT_REPURCHASE_TRANSACTION_DEC_FORWARD 1045 成交申报正回购
OPT_AGREEMENT_REPURCHASE_TRANSACTION_DEC_REVERSE 1046 成交申报逆回购
OPT_AGREEMENT_REPURCHASE_EXPIRE_CONFIRM 1047 到期确认
OPT_AGREEMENT_REPURCHASE_ADVANCE_REPURCHASE 1048 提前购回正回购
OPT_AGREEMENT_REPURCHASE_ADVANCE_REVERSE 1049 提前购回逆回购
OPT_AGREEMENT_REPURCHASE_EXPIRE_RENEW 1050 到期续做正回购
OPT_AGREEMENT_REPURCHASE_EXPIRE_REVERSE 1051 到期续做逆回购
OPT_TRANSACTION_IN_CASH_BUY 1052 现券买入
OPT_TRANSACTION_IN_CASH_SELL 1053 现券卖出
OPT_OUTRIGHT_REPO_FUND_REPURCHASE 1054 买断式融资回购
OPT_OUTRIGHT_REPO_BOND_REPURCHASE 1055 买断式融券回购
OPT_OUTRIGHT_REPO_BOND_REPAY 1056 买断式融资购回
OPT_OUTRIGHT_REPO_FUND_RETRIEVE 1057 买断式融券购回
OPT_DISTRIBUTION_BUYING 1058 分销买入
OPT_FIXRATE_TO_FLOATINGRATE 1059 固定利率换浮动利率
OPT_FLOATINGRATE_TO_FIXRATE 1060 浮动利率换固定利率
OPT_IBANK_TRANSFER_OUT 1061 银行间转出托管
OPT_IBANK_TRANSFER_IN 1062 银行间转入托管
OPT_AGREEMENT_REPURCHASE_INTENTION_BUY 1063 意向申报正回购买入
OPT_AGREEMENT_REPURCHASE_INTENTION_SELL 1064 意向申报正回购卖出
OPT_AGREEMENT_REPURCHASE_BIZ_APPLY_CONFIRM 1065 协议回购成交申报确认
OPT_AGREEMENT_REPURCHASE_BIZ_APPLY_REJECT 1066 协议回购成交申报拒绝
OPT_AGREEMENT_REPURCHASE_CONTINUE_CONFIRM 1067 协议回购到期续做申报确认
OPT_AGREEMENT_REPURCHASE_CONTINUE_REJECT 1068 协议回购到期续做申报拒绝
OPT_AGREEMENT_REPURCHASE_INTENTION_CHANGE_BONDS 1069 协议回购换券申报
OPT_AGREEMENT_REPURCHASE_INTENTION_CHANGE_BONDS_CONFIRM 1070 协议回购换券申报确认
OPT_AGREEMENT_REPURCHASE_INTENTION_CHANGE_BONDS_REJECT 1071 协议回购换券申报拒绝
OPT_AGREEMENT_REPURCHASE_STOP_AHEAD_CONFIRM 1072 协议回购正回购提前终止申报确认
OPT_AGREEMENT_REPURCHASE_STOP_AHEAD_REJECT 1073 协议回购正回购提前终止申报拒绝
OPT_AGREEMENT_REPURCHASE_RELEASE_PLEDGE 1074 协议回购正回购方解除质押申报
OPT_AGREEMENT_REPURCHASE_RELEASE_PLEDGE_CONFIRM 1075 协议回购正回购解除质押申报确认
OPT_AGREEMENT_REPURCHASE_RELEASE_PLEDGE_REJECT 1076 协议回购正回购解除质押申报拒绝
OPT_AGREEMENT_REPURCHASE_EXPIRE_CONFIRM_SELL 1077 深圳到期确认卖出
OPT_LOAN_DISTRIBUTION_BUY 1078 债券分销
OPT_PREFERENCE_SHARES_BIDDING_BUY 1079 优先股竞价买入
OPT_PREFERENCE_SHARES_BIDDING_SELL 1080 优先股竞价卖出
OPT_TOC_BOND 1081 债券转托管
OPT_TOC_FUND 1082 基金转托管
OPT_IBANK_BORROW 1083 同业拆入
OPT_IBANK_LOAN 1084 同业拆出
OPT_IBANK_BORROW_REPAY 1085 拆入还款
OPT_IBANK_LOAN_REPAY 1086 拆出还款
OPT_FINANCIAL_PRODUCT_BUY 1087 理财产品申购
OPT_FINANCIAL_PRODUCT_SELL 1088 理财产品赎回
OPT_OPTION_COMB_EXERCISE 1089 组合行权
OPT_OPTION_BUILD_COMB_STRATEGY 1090 构建组合策略
OPT_OPTION_RELEASE_COMB_STRATEGY 1091 解除组合策略
OPT_AGREEMENT_REPURCHASE_REVERSE_STOP_AHEAD_CONFIRM 1092 协议回购逆回购提前终止申报确认
OPT_AGREEMENT_REPURCHASE_REVERSE_STOP_AHEAD_REJECT 1093 协议回购逆回购提前终止申报拒绝
OPT_AGREEMENT_REPURCHASE_REVERSE_RELEASE_PLEDGE 1094 协议回购逆回购方解除质押申报
OPT_AGREEMENT_REPURCHASE_REVERSE_RELEASE_PLEDGE_CONFIRM 1095 协议回购逆回购解除质押申报确认
OPT_AGREEMENT_REPURCHASE_REVERSE_RELEASE_PLEDGE_REJECT 1096 协议回购逆回购解除质押申报拒绝
OPT_BOND_TENDER 1097 债券投标
OPT_FINANCIAL_PRODUCT_CALL 1098 理财产品认购
OPT_NEEQ_O3B_CONTINUOUS_AUCTION_BUY 1099 全国股转-北交所买入
OPT_NEEQ_O3B_CONTINUOUS_AUCTION_SELL 1100 全国股转-北交所卖出
OPT_NEEQ_O3B_ASK_PRICE 1101 全国股转-申购-询价申报
OPT_NEEQ_O3B_PRICE_CONFIRM 1102 全国股转-申购-申购申报
OPT_NEEQ_O3B_BLOCKTRADING_BUY 1103 全国股转-大宗交易买入
OPT_NEEQ_O3B_BLOCKTRADING_SELL 1104 全国股转-大宗交易卖出
OPT_LMT_LOAN_SET 1105 转融通非约定出借申报
OPT_LMT_LOAN_CONVENTION 1106 转融通约定出借申报
OPT_LMT_LOAN_RENEWAL 1107 转融通出借展期
OPT_LMT_LOAN_SETTLE_EARLY 1108 转融通出借提前了结
OPT_CROSS_MARKET_IN_ETF_PURCHASE 1109 跨市场ETF场内申购
OPT_CROSS_MARKET_IN_ETF_REDEMPTION 1110 跨市场ETF场内赎回
OPT_CROSS_MARKET_OUT_ETF_PURCHASE 1111 跨市场ETF场外申购
OPT_CROSS_MARKET_OUT_ETF_REDEMPTION 1112 跨市场ETF场外赎回
OPT_CREDIT_APPOINTMENT 1113 券源预约
OPT_OFF_IPO_PUB_PRICE 1114 网下申购-公开发行询价
OPT_OFF_IPO_PUB_PURCHASE 1115 网下申购-公开发行申购
OPT_OFF_IPO_NON_PUB_PRICE 1116 网下申购-非公开发行询价
OPT_OFF_IPO_NON_PUB_PURCHASE 1117 网下申购-非公开发行申购
OPT_IBANK_PUT 1118 债券回售
OPT_IBANK_BOND_BORROW 1119 债券借贷融入
OPT_IBANK_BOND_LEND 1120 债券借贷融出
OPT_IBANK_BOND_BORROW_REPAY 1121 债券借贷融入购回
OPT_IBANK_BOND_LEND_RETRIEVE 1122 债券借贷融出购回
OPT_IBANK_BOND_DISPLACE 1123 债券借贷-质押券置换
OPT_LENDING_INTEGRATE_INTO 1124 融券通-预约融券融入
OPT_LENDING_MELT_OUT 1125 融券通-预约融券融出
OPT_FICC_MANUAL_DECLARE_BUY 1126 固收业务-点击成交-报价申报买入
OPT_FICC_MANUAL_DECLARE_SELL 1127 固收业务-点击成交-报价申报卖出
OPT_FICC_MANUAL_CONFIRM_BUY_CONFIRM 1128 固收业务-点击成交-报价确认-买入-确认
OPT_FICC_MANUAL_CONFIRM_BUY_REJECT 1129 固收业务-点击成交-报价确认-买入-拒绝
OPT_FICC_MANUAL_CONFIRM_SELL_CONFIRM 1130 固收业务-点击成交-报价确认-卖出-确认
OPT_FICC_MANUAL_CONFIRM_SELL_REJECT 1131 固收业务-点击成交-报价确认-卖出-拒绝
OPT_FICC_CONSULT_DECLARE_BUY 1132 固收业务-协商成交-协商申报买入
OPT_FICC_CONSULT_DECLARE_SELL 1133 固收业务-协商成交-协商申报卖出
OPT_FICC_CONSULT_CONFIRM_BUY_CONFIRM 1134 固收业务-协商成交-协商确认-买入-确认
OPT_FICC_CONSULT_CONFIRM_BUY_REJECT 1135 固收业务-协商成交-协商确认-买入-拒绝
OPT_FICC_CONSULT_CONFIRM_SELL_CONFIRM 1136 固收业务-协商成交-协商确认-卖出-确认
OPT_FICC_CONSULT_CONFIRM_SELL_REJECT 1137 固收业务-协商成交-协商确认-卖出-拒绝
OPT_FICC_ENQUIRY_DECLARE_BUY 1138 固收业务-询价成交-询价申报买入
OPT_FICC_ENQUIRY_DECLARE_SELL 1139 固收业务-询价成交-询价申报卖出
OPT_FICC_ENQUIRY_REPLAY_BUY_CONFIRM 1140 固收业务-询价成交-报价回复-买入-确认
OPT_FICC_ENQUIRY_REPLAY_BUY_REJECT 1141 固收业务-询价成交-报价回复-买入-拒绝--预留字段
OPT_FICC_ENQUIRY_REPLAY_SELL_CONFIRM 1142 固收业务-询价成交-报价回复-卖出-确认
OPT_FICC_ENQUIRY_REPLAY_SELL_REJECT 1143 固收业务-询价成交-报价回复-卖出-拒绝--预留字段
OPT_FICC_ENQUIRY_INQUIRY_BUY_CONFIRM 1144 固收业务-询价成交-询价成交-买入-确认
OPT_FICC_ENQUIRY_INQUIRY_BUY_REJECT 1145 固收业务-询价成交-询价成交-买入-拒绝--预留字段
OPT_FICC_ENQUIRY_INQUIRY_SELL_CONFIRM 1146 固收业务-竞买成交-询价成交-卖出-确认
OPT_FICC_ENQUIRY_INQUIRY_SELL_REJECT 1147 固收业务-竞买成交-询价成交-卖出-拒绝--预留字段
OPT_FICC_BINDDING_RESERVE_BUY 1148 固收业务-竞买成交-竞买预约买入
OPT_FICC_BINDDING_RESERVE_SELL 1149 固收业务-竞买成交-竞买预约卖出
OPT_FICC_BINDDING_DECLARE_BUY 1150 固收业务-竞买成交-竞买申报买入
OPT_FICC_BINDDING_DECLARE_SELL 1151 固收业务-竞买成交-竞买申报卖出
OPT_FICC_BINDDING_PRICE_DECLARE_BUY 1152 固收业务-竞买成交-应价申报买入
OPT_FICC_BINDDING_PRICE_DECLARE_SELL 1153 固收业务-竞买成交-应价申报卖出
OPT_OPTION_BUY_CLOSE_THEN_OPEN 1154 买入优先平仓,个股期权交易业务补充类型
OPT_OPTION_SELL_CLOSE_THEN_OPEN 1155 卖出优先平仓
OPT_FUND_TRANSFER_IN 1156 资金划入
OPT_FUND_TRANSFER_OUT 1157 资金划出
# enum_EOrderType - 算法交易、普通交易类型
变量名称 数值 描述
OTP_ORDINARY 0 常规
OTP_ALGORITHM 1 算法交易
OTP_RANDVOLUME 2 随机量交易
OTP_ALGORITHM3 3 算法交易3
OTP_ZXJT 4 中信建投算法
OTP_ZSGS 5 隔时交易
OTP_ORDINARY_BASKET_TRIGGER_SINGLE_ORDER 6 普通交易的触价单笔委托方式
OTP_ALGORITHM_BASKET_TRIGGER_SINGLE_ORDER 7 算法交易的触价单笔委托方式
OTP_ZXZQ 8 中信证券算法
OTP_GENUS 9 金纳算法
OTP_JAZZ 10 爵士算法
OTP_VWAP 11 智能VWAP
OTP_TWAP 12 智能TWAP
OTP_XTALGO 13 智能算法
OTP_HUACHUANG 14 华创算法
OTP_HUARUN 15 华润算法
OTP_CUSTOM 16 回转算法
OPT_EXTERN 17 主动算法
OTP_GUANGFA 18 广发算法
# enum_EPriceType - 价格类型
变量名称 数值 描述
PRTP_SALE5 0 卖5
PRTP_SALE4 1 卖4
PRTP_SALE3 2 卖3
PRTP_SALE2 3 卖2
PRTP_SALE1 4 卖1
PRTP_LATEST 5 最新价
PRTP_BUY1 6 买1
PRTP_BUY2 7 买2
PRTP_BUY3 8 买3
PRTP_BUY4 9 买4
PRTP_BUY5 10 买5
PRTP_FIX 11 指定价
PRTP_MARKET 12 市价_涨跌停价
PRTP_HANG 13 挂单价
PRTP_COMPETE 14 对手价
PRTP_AUTO 15 自动盘口
PRTP_CLOSE 16 昨收价
PRTP_AVERAGE 17 大宗加权平均价
PRTP_MARKET_BEST 18 市价_最优价
PRTP_MARKET_CANCEL 19 市价_即成剩撤
PRTP_MARKET_CANCEL_ALL 20 市价_全额成交或撤
PRTP_MARKET_CANCEL_1 21 市价_最优1档即成剩撤
PRTP_MARKET_CANCEL_5 22 市价_最优5档即成剩撤
PRTP_MARKET_CONVERT_1 23 市价_最优1档即成剩转
PRTP_MARKET_CONVERT_5 24 市价_最优5档即成剩转
PRTP_STK_OPTION_ASK 25 询价
PRTP_STK_OPTION_FIX_CANCEL_ALL 26 限价即时全部成交否则撤单
PRTP_STK_OPTION_MARKET_CACEL_LEFT 27 市价即时成交剩余撤单
PRTP_STK_OPTION_MARKET_CANCEL_ALL 28 市价即时全部成交否则撤单
PRTP_STK_OPTION_MARKET_CONVERT_FIX 29 市价剩余转限价
PRTP_SALE6 30 卖6
PRTP_SALE7 31 卖7
PRTP_SALE8 32 卖8
PRTP_SALE9 33 卖9
PRTP_SALE10 34 卖10
PRTP_BUY6 35 买6
PRTP_BUY7 36 买7
PRTP_BUY8 37 买8
PRTP_BUY9 38 买9
PRTP_BUY10 39 买10
PRTP_UPPER_LIMIT_PRICE 40 涨停价
PRTP_LOWER_LIMIT_PRICE 41 跌停价
PRTP_MARKET_SH_CONVERT_5_CANCEL 42 最优五档即时成交剩余撤销
PRTP_MARKET_SH_CONVERT_5_LIMIT 43 最优五档即时成交剩转限价
PRTP_MARKET_PEER_PRICE_FIRST 44 对手方最优价格委托
PRTP_MARKET_MINE_PRICE_FIRST 45 本方最优价格委托
PRTP_MARKET_SZ_INSTBUSI_RESTCANCEL 46 即时成交剩余撤销委托
PRTP_MARKET_SZ_CONVERT_5_CANCEL 47 最优五档即时成交剩余撤销委托
PRTP_MARKET_SZ_FULL_REAL_CANCEL 48 全额成交或撤销委托
PRTP_AFTER_FIX_PRICE 49 盘后定价申报
# enum_ETaskStatus - 任务状态
变量名称 数值 描述
TASK_STATUS_UNKNOWN 0 未知
TASK_STATUS_WAITING 1 等待
TASK_STATUS_COMMITING 2 提交中
TASK_STATUS_RUNNING 3 执行中
TASK_STATUS_PAUSE 4 暂停
TASK_STATUS_CANCELING_DEPRECATED 5 撤销中(已弃用)
TASK_STATUS_EXCEPTION_CANCELING_DEPRECATED 6 异常撤销中(已弃用)
TASK_STATUS_COMPLETED 7 完成
TASK_STATUS_CANCELED 8 已撤
TASK_STATUS_REJECTED 9 打回
TASK_STATUS_EXCEPTION_CANCELED 10 异常终止
TASK_STATUS_DROPPED 11 放弃(用于组合交易中,放弃补单)
TASK_STATUS_FORCE_CANCELED_DEPRECATED 12 强制终止(已弃用)
上次更新:
邀请注册送VIP优惠券
分享下方的内容给好友、QQ群、微信群,好友注册您即可获得VIP优惠券
玩转qmt,上迅投qmt知识库
登录后获取
绘图函数 完整示例
File diff suppressed because one or more lines are too long
@@ -0,0 +1,284 @@
界面操作 | 迅投知识库
迅投知识库
首页
API API
投研新手教程
QMT新手教程
内置Python
XtQuant文档
VBA
数据字典 数据字典
快速开始
股票数据
行业概念数据
指数数据
期货数据
期权数据
场内基金数据
债券数据
常见问题
场景化示例
迅投因子
数据浏览器
策略迁移 策略迁移
聚宽策略
客服QQ: 810315303
联系方式: 18309226715
视频教程 视频教程
投研端教程
迅投学堂
在新窗口打开
投研平台
在新窗口打开
迅投社区
在新窗口打开
迅投官网
在新窗口打开
注册
登录
智能助手加载中...
首页
API API
投研新手教程
QMT新手教程
内置Python
XtQuant文档
VBA
数据字典 数据字典
快速开始
股票数据
行业概念数据
指数数据
期货数据
期权数据
场内基金数据
债券数据
常见问题
场景化示例
迅投因子
数据浏览器
策略迁移 策略迁移
聚宽策略
客服QQ: 810315303
联系方式: 18309226715
视频教程 视频教程
投研端教程
迅投学堂
在新窗口打开
投研平台
在新窗口打开
迅投社区
在新窗口打开
迅投官网
在新窗口打开
内置Python
快速开始
使用须知
界面操作
新建策略
导入导出策略
策略编写
基本信息-字段描述
回测参数-字段描述
补充数据
策略运行
策略调试
独立python进程
策略回测
回测、运行两种模式的区别
配置/获取模拟账号
自动导出交易记录
操作界面快捷键
策略编辑器快捷键
变量约定
数据结构
系统函数
行情函数
交易函数
成交回报实时主推函数
引用函数
绘图函数
枚举常量
完整示例
常见问题
# 新建策略
模型创建方法有三种:
方法一,在【模型研究】界面,使用系统预置的各种示例模型,点击后方“编辑”按钮,并在弹出的【策略编辑器】中以此示例模型代码为基础进行编写。
我的主页-编辑模型
方法二,在【模型研究】界面,点击新建模型,选择 Python 模型,在弹出的【策略编辑器】中从头到尾编写一个用户自己的量化模型。
模型研究-新建模型
方法三,在模型管理面板右键,选择新建模型,并选择 Python 模型。
模型管理-新建模型
# 导入导出策略
QMT系统支持将策略以 加密 的模式进行导出或导入,用户可以便捷的迁移系统本地策略。
方式一.模型研究界面导入导出
方式二.模型研究界面导入导出
# 策略编写
【策略编辑器】是迅投专门为模型开发者设计的,集成了模型列表、函数列表、函数帮助、模型基本信息、参数设置、回测参数等多个部分,拥有代码高亮、自动补全等便捷功能于一体的便捷的模型编辑、开发环境。
模型编辑页面 右侧可选择策略默认的周期、品种
编写 Python 策略需在开始时定义编码格式,如 gbk 。
之后可选择导入第三方库,所选第三方库要在券商管理端白名单内才可运行。
Init 方法和 handlebar 方法的定义是必须的。Init 方法会在策略运行开始时调用一次,用以初始化所需对象(包裹在 ContextInfo 对象中传递),设定股票池等。
Handlebar 方法会在历史 K 线上逐 K 线调用,系统会保存函数所做更改。
在盘中交易时间,handlebar 函数会随行情推送(tick 数据)被调用,当一个 tick 数据为所在 K 线最后一个 tick 时,此 tick 调用的 handlebar 所做的更改会被系统保存,如有交易指令,会在下一根K 线的第一个 tick 到来时发送;其他 tick 可以打印运行结果,但 handlebar 所做更改不会被保存,也不会发送交易信号。
编写创建完模型后,对应模型的基本信息和回测参数进行设置。
# 基本信息-字段描述
字段 描述
名称 填写模型名称
快捷码 默认根据模型名称自动生成拼音首字母拼写,如需自定义可以手动进行更改,用于键盘精灵快速引用模型
说明 简单的说明模型功能
分类 保存当前模型到某个分类下面
位置 模型回测或运行时的位置,有副图、主图叠加、主图三种显示位置
默认周期 点击模型回测或运行时的默认主图周期,可手动切换
默认品种 点击模型回测或运行时的默认主图品种,可手动切换
复权方式 提供不复权、前复权、后复权、等比前复权、等比后复权 5 种复权方式
快速计算 限制计算范围,默认为 0 时模型运行会从模型设置的默认品种(主图)的第一根 K 线开始计算,设置为 n 则从当前 K 线再往前 n 个 K 线开始计算
刷新间隔 用来设置策略运行的时间间隔。设置了刷新间隔,即每隔一段时间策略按照当前行情运行一次
加密公式 加密后的公式只有输入密码才可以查看源代码
凭密码导出公式 此项只有在开启 “加密公式” 后才能生效,生效后只能使用密码导出到本地
用法注释 简短的说明模型使用的一些注意项,可不填
策略编辑器-基本信息
回测模式指策略以历史行情为依据进行运算,投资者可观察该策略在历史行情所获得的年化收益率、夏普比率、最大回撤、信息比率等指标表现。
# 回测参数-字段描述
字段 描述
开始时间
结束时间 设置模型回测时间区间
基准 设置模型收益的参考基准
初始资金 设置模型回测的初始资金
保证金比例 设置期货的保证金比例
滑点 设置回测撮合时的滑点,模拟真实交易的冲击成本
手续费类型 支持按成交额比例或者固定值计算手续费
买入印花税 设置买入印花税比例
卖出印花税 设置卖出印花税比例
最低佣金 设置单笔交易的最低佣金数额
买入佣金 设置买入标的时的佣金比例
平昨佣金 设置股票、期货平昨佣金比例
平今佣金 设置期货平今佣金比例
最大成交比例 控制回测中最大成交量不超过同期成交量*最大成交比例。可以点击此参数旁边的'?'按钮了解详情
策略编辑器-回测参数
使用者也可在参数设置中设置好参数值,参数名为变量名,模型中可以调用。最新值为变量默认值,运行/回测模式使用。
其中最小 / 最大 / 步长项,为遍历参数。初始项可不填。最小 / 最大都是包含在遍历区间内的,如图所示三个变量,测评时会遍历(20-3+1 [160-140/10+1] [-150+250/10+1] 种组合。
策略编辑器-参数设置
点击公式测评,可选择回测模式支持的指标,如单位净值,最大回撤等,作为评价标准。
策略编辑器-公式评测
点击优化,评测结果弹窗显示不同参数变量组合下的回测结果,根据结果选择最优参数组合。可点击所需指标进行排序。需要注意的是,在测评之前,需要针对所选品种和周期补充数据。
# 补充数据
在创建用户的模型之前,用户应使用客户端提供的“数据管理”功能,选择并补充模型所需的相应市场、品种以及对应周期的历史数据。
操作-数据管理
# 策略运行
策略编写完毕后,点击编译,可保存策略。编译按钮在 Python 策略中只起保存功能,不会检查语法与引用的正误。之后点击运行可以看到策略运行效果(如有错误,会在日志输出的位置报错)。
策略编辑器-运行
如当前系统所处界面为“行情”界面或“交易”界面,点击运行之前,需在行情中手动设置好 K 线品种和周期,点击运行后,策略即可在当前主图下运行,如下图所示。
策略运行状态之一
如系统当前界面处于“我的主页”界面或“模型研究”和“模型交易”等非行情界面,点击运行时,会基于策略编辑器 - 基本信息中所设置的默认周期和默认品种运行。
策略运行状态之二
当选择的运行位置为副图时, 如想关闭策略,将主图下方策略运行的附图关闭即可。
关闭运行中的策略之一
关闭运行中的策略之二
当选择的运行位置为主图叠加时,如想关闭策略,在主图上右键单击取消叠加指标即可。
当选择的运行位置为主图时,键盘精灵输入KLINE即可结束模型运行。
关闭运行中的策略之三
点击策略编辑器上放的停止按钮
# 策略调试
如果策略运行不成功,需要进行策略调试这一步。当运行出错时,报错信息会显示在日志输出面板,以供修改调试之用。
策略调试-输出日志
# 独立python进程
注意
在您充分理解软件使用前,不建议开启该功能
勾选此功能后,程序将把代码作为main执行脚本, 不会触发init,handlebar等函数
反之,系统会import策略,按规则触发init handlebar等函数
# 策略回测
对某一策略编译成功后,点击回测,可以通过日志输出查看模型基于历史行情数据回测情况和表现。
在回测之前,需要设置好策略回测运行的主图品种和周期,以及相关的回测参数。回测主图和周期可以在策略编辑器-基本信息中进行设置,回测开始和结束时间、基准、费率等可以在策略编辑器 - 回测参数中设置。
用户在回测前,需根据此策略回测运行的主图、周期和时间,在【数据管理】中对行情数据进行下载补充。如回测时间设置为 20180930 至 20190530 ,运行主图为 SZ.000001 平安银行日线,补充数据时可进行如下设置。
操作-数据管理
如果回测正常的话,主界面会跳转到模型设置的默认标的和默认周期界面,并输出模型绩效分析结果。
策略编辑器-模型回测
此时,可最小化或者关闭【策略编辑器】,并对回测结果进行分析,随着光标在 K 线主图上的移动,右边回测结果展示窗口会动态显示截止光标所在当日的绩效分析结果(包括年化收益,基准年化收益,单位净值,下方差,信息比率,夏普比率,波动率,索提诺比率,阿尔法系数,贝塔系数,跟踪误差,最大回撤,胜率等)、当日买入、当日卖出、持仓列表。
以上列表均可鼠标右键复制和导出数据。
回测结果分析-回测结果随十字光标移动动态展示
如需根据模型生成的买入、卖出列表进行手工交易,则可直接点击“买入'或“卖出'按钮,系统会弹出下单界面由用户进行确认后进行普通交易下单或算法交易下单。交易方式可点击卖出按钮右侧的普通交易进行切换设置。
回测结果买入
副图回测指标:提供图形化的展示,除去绩效分析的相关指标外,用户可以通过编辑模型代码自定义输出一些特色指标,鼠标右键可以选择复制模型运行结果(每一天的数据)。
回测结果分析-附图指标输出
另外,回测结果还提供了持仓分析、历史板块汇总、操作明细、日志输出等信息,方便用户进行深入分析。
说明
持仓分析: 可查看光标所在当天持仓的行业分布,展示在相关行业的市值情况、盈利情况、权重以及股票数量情况,鼠标右键可以复制和导出数据;可切换对比基准,和模型持仓进行对比。
历史板块汇总: 可查看模型自回测日期以来到光标所在日期该模型交易标的的汇总信息,包括累计盈亏、累计交易量、累计交易额、持仓天数等;点击选择板块,可以自行选择其常用板块进行板块的各项数据累计汇总;汇总数据均可以进行排序,鼠标右键可以复制和导出数据。
操作明细: 可查看模型回测的历史每一笔交易的明细。
日志输出: 可用于调试输出模型回测和运行情况。
回测结果分析-绩效分析、当日买入、当日卖出、当日持仓
回测结果分析-持仓分析
回测结果分析-历史品种汇总、历史板块汇总
回测结果分析-操作明细、日志输出
# 回测、运行两种模式的区别
在模型编辑器中,有“回测”和“运行”两个按钮,分别代表两种模式,它们之间的区别如下:
(1)回测模式指策略以历史行情为依据,以回测参数中的开始时间、结束时间为回测时间区间进行运算,投资者可观察该策略在历史行情所获得的年化收益率、夏普比率、最大回撤、信息比率等指标表现。
(2)运行模式指策略根据实时行情信号进行运算,以主图行情开始时间到当前时间为运行区间,进行策略的模拟运行,但不进行真实的委托。
注意
如果需要向模拟/实盘柜台发送真实的委托,请将策略加入到“模型交易”中。
# 配置/获取模拟账号
注意
券商QMT的模拟账号通常联系对应券商解决相关问题
以下均为迅投模拟账号相关指南
迅投模拟账号的格式如下
股票账号:200xxxx
期货账号:100xxxx
期权账号:600xxxx
提示
新用户注册投研账号,可获得 14 天模拟仿真交易体验
VIP权限用户 可以通过 用户中心 - 下载中心 的客户端进行模拟交易,交易支持 股票/期货/股票期权 三个市场
在 投研服务平台
在新窗口打开 登录您的投研账号,如您没有投研账号,请先 注册
在新窗口打开
点击右上角的 用户中心 ,选择下方栏目的 模拟撮合 按钮,可以看到您的 投研模拟账号
VIP权限用户 可以通过 用户中心 - 下载中心 的客户端进行模拟交易,交易支持 股票/期货/股票期权 三个市场
客户端配置账号
# 自动导出交易记录
# 操作界面快捷键
操作 描述
SHIFT + Q 分时 K线 附图变量查看器
SHIFT + G k线 附图组合模型持仓界面
SHIFT + L k线 锁定十字线
SHIFT + S 星空图
CTRL + Windows键 快速唤起多屏
CTRL + O 分时 K线 叠加品种
CTRL + Z 分时 K线 添加自选
CTRL + M 跳转到多股同列界面
CTRL + X 跳转到多周期同列界面
CTRL + ← K线 多股同列 向左快捷移动十字线
CTRL + → K线 多股同列 向右快捷移动十字线
CTRL + R k线 移动十字线到当前界面结尾
CTRL + V k线 切换复权方式为等比前复权/不复权
CTRL + B k线 切换复权方式为等比后复权/不复权
CTRL + A 分时 k线 浏览列表 样板股分析
CTRL + E 分时 k线 浏览列表 预警雷达
ALT + 数字 (1~9) 分时 K线 设置附图指标数量
ALT + ← 分时 K线 显示走势图
ALT + → 分时 K线 显示走势图
F3 切换到上证指数分时图
F4 切换到深证成指分时图
F5 切换到分时/K线图
F6 切换到“我的自选”板块浏览列表
F8 K线 循环切换周期
F10 个股切换财务数据界面,指数切换到成分股界面
# 策略编辑器快捷键
操作 描述
Ctrl + C 复制
Ctrl + X 剪切
Ctrl + V 粘贴
Ctrl + Q 多行注释
Ctrl + Z 撤消
Ctrl + Y 恢复
Ctrl + A 全选
Ctrl + F 键查找对话框启动
Ctrl + D 复制并粘贴当行
Ctrl + L 删除当前行
Ctrl + T 当行向上移动一行
Ctrl + S 保存文件
上次更新:
邀请注册送VIP优惠券
分享下方的内容给好友、QQ群、微信群,好友注册您即可获得VIP优惠券
玩转qmt,上迅投qmt知识库
登录后获取
使用须知 变量约定
File diff suppressed because one or more lines are too long
@@ -0,0 +1,315 @@
常见问题 | 迅投知识库
迅投知识库
首页
API API
投研新手教程
QMT新手教程
内置Python
XtQuant文档
VBA
数据字典 数据字典
快速开始
股票数据
行业概念数据
指数数据
期货数据
期权数据
场内基金数据
债券数据
常见问题
场景化示例
迅投因子
数据浏览器
策略迁移 策略迁移
聚宽策略
客服QQ: 810315303
联系方式: 18309226715
视频教程 视频教程
投研端教程
迅投学堂
在新窗口打开
投研平台
在新窗口打开
迅投社区
在新窗口打开
迅投官网
在新窗口打开
注册
登录
智能助手加载中...
首页
API API
投研新手教程
QMT新手教程
内置Python
XtQuant文档
VBA
数据字典 数据字典
快速开始
股票数据
行业概念数据
指数数据
期货数据
期权数据
场内基金数据
债券数据
常见问题
场景化示例
迅投因子
数据浏览器
策略迁移 策略迁移
聚宽策略
客服QQ: 810315303
联系方式: 18309226715
视频教程 视频教程
投研端教程
迅投学堂
在新窗口打开
投研平台
在新窗口打开
迅投社区
在新窗口打开
迅投官网
在新窗口打开
内置Python
快速开始
使用须知
界面操作
变量约定
数据结构
系统函数
行情函数
交易函数
成交回报实时主推函数
引用函数
绘图函数
枚举常量
完整示例
常见问题
Python环境相关
安装第三方 Python 库报错
启动策略时pandas库报错
对第三方库的支持
系统自带的 Python 环境
第三方库导入指引
业务规则相关
交易所委托数量规则
策略运行相关
在策略没有勾选终端启动后自动运行的情况下,策略自动启动运行
策略回测相关
QMT在回测时如何选择复权方式
交易相关
系统对象 ContextInfo 逐 k 线保存的机制
快速交易参数 quickTrade
下单与回报相关
QMT 下单失败
行情相关
QMT 行情数据基础概念
QMT 行情调用函数对比说明
全推接口和订阅接口的分笔行情没有5档行情,只有最新价
行情中心和交易中心到底有啥区别?
passorder使用对手价下单报错/有误
为什么在handlebar中获取期货tick时,tick是3S一个而非0.5s一个?
为什么在非交易时间段handlebar也会被调用
关于证券状态openint值的详细说明
静态数据问题
报错:[系统]ERROR:******.**获取合约乘数和最小变动价位失败,跳过
软件运行日志相关
如何找到软件运行日志
# Python环境相关
# 安装第三方 Python 库报错
问题描述:
"ImportError:Forbidden:Moduleopenpyxl not in whitelist!"
问题解答:
该报错是由于券商后台开启了 Python 库白名单,若您使用的是券商提供的QMT终端,请联系您的所属券商开通对应 Python 库白名单权限即可。
# 启动策略时pandas库报错
报错信息1 NameError: name 'pandas' is not defined
解答:
该报错是指当前环境下没有找到pandas库
解决方法
请在 设置-模型设置 中检查正确设置了路径,正确路径应指向 {安装目录}\bin.x64
请检查是否已经下载了python环境
报错信息2 AttributeError: module 'pandas' has no attribute 'core'
解答:
该报错是由于在pandas导入中被强行中断导致的
解决方法
重启客户端
# 对第三方库的支持
QMT Python API 提供基于 Python 3.6 规范的标准量化投资策略应用程序接口,本文档示例代码基于 Python 3.6 规范。我司主要通过以下两种方式对外提供:
# 系统自带的 Python 环境
QMT 系统的安装包默认自带 Python 运行环境。用户安装完迅投客户端后,默认可以直接使用 Python 。在这个打包的 Python 环境中,迅投除了提供标准的 Python api 带的库外,还集成了如下一些第三方库:
名称 说明
NumPy NumPy (Numeric Python) 提供了许多高级的数值编程工具,如:矩阵数据类型、矢量处理,以及精密的运算库。专为进行严格的数字处理而产生。
Pandas Python Data Analysis Library 或 Pandas 是基于 NumPy 的一种工具,该工具是为了解决数据分析任务而创建的。Pandas 纳入了大量库和一些标准的数据模型,提供了高效地操作大型数据集所需的工具。Pandas 提供了大量能使我们快速便捷地处理数据的函数和方法。
Patsy 一个线性模型分析和构建工具库。
SciPy SciPy 函数库在 NumPy 库的基础上增加了众多的数学、科学以及工程计算中常用的库函数。例如线性代数、常微分方程数值求解、信号处理、图像处理、稀疏矩阵等等。
Statsmodels Python 的统计建模和计量经济学工具包,包括一些描述统计、统计模型估计和推断。
TA_Lib 称作技术分析库,是一种广泛用在程序化交易中进行金融市场数据的技术分析的函数库。它提供了多种技术分析的函数,可以大大方便我们量化投资中编程工作,内容包括:多种指标,如 ADX, MACD, RSI, 布林轨道等;K 线形态识别,如黄昏之星,锤形线等等。
# 第三方库导入指引
除迅投提供的标准 Python api 和集成的部分第三方库,用户也可自己在 Python 官网下载其他所需第三方库,使用方式如下:
(1)本地安装Python环境,下载python3.6Python官网:https://www.python.org/downloads/release/python-360/
2)安装位置:C:\Python36
​ 新增环境变量:我的电脑--属性--高级系统设置--高级--环境变量---pathC:\Python36;C:\Python36\Scripts
3Python环境检查
Win+R 打开运行,输入 cmd
检查Python变量
4)安装第三方库
​ 安装前先确认客户端安装目录,根据个人电脑进行调整。
​ 安装时若遇到下面错误提示,请执行 pip 更新命令 python -m pip install --upgrade pip
安装三方库命令 pip install openpyxl -t E:\QMT交易端20962\bin.x64\Lib\site-packages
5)检查安装结果
安装位置\bin.x64\Lib\site-packages检查安装库
# 业务规则相关
# 交易所委托数量规则
科创板,连续交易时段限价单笔最大是10万股,市价单笔最大是5万股,盘后定价交易单笔最大量是100万股,200股起,1股递增。
创业板,连续交易时段限价单笔最大30万股,市价单笔最大15万股,100股起,100股递增。
主板,6和0开头的,连续交易时段单笔最大100万股,100股起,100股递增。
# 策略运行相关
# 在策略没有勾选 终端启动后自动运行 的情况下,策略自动启动运行
情况一
策略被运行于行情界面的副图上,随客户端启动被启动
解决方法
在右上角的页面布局中选择 恢复默认布局 ,并重启客户端
情况二
交易日切换/行情断线重连时,所有挂着的模型会被重新运行,这是正常的
# 策略回测相关
# QMT在回测时如何选择复权方式
解答
回测是为了更贴近历史数据,但实际中各类配股、增发的动作,会造成价格的异常波动,为了避免这样的波动对回测的影响,我们推荐用户在回测中使用 等比前复权价 ,这样在回测过程中,无需考虑配股、增发带来的变化,始终以统一标准的价格进行买卖,方便的同时也能得到更贴合历史数据的回测收益和表现。
# 交易相关
# 系统对象 ContextInfo 逐 k 线保存的机制
机制说明
ContextInfo 是由底层维护并传递给 init 、 handlebar 等系统函数的参数,同一个 bar(不是 bar 里面的 tick,下同)内 ContextInfo 本质上是同一个变量且对其进行的修改只会对本次handlebar调用的下文所起作用。 handlebar 里对 ContextInfo 做的修改在该 bar 结束后才会进行保存,也就是说,对 ContextInfo 做的修改会在下一个 bar 体现出来。
具体来说, ContextInfo 不同于一般 python 对象,做了逐 k 线更新设计,盘中主图品种每个 Level 1 分笔到达会触发 handlebar 函数调用,但只有 k 线结束时最后一个分笔触发的 handlebar 调用,对 ContextInfo 的修改才有效。
每次 handlebar 函数调用前会对 ContextInfo 对象进行深拷贝, 下一次分笔行情到来时,如果新的分笔不是新 k 线 bar 第一个分笔,则判断上一个分笔不是k线最后分笔, ContextInfo 对象被回退为之前深拷贝的那个。
ContextInfo 对象逐k线更新机制设计的目的,是为了在盘中时模拟k线的效果,只在k线结束的分笔触发的 handlebar 函数运行时生效一次,丢弃所有其他分笔的修改。
影响
该机制有两个影响,一是在 ContextInfo 对象中存数据每次分笔到达时会被深拷贝,拖慢策略运行;二是 ContextInfo 适用于记录逐k线生效的交易信号( quickTrade 参数传 0 ),不适宜立刻下单的情况。
如不需要模拟k线效果,希望调用交易函数后立刻下单, quickTrade 参数可以传 2 , 下单记录可以用普通的全局变量保存, 不能存在 ContextInfo 对象的属性里(实现可以参考实盘示例7-调整至目标持仓Demo)。
# 快速交易参数 quickTrade
下单函数 passorder 有可选参数快速交易 quickTrade , 默认为 0 。
传 0 ,只在k线结束分笔时调用 passorder 产生有效信号,其他情况调用不产生信号。
传 1 ,在当前k线为最新k线时调用 passorder 函数产生有效信号, 历史k线调用不产生信号。
传 2 ,任何情况下调用 passorder 都产生有效信号,不会丢弃任何一次调用的信号。
如果在定时器注册的回调函数,行情回调函数, after_init 函数中调用下单函数,需要传 2 ,确保不会漏单。
passorder 以外的下单函数不能指定快速交易参数,效果与传 0 的 passorder 一致。
# 下单与回报相关
为保证以尽快的速度执行交易信号, qmt 客户端提供的交易接口是异步的, 以快速交易参数填 2 的 passorder 函数为例,调用后会立刻发出委托, 然后返回。不会等待委托回报, 也不会阻塞python线程的运行。
委托/成交/持仓/账号信息的更新, 是在客户端后台进行的, python策略中无法手动控制。python提供的取账号信息接口 get_trade_detail_data 与四种交易回调函数, 都是从客户端本地缓存中读取数据 / 触发调用,不是调用时查询柜台再返回。客户端本地缓存状态定期接收柜台推送刷新,有交易主推的柜台50ms一次,没有交易主推的柜台1-6秒一次。 不能认为get_trade_detail_data查到的状态是与柜台完全一致的, 比如卖出委托后立刻查询, 不会查到对应委托, 可用资金也不会变多。
实盘策略需要设计盘中保存/更新委托状态的机制。常见的做法是用全局变量字典保存委托状态, 给每一笔委托独立的投资备注作为字典的 key ,委托状态作为字典的 value , 下单后默认设置为待报, 之后查到委托后更新状态。如果某品种股票存在待报状态委托, 暂停该品种后续报单, 防止发生超单的情况。(实现可以参考实盘示例7-调整至目标持仓Demo)
QMT 所有策略是在同一个线程中被调用的,任意一个策略阻塞线程(死循环 sleep 加锁等操作)会导致所有策略的执行被阻塞,所以不能在策略里写等待操作。如需要多线程 / 多进程的用法,可以使用极简模式配合 xtquant 库使用
# QMT 下单失败
检查是否是在模型交易界面,实盘模式运行的策略。模拟模式只显示策略信号,不发出委托。
如运行到交易函数,未看到策略信号,检查交易函数是否使用了快速下单参数( quickTrade ),默认为 0 ,只会在k线结束发出委托,日线及以上周期等于全天不会委托。传 1 时,非历史bar上执行时(ContextInfo.is_last_bar()为True),只要策略模型中调用到就触发下单交易。传 2 ,无论是否是历史bar,运行到交易函数时立刻发出委托。
如果希望盘中出现信号立即下单,建议传 1 ,这种情况下会有策略信号闪烁的风险,需要自己处理;如果希望K线结束下单(信号不闪烁),建议传 0 , 通常情况下不建议传 2
提示
具体到场景:
handlebar逐k线下单, 每次k线结束的分笔生效一次, 传0;
需要在handlebar盘中触发立刻下单, 传1;
定时器/init/after_init与交易回调函数, 行情回调函数内下单, 传2.
如看到实盘的策略信号,未找到对应委托,检查客户端左下角消息提示是否有报错,如有,请根据消息提示的描述修改下单参数
# 行情相关
# QMT 行情数据基础概念
QMT行情数据主要分为三种,包括 本地数据 , 全推数据 , 订阅数据 。
本地数据: 指下载到本地的行情数据加密文件。包括历史数据,适合回测模式使用,对应python接口为 get_market_data_ex(subscribe=False)
在新窗口打开
全推数据: 指客户端启动后, 自动接收,更新的全市场最新数据快照, 包括日线的开高低收,成交量成交额,与五档盘口(在行情界面选择了五档行情时可用五档 具体见行情常规问题3)。支持取全市场品种, 只有最新值,没有历史值, 服务器对交易所下发的数据即时转发,打包增量部分发送给下游客户端 。可以用 get_full_tick 一次性取出当前最新值,也可以用 subscribe_whole_quote 注册回调函数,每次处理增量的部分。 对应python接口为 get_full_tick
在新窗口打开 subscribe_whole_quote
在新窗口打开
订阅: 指向行情服务器订阅指定品种行情, 共有四种周期(分笔 1分钟 5分钟 日线),可以订阅当日数据,当天以前的需要用 down_history_data 下. 订阅有最大数量限制(例如:假设最大数量限制为300个,则可以单独订阅日线300个,若同时订阅日线和五分钟 则各150个),如需订阅超过定义上限,可以在页面右上角,选购 行情vip服务 。对应python接口为 subscribe_quote
在新窗口打开 和 get_market_data_ex(subscribe=True,)
在新窗口打开 其中,使用 get_market_data 或 get_market_data_ex(subscribe=True,) 时客户端会自动订阅传入的品种,不需要额外调用 subscibe_quote ,但这种方式订阅的品种没有订阅号,无法手动反订阅,只能通过停止策略释放可订阅数。
警告
如果超出订阅数量限制,则返回的行情数据会使用前值填充,出现重复值,非正确行情数据。
# QMT 行情调用函数对比说明
down_history_data 下载指定区间的行情数据到本地,存放在硬盘上。效果和界面,点击行情数据下载一致。 开始时间不填时,为增量下载(以本地数据最后一天为开始时间), 填写的话按填写值下载。
get_local_data 取本地数据函数,盘中不会更新,速度快,回测可以用这个函数取。
get_full_tick 取客户端缓存中的最新全推数据。全推数据不包括历史,不用订阅,没有品种数量限制,盘中50ms更新一次,速度快。
subscribe_quote 向服务器订阅股票行情 盘中实时更新 初次订阅耗时长,最大订阅品种数受限. 订阅超过一定数量的品种k线行情不会更新.可订阅四种基本周期(分笔 一分钟 五分钟 日线)行情(如果有 Level-2 行情权限 也可以订 Level-2 的), 同一品种订阅了不同周期累加计数(如订阅浦发银行 1分钟 5分钟 日线行情 算订阅3次). 复数策略订阅同一品种计数不会累加. Level-2 的订阅也会受限,但是和 Level 1 的互不影响。
unsubscribe_quote 按订阅号反订阅行情, 释放可订阅数.
get_market_data_ex 取订阅/本地数据接口。用 subscribe_quote 在 init 函数中先订阅后 subscribe 参数为 True 时,取本地数据和订阅的最新行情。 subscribe 参数传 False 时,可以用来取本地数据,不会订阅。 如股票池超过一定数量,可用 down_history_data + get_local_data + get_full_tick 拼接历史和最新数据替代 get_market_data_ex 。
注意
gmd 系列函数在 init 中运行时,只能读取到本地数据,不会取到最新行情数据,因子, 不建议在 init 中调用使用 gmd 系列函数
警告
不再推荐使用!
set_universe , get_history_data , get_market_data 是早期订阅股票池, 取订阅的行情数据接口. 因为 set_universe 订阅的品种没有订阅号 无法在策略中反订阅, 只能通过停止策略释放订阅数。
# 全推接口和订阅接口的分笔行情没有5档行情,只有最新价
问题描述
get_full_tick , subscribe_while_quote 函数中获取分笔行情没有5档行情,只有最新价。
解决办法
修改行情源对应的 全推行情 级别,见下图
或者
# 行情中心和交易中心到底有啥区别?
行情中心控制单支订阅,例如 subscribe_quote
交易中心影响全推数据,例如 get_full_tick , subscribe_whole_quote
# passorder使用对手价下单报错/有误
问题描述
passorder参数 prType 填写 14 (对手价下单)时,委托价格有误,或信息提示 对手价无效,无法下单!
解决办法
修改行情源对应的 全推行情 级别,见下图
或者
# 为什么在handlebar中获取期货tick时,tick是3S一个而非0.5s一个?
这是由于handlebar函数是 逐K线驱动
在新窗口打开 的, 在实时行情中,handlebar会随着主图标的tick的更新被调用 。
在这个问题场景中,主图的标的通常被设置为股票,而股票的tick通常是3s一个,这就导致handlebar函数3s才被调用一次
解决方法
使用 定时器(run_time)
在新窗口打开 进行计算
使用 订阅推送(subscribe)
在新窗口打开 ,在回调函数中进行计算
如果需要在handlebar中进行期货策略编写,建议将主图设置为期货品种,来保证handlebar调用频率
# 为什么在非交易时间段handlebar也会被调用
handlebar 受行情数据推送驱动,在非交易时段,行情服务会做一系列准备工作,其中可能伴随着服务重启,在重启后为了保证数据齐全,客户端会重新订阅数据,这时服务会推送最新的数据,客户端会把推送的最新数据更新至缓存,并向上层策略推送更新,也就是触发 handlebar 执行
这个合并数据的驱动执行只会在最新一根bar而不会在历史范围,策略可以根据需要处理这次推送或直接根据交易时间跳过这个驱动,例如判断time小于09:15则直接return
# 关于证券状态 openint 值的详细说明
沪市
时间段 状态 编码
9:15 - 9:25 盘前集合竞价 12
9:25 - 14:57 盘中连续竞价 13
14:57 - 15:00 盘后集合竞价 18
15:00 收盘状态 15
15:05 - 15:30 盘后定价 22
15:30 盘后定价结束 23
上一状态后 收盘状态 15
深市
时间段 状态 编码
9:15 - 9:25 盘前集合竞价 12
9:25 - 9:30 休市 14
9:30 - 11:30 盘中连续竞价 13
11:30 - 13:00 休市 14
13:00 - 14:57 盘中连续竞价 13
14:57 - 15:00 盘后集合竞价 18
15:00 发收盘状态 15
15:05 - 15:30 盘后定价 22
15:30 盘后定价结束 23
上一状态后 收盘状态 15
# 静态数据问题
# 报错:[系统]ERROR******.**获取合约乘数和最小变动价位失败,跳过
点击右下角【 行情 】按钮,选择【 智能下载 】,数据选项下拉框勾选【 过期合约列表 】点击该面板右下角【 开始 】,待过期合约数据补充完毕后,即可正常获取过期合约数据。
# 软件运行日志相关
# 如何找到软件运行日志
Log文件通常在安装目录下的 .\userdata\log 文件夹中,在 .\userdata\log 文件夹中,你可能会看到一个或者多个 log 文件,通常以 '.log' 作为扩展名。这些文件将包含软件运行时的详细情况。
投研:{安装目录}\userdata\log
说明
XtClient_20210922.log - 客户端常规日志
XtClient_datasource_20210922.log - 行情数据日志
XtClient_Formula_20210922.log - 策略运行日志
XtClient_FormulaOutput.log - 策略输出日志
QMT{安装目录}\userdata\log
说明
XtClient_20210922.log - 客户端常规日志
XtClient_Formula_20210922.log - 策略运行日志
XtClient_FormulaOutput.log - 策略输出日志
XtClient_PerformanceFile_20210922.log - 客户端流程节点日志
极简模式:{安装目录}\userdata_mini\log
说明
XtMiniQuote_20210917.log - 行情策略模块日志
XtMiniQmt_20210917.log - 客户端常规日志
XtMiniQmt_perform_20210917.log - 客户端流程节点日志
上次更新:
邀请注册送VIP优惠券
分享下方的内容给好友、QQ群、微信群,好友注册您即可获得VIP优惠券
玩转qmt,上迅投qmt知识库
登录后获取
完整示例
File diff suppressed because one or more lines are too long
@@ -0,0 +1,268 @@
引用函数 | 迅投知识库
迅投知识库
首页
API API
投研新手教程
QMT新手教程
内置Python
XtQuant文档
VBA
数据字典 数据字典
快速开始
股票数据
行业概念数据
指数数据
期货数据
期权数据
场内基金数据
债券数据
常见问题
场景化示例
迅投因子
数据浏览器
策略迁移 策略迁移
聚宽策略
客服QQ: 810315303
联系方式: 18309226715
视频教程 视频教程
投研端教程
迅投学堂
在新窗口打开
投研平台
在新窗口打开
迅投社区
在新窗口打开
迅投官网
在新窗口打开
注册
登录
智能助手加载中...
首页
API API
投研新手教程
QMT新手教程
内置Python
XtQuant文档
VBA
数据字典 数据字典
快速开始
股票数据
行业概念数据
指数数据
期货数据
期权数据
场内基金数据
债券数据
常见问题
场景化示例
迅投因子
数据浏览器
策略迁移 策略迁移
聚宽策略
客服QQ: 810315303
联系方式: 18309226715
视频教程 视频教程
投研端教程
迅投学堂
在新窗口打开
投研平台
在新窗口打开
迅投社区
在新窗口打开
迅投官网
在新窗口打开
内置Python
快速开始
使用须知
界面操作
变量约定
数据结构
系统函数
行情函数
交易函数
成交回报实时主推函数
引用函数
ext_data - 获取扩展数据
ext_data_rank - 获取引用的扩展数据的数值在所有品种中的排名
ext_data_rank_range - 获取引用的扩展数据的数值在指定时间区间内所有品种中的排名
ext_data_range - 获取扩展数据在指定时间区间内的值
get_factor_value - 获取因子数据
get_factor_rank - 获取引用的因子数据的数值在所有品种中排名
获取引用的 VBA 模型运行的结果
(投研版QMT) 获取引用的 VBA 模型运行的结果
绘图函数
枚举常量
完整示例
常见问题
# ext_data - 获取扩展数据
获取扩展数据
调用方法: ext_data(extdataname, stockcode, deviation, ContextInfo)
参数:
参数名 类型 说明 提示
extdataname string 扩展数据名
stockcode string 证券代码 形式如 '600000.SH'
deviation number K 线偏移 0:不偏移,N:向右偏移N,-N:向左偏移N
ContextInfo pythonObj Python 对象 ython 对象,这里必须是 ContextInfo
返回:  number
** 示例:**
#coding:gbk
def init ( ContextInfo ):
print (ext_data( ' CR ' , ' 600000.SH ' , 0 , ContextInfo))
# ext_data_rank - 获取引用的扩展数据的数值在所有品种中的排名
获取引用的扩展数据的数值在所有品种中的排名
调用方法: ext_data_rank(extdataname, stockcode, deviation, ContextInfo)
参数:
参数名 类型 说明 提示
extdataname string 扩展数据名
stockcode string 证券代码 形式如 '600000.SH'
deviation number K 线偏移 0:不偏移,N:向右偏移N,-N:向左偏移N
ContextInfo pythonObj Python 对象 ython 对象,这里必须是 ContextInfo
返回:  number
** 示例:**
#coding:gbk
def init ( ContextInfo ):
print (ext_data_rank( ' mycci ' , ' 600000.SH ' , 0 , ContextInfo))
# ext_data_rank_range - 获取引用的扩展数据的数值在指定时间区间内所有品种中的排名
获取引用的扩展数据的数值在指定时间区间内所有品种中的排名
** 调用方法: ** ext_data_rank_range(extdataname, stockcode, begintime, endtime, ContextInfo)
参数:
参数名 类型 说明 提示
extdataname string 扩展数据名
stockcode string 证券代码 形式如 '600000.SH'
begintime string 区间的起始时间 格式为 '2016-08-02 12:12:30'(包括该时间点在内)
endtime string 区间的结束时间 格式为 '2017-08-02 12:12:30' (包括该时间点在内)
ContextInfo pythonObj Python对象 Python 对象,这里必须是 ContextInfo
返回:  pythonDict
** 示例:**
#coding:gbk
def init ( ContextInfo ):
print (ext_data_rank_range( ' mycci ' , ' 600000.SH ' , ' 2022-08-02 12:12:30 ' , ' 2023-08-02 12:12:30 ' , ContextInfo))
# ext_data_range - 获取扩展数据在指定时间区间内的值
获取扩展数据在指定时间区间内的值
调用方法: ext_data_range(extdataname, stockcode, begintime, endtime, ContextInfo)
参数:
参数名 类型 说明 提示
extdataname string 扩展数据名
stockcode string 证券代码 形式如 '600000.SH'
begintime string 区间的起始时间 格式为 '2016-08-02 12:12:30'(包括该时间点在内)
endtime string 区间的结束时间 格式为 '2017-08-02 12:12:30' (包括该时间点在内)
ContextInfo pythonObj Python对象 Python 对象,这里必须是 ContextInfo
返回:  pythonDict
示例:
#coding:gbk
def init ( ContextInfo ):
print (ext_data_range( ' mycci ' , ' 600000.SH ' , ' 2022-08-02 12:12:30 ' , ' 2023-08-02 12:12:30 ' , ContextInfo))
# get_factor_value - 获取因子数据
获取因子数据
调用方法: get_factor_value(factorname, stockcode, deviation, ContextInfo)
参数:
参数名 类型 说明 提示
factorname string 因子名称
stockcode string 证券代码 形式如 '600000.SH'
deviation number K 线偏移 0 不偏移,N 向右偏移 N,-N 向左偏移 N
ContextInfo pythonObj Python对象 Python 对象,这里必须是 ContextInfo
返回:  number
示例:
#coding:gbk
def init ( ContextInfo ):
print (get_factor_value( ' zzz ' , ' 600000.SH ' , 0 , ContextInfo))
# get_factor_rank - 获取引用的因子数据的数值在所有品种中排名
获取引用的因子数据的数值在所有品种中排名
调用方法: get_factor_rank(factorname, stockcode, deviation, ContextInfo)
参数:
参数名 类型 说明 提示
factorname string 因子名称
stockcode string 证券代码 形式如 '600000.SH'
deviation number K 线偏移 0 不偏移,N 向右偏移 N,-N 向左偏移 N
ContextInfo pythonObj Python对象 Python 对象,这里必须是 ContextInfo
示例:
#coding:gbk
def init ( ContextInfo ):
print (get_factor_rank( ' zzz ' , ' 600000.SH ' , 0 , ContextInfo))
# 获取引用的 VBA 模型运行的结果
(不推荐)券商版qmt函数:call_vba
更推荐函数(投研版qmt):
获取历史数据 (必须先建vba公式) call_formula
在新窗口打开
订阅实时+历史数据 (必须先建vba公式) subscribe_formula
在新窗口打开 更多实时和历史数据调用的示例: Python调用VBA因子公式的案例
在新窗口打开 -订阅实时+历史数据(直接在python中写VBA,VBA公式不用先建)[get_vba_func_result]
获取引用的 VBA 模型运行的结果
提示
注意
使用该函数时需补充好本地 K 线或分笔数据
调用方法:   call_vba(factorname, stockcode,[period, dividend_type, barpos],ContextInfo)
参数:
参数名 类型 说明 提示
factorname string 因子名称
stockcode string 证券代码 形式如 '600000.SH'
period string K 线偏移 可缺省,默认为当前主图周期线型
dividend_type string 复权方式 可缺省,默认当前图复权方式,具体可选值如下
barpos number 对应 bar 下标 可缺省,默认当前主图调用到的 bar 的对应下标xtInfo
ContextInfo pythonObj Python 对象 Python 对象,这里必须是 ContextInfo
period 可选值:
'tick':分笔线 '1d':日线 '1m'1分钟线 '3m'3分钟线 '5m'5分钟线 '15m'15分钟线 '30m'30分钟线 '1h':小时线 '1w':周线 '1mon':月线 '1q':季线 '1hy':半年线 '1y':年线
dividend_type 可选值:
'none':不复权 'front':向前复权 'back':向后复权 'front_ratio':等比向前复权 'back_ratio':等比向后复权
返回:  number
示例:
python
#coding:gbk
def init ( ContextInfo ):
print (call_vba( ' MA.ma1 ' , ' 600036.SH ' , ContextInfo))
@ tab 返回值
-1.0
# (投研版QMT) 获取引用的 VBA 模型运行的结果
更推荐函数(投研版QMT):
订阅实时+历史数据(直接在python中写VBA,VBA公式不用先建)[get_vba_func_result]
获取引用的 VBA 模型运行的结果
提示
注意
使用该函数时需补充好本地 K 线或分笔数据
调用方法:   get_vba_func_result(func,stock_code,period='1d',start_time='',end_time='',count=-1,dividend_type=None,extend_param={},subscribe=True)
参数:
参数名 类型 说明 提示
func str或者list[str] vba函数
stockcode string 合约品种 形式如 '600000.SH'
period string 周期 可缺省,默认为当前主图周期线型
start_time string 开始时间 形式如 %Y%m%d 或 %Y%m%d%H%M%S
end_time string 结束时间 形式如 %Y%m%d 或 %Y%m%d%H%M%S
count int 条数 模型运行范围为向前 count 根 bar,默认为 -1 运行所有 bar
dividend_type string 除权类型 复权方式,默认为主图除权方式,可选范围:'none':不复权,'front':向前复权,'back':向后复权,'front_ratio':等比向前复权,'back_ratio':等比向后复权
extend_param dict 扩展参数 模型的入参,{"模型名:参数名":参数值},例如在跑模型MA时,{'MA:n1':1};入参可以添加__basket:dict,组合模型的股票池权重,形如{'__basket':{'600000.SH':0.06,'000001.SZ':0.01}},如果在跑一个模型1的时候,模型1调用了模型2,如果只想修改模型2的参数可以传{'模型2:参数':参数值}
subscribe bool 是否订阅数据 True:历史加实时, False: 历史
period 可选值:
'tick':分笔线 '1d':日线 '1m'1分钟线 '3m'3分钟线 '5m'5分钟线 '15m'15分钟线 '30m'30分钟线 '1h':小时线 '1w':周线 '1mon':月线 '1q':季线 '1hy':半年线 '1y':年线
dividend_type 可选值:
'none':不复权 'front':向前复权 'back':向后复权 'front_ratio':等比向前复权 'back_ratio':等比向后复权
示例:
python 返回值
# coding:GBK
def init ( C ):
fml = """
基准:=-1//-1
档位:=1
bb:getoptcodebyno('','C',1,档位,基准,10,6);
if bb = '' then exit;
//qh1:convindex('',1);//返回IFIC00
ss:getoptcodebyno('','P',1,-1*档位,基准,1,0,6);
xx:getexerciseinterval('SH510050',1),nodraw;
x:deliveryinterval(),LINETHICK0;//,noaxis;//
bb1:STKNAME(bb);
ss1:STKNAME(ss);
认沽今收:callstock(ss,vtclose,-1,0)noaxis();
认购今收:callstock(bb,vtclose,-1,0),noaxis();
"""
d = get_vba_func_result(fml, ' 510050.SH ' , ' 1d ' , count = 10 )
print (d)
start simulation mode
bb bb1 ss ss1 time \
20260702 SHO10011685 50ETF购7月3000 SHO10011694 50ETF沽7月3000 1782921600000
x xx 认沽今收 认购今收
20260702 11.0 14.0 0.0598 0.0613
上次更新:
邀请注册送VIP优惠券
分享下方的内容给好友、QQ群、微信群,好友注册您即可获得VIP优惠券
玩转qmt,上迅投qmt知识库
登录后获取
成交回报实时主推函数 绘图函数
File diff suppressed because one or more lines are too long
@@ -0,0 +1,122 @@
相关说明 | 迅投知识库
迅投知识库
首页
API API
投研新手教程
QMT新手教程
内置Python
XtQuant文档
VBA
数据字典 数据字典
快速开始
股票数据
行业概念数据
指数数据
期货数据
期权数据
场内基金数据
债券数据
常见问题
场景化示例
迅投因子
数据浏览器
策略迁移 策略迁移
聚宽策略
客服QQ: 810315303
联系方式: 18309226715
视频教程 视频教程
投研端教程
迅投学堂
在新窗口打开
投研平台
在新窗口打开
迅投社区
在新窗口打开
迅投官网
在新窗口打开
注册
登录
智能助手加载中...
首页
API API
投研新手教程
QMT新手教程
内置Python
XtQuant文档
VBA
数据字典 数据字典
快速开始
股票数据
行业概念数据
指数数据
期货数据
期权数据
场内基金数据
债券数据
常见问题
场景化示例
迅投因子
数据浏览器
策略迁移 策略迁移
聚宽策略
客服QQ: 810315303
联系方式: 18309226715
视频教程 视频教程
投研端教程
迅投学堂
在新窗口打开
投研平台
在新窗口打开
迅投社区
在新窗口打开
迅投官网
在新窗口打开
内置Python
快速开始
使用须知
界面操作
变量约定
数据结构
系统函数
行情函数
交易函数
成交回报实时主推函数
引用函数
绘图函数
枚举常量
完整示例
常见问题
提示
QMT所使用的Python版本为 3.6.8 - 64位
# 手册说明
# 对第三方库的支持
QMT Python API 提供基于 Python 3.6 规范的标准量化投资策略应用程序接口,本文档示例代码基于 Python 3.6 规范。我司主要通过以下两种方式对外提供:
# 系统自带的 Python 环境
QMT 系统的安装包默认自带 Python 运行环境。用户安装完迅投客户端后,默认可以直接使用 Python 。在这个打包的 Python 环境中,迅投除了提供标准的 Python api 带的库外,还集成了如下一些第三方库:
名称 说明
NumPy NumPy (Numeric Python) 提供了许多高级的数值编程工具,如:矩阵数据类型、矢量处理,以及精密的运算库。专为进行严格的数字处理而产生。
Pandas Python Data Analysis Library 或 Pandas 是基于 NumPy 的一种工具,该工具是为了解决数据分析任务而创建的。Pandas 纳入了大量库和一些标准的数据模型,提供了高效地操作大型数据集所需的工具。Pandas 提供了大量能使我们快速便捷地处理数据的函数和方法。
Patsy 一个线性模型分析和构建工具库。
SciPy SciPy 函数库在 NumPy 库的基础上增加了众多的数学、科学以及工程计算中常用的库函数。例如线性代数、常微分方程数值求解、信号处理、图像处理、稀疏矩阵等等。
Statsmodels Python 的统计建模和计量经济学工具包,包括一些描述统计、统计模型估计和推断。
TA_Lib 称作技术分析库,是一种广泛用在程序化交易中进行金融市场数据的技术分析的函数库。它提供了多种技术分析的函数,可以大大方便我们量化投资中编程工作,内容包括:多种指标,如 ADX, MACD, RSI, 布林轨道等;K 线形态识别,如黄昏之星,锤形线等等。
# 第三方库导入指引
除迅投提供的标准 Python api 和集成的部分第三方库,用户也可自己在 Python 官网下载其他所需第三方库,使用方式如下:
(1)本地安装Python环境,下载python3.6Python官网:https://www.python.org/downloads/release/python-360/
2)安装位置:C:\Python36
​ 新增环境变量:我的电脑--属性--高级系统设置--高级--环境变量---pathC:\Python36;C:\Python36\Scripts
3Python环境检查
Win+R 打开运行,输入 cmd
检查Python变量
4)安装第三方库
​ 安装前先确认客户端安装目录,根据个人电脑进行调整。
​ 安装时若遇到下面错误提示,请执行 pip 更新命令 python -m pip install --upgrade pip
安装三方库命令 pip install openpyxl -t E:\QMT交易端20962\bin.x64\Lib\site-packages
5)检查安装结果
安装位置\bin.x64\Lib\site-packages检查安装库
上次更新:
邀请注册送VIP优惠券
分享下方的内容给好友、QQ群、微信群,好友注册您即可获得VIP优惠券
玩转qmt,上迅投qmt知识库
登录后获取
File diff suppressed because one or more lines are too long
@@ -0,0 +1,357 @@
快速开始 | 迅投知识库
迅投知识库
首页
API API
投研新手教程
QMT新手教程
内置Python
XtQuant文档
VBA
数据字典 数据字典
快速开始
股票数据
行业概念数据
指数数据
期货数据
期权数据
场内基金数据
债券数据
常见问题
场景化示例
迅投因子
数据浏览器
策略迁移 策略迁移
聚宽策略
客服QQ: 810315303
联系方式: 18309226715
视频教程 视频教程
投研端教程
迅投学堂
在新窗口打开
投研平台
在新窗口打开
迅投社区
在新窗口打开
迅投官网
在新窗口打开
注册
登录
智能助手加载中...
首页
API API
投研新手教程
QMT新手教程
内置Python
XtQuant文档
VBA
数据字典 数据字典
快速开始
股票数据
行业概念数据
指数数据
期货数据
期权数据
场内基金数据
债券数据
常见问题
场景化示例
迅投因子
数据浏览器
策略迁移 策略迁移
聚宽策略
客服QQ: 810315303
联系方式: 18309226715
视频教程 视频教程
投研端教程
迅投学堂
在新窗口打开
投研平台
在新窗口打开
迅投社区
在新窗口打开
迅投官网
在新窗口打开
内置Python
快速开始
一、概述
二、场景需求
回测模型
实盘模型
三、运行机制对比
逐 K 线驱动:handlebar
事件驱动 subscribe 订阅推送
定时任务 :run_time 定时运行
不同机制匹配不同场景需求
四、逐 K 线驱动(handlebar)示例
回测示例-基于 handlebar
实盘示例-基于 handlebar
五、事件驱动(subscribe)示例
实盘示例-基于 subscribe
六、定时任务(run_time)示例
实盘示例-基于 run_time
使用须知
界面操作
变量约定
数据结构
系统函数
行情函数
交易函数
成交回报实时主推函数
引用函数
绘图函数
枚举常量
完整示例
常见问题
# 一、概述
QMT 极速策略交易系统,以下简称 QMT 系统 ,内置了 3.6 版本 的 python 运行环境,提供 行情数据 与 交易下单 两大核心功能。通过编写 python 脚本,可以完成指标计算,策略编写,策略回测,实盘下单等需求。
# 二、场景需求
QMT 系统支持 回测模型 与 实盘模型 。
回测模型: 指在历史 k 线上,自左向右逐根遍历 k 线,以模拟的资金账号记录每日的买卖信号,持仓盈亏,最终展示策略在历史上的净值走势结果。
实盘模型: 指在盘中收取最新的动态行情,即时发送买卖信号到交易所,判断委托状态,需要实时重复报撤的模型。
两类模型分别有各自的注意点:
# 回测模型
回测是遍历固定的历史数据:
首先需要下载历史行情,首次下载可以在界面左上角,点击 操作 ,选择 数据管理 补充行情,选择回测的周期,如 日线 ,所需的板块数据,如 沪深A股板块 ,时间范围选择 全部 ,下载完整历史行情
其次设置每日定时更新,可以点击客户端右下角 行情 按钮,在 批量下载 界面选择需要每天更新的数据,勾选 定时下载 选项,之后每天在指定时间会自动下载行情数据到本地
回测模型取本地数据遍历,不需要向服务器订阅实时行情,应使用 get_market_data_ex 函数,指定 subscribe 参数为 False ,来读取本地行情数据。
回测模型的撮合规则为,指定交易价格在当前k线高低点间的,按指定价格撮合,超过高低点的,按当前 k 线收盘价撮合。委托数量大于可用数量时,按可用数量撮合。
回测模型右侧的基本信息,如默认周期,默认主图,在 我的界面 点击回测时会生效。在行情界面k线下点击回测,以当前 k 线的周期,品种为准。回测必须以 副图模式 执行,不要选择主图 /主图叠加.
# 实盘模型
当你回测结束,你需要开始实盘模型,注意这里提到的实盘,指的是接收未来 K 线的数据,生成策略信号,进行交易下单。
提示
实盘模型也分 模拟柜台模拟交易 和 真实柜台实盘交易 两种。具体请参考 如何配置账号
在新窗口打开
你要运行实盘模型,QMT 系统提供两种交易模式:
默认的交易模式为 逐 k 线生效 ( passorder 函数 快速交易 quicktrade 参数填 0 即默认值),适用与需要在盘中模拟历史上逐 k 线的效果需求。例如选择一分钟周期,将下单判断,下单函数放在 handlebar 函数内,盘中主图每个分笔 (三秒一次)会触发一次 handlebar 函数调用,系统会暂存当前 handlebar 产生的下单信号。三秒后下一个分笔到达时,如果是新的一分钟 k 线的第一个分笔,判断上一个分笔为前一根k线最后分笔,会将暂存的交易信号发送给交易所,完成交易。如到达的下一个分笔不是新一根 k 线的,则判定当前 k 线未完成,丢弃暂存的交易信号。1 分钟 k 线情形,每根k线内会有 20 个分笔,前 19 个分笔产生的信号会被丢弃,最后一个分笔的信号,会在下一根k线,首个分笔到达时,延迟三秒发出。系统自带的 ContxtInfo 也做了同样的等待,回退处理,逐 k 线模式的交易记录可以保存在 ContextInfo 对象的属性中。详细说明参见 常见问题:系统对象ContextInfo 逐K线保存的机制
QMT 系统也支持立即下单的交易模式, passorder 函数的 快速交易 quicktrade 参数填 2 ,可以在运行后立刻发出委托,不对信号进行等待,丢弃的操作。此时需要用普通的全局变量(如自定义一个 Class a() )保存委托状态,不能存在 ContextInfo 的属性里。参见 使用快速交易参数委托 、 调整至目标持仓Demo
实盘的撮合规则以交易所为准。股票品种的话,价格不能超过 2% 的价格笼子否则废单。数量超过可用数量时会废单。
实盘模型需要在模型交易界面执行。模型交易界面,选择新建策略交易,添加需要的模型。运行模式可以选择 模拟 或 实盘 。
选择 模拟信号模式 ,在策略信号界面显示买卖信号,不实际发出委托。具体请参考 模拟信号模式
选择 实盘交易模式 ,显示的策略信号会实际发出到交易所。具体请参考 实盘交易模式
提示
运行模式的 模拟 和 实盘 ,与您使用的账号实际是 实盘账号(真实交易所柜台) 或是 模拟账号(模拟交易柜台) 无关。相关账号申请需要联系您做所在券商的工作人员,或者购买 投研端账号
在新窗口打开 获取模拟柜台撮合服务。
# 三、运行机制对比
QMT 系统提供两大类(事件驱动与定时任务),共三种运行机制。
# 逐 K 线驱动: handlebar
handlebar 是 主图历史 k 线 + 盘中订阅推送 。运行开始时,所选周期历史 k 线从左向右每根触发一次 handlebar 函数调用。盘中时,主图品种每个新分笔数据到达,触发一次 handlebar 函数调用。
提示
盘中分笔驱动,但是逐 K 线生效。请参考 常见问题:系统对象ContextInfo 逐K线保存的机制
# 事件驱动 subscribe 订阅推送
盘中订阅指定品种的分笔数据,新分笔到达时,触发指定的回调函数。
# 定时任务 run_time 定时运行
指定固定的时间间隔,持续触发指定的回调函数.
# 不同机制匹配不同场景需求
机制 分类 特点 匹配需求
逐 K 线运行( handlebar ) 事件驱动 同时支持历史回测和盘中可模拟逐K线效果 在实盘中模拟逐K线运行的效果
订阅推送( subscribe ) 事件驱动 盘中行情分笔触发函数调用 盘中随分笔行情判断交易
定时运行( run_time ) 定时任务 固定间隔触发调用 盘中固定时间间隔判断交易
# 四、逐 K 线驱动(handlebar)示例
因此,结合不同场景需求(回测或实盘),针对不同的机制(定时任务或事件驱动),我们分别给出回测与实盘的完整示例,复制到策略编辑器中即可使用。
在编写策略前,有以下注意事项:
警告
在编写一个策略时,首先需要在代码的最前一行写上: #coding:gbk 统一脚本的编码格式是 GBK
缩进需要统一 全部统一为 ···· 或者 ->
# 回测示例-基于 handlebar
回测的操作流程请参考: 界面操作-策略回测
复制代码以下代码到策略编辑器:
python
#coding:gbk
#导入常用库
import pandas as pd
import numpy as np
import talib
#示例说明:本策略,通过计算快慢双均线,在金叉时买入,死叉时做卖出 点击回测运行 主图选择要交易的股票品种
def init ( C ):
#init handlebar函数的入参是ContextInfo对象 可以缩写为C
#设置测试标的为主图品种
C.stock = C.stockcode + ' . ' + C.market
#line1和line2分别为两条均线期数
C.line1 = 10 #快线参数
C.line2 = 20 #慢线参数
#accountid为测试的ID 回测模式资金账号可以填任意字符串
C.accountid = " testS "
def handlebar ( C ):
#当前k线日期
bar_date = timetag_to_datetime(C.get_bar_timetag(C.barpos), ' %Y%m %d %H%M%S ' )
#回测不需要订阅最新行情使用本地数据速度更快 指定subscribe参数为否. 如果回测多个品种 需要先下载对应周期历史数据
local_data = C.get_market_data_ex([ ' close ' ], [C.stock], end_time = bar_date, period = C.period, count = max (C.line1, C.line2), subscribe = False )
close_list = list (local_data[C.stock].iloc[ : , 0 ])
#将获取的历史数据转换为DataFrame格式方便计算
#如果目前未持仓,同时快线穿过慢线,则买入8成仓位
if len (close_list) < 1 :
print (bar_date, ' 行情不足 跳过 ' )
line1_mean = round (np.mean(close_list[ - C.line1 : ]), 2 )
line2_mean = round (np.mean(close_list[ - C.line2 : ]), 2 )
print ( f " { bar_date } 短均线 { line1_mean } 长均线 { line2_mean } " )
account = get_trade_detail_data( ' test ' , ' stock ' , ' account ' )
account = account[ 0 ]
available_cash = int (account.m_dAvailable)
holdings = get_trade_detail_data( ' test ' , ' stock ' , ' position ' )
holdings = {i.m_strInstrumentID + ' . ' + i.m_strExchangeID : i.m_nVolume for i in holdings}
holding_vol = holdings[C.stock] if C.stock in holdings else 0
if holding_vol == 0 and line1_mean > line2_mean:
vol = int (available_cash / close_list[ - 1 ] / 100 ) * 100
#下单开仓
passorder( 23 , 1101 , C.accountid, C.stock, 5 , - 1 , vol, C)
print ( f " { bar_date } 开仓" )
C.draw_text( 1 , 1 , ' 开 ' )
#如果目前持仓中,同时快线下穿慢线,则全部平仓
elif holding_vol > 0 and line1_mean < line2_mean:
#状态变更为未持仓
C.holding = False
#下单平仓
passorder( 24 , 1101 , C.accountid, C.stock, 5 , - 1 , holding_vol, C)
print ( f " { bar_date } 平仓" )
C.draw_text( 1 , 1 , ' 平 ' )
基础信息设置 请参考 基础信息-字段描述
回测参数设置 请参考 回测参数-字段描述
# 实盘示例-基于 handlebar
实盘的操作流程请参考: 界面操作-模型交易
复制代码以下代码到策略编辑器:
python
#coding:gbk
# 导入包
import pandas as pd
import numpy as np
import datetime
"""
示例说明:双均线实盘策略,通过计算快慢双均线,在金叉时买入,死叉时做卖出
"""
class a ():
pass
A = a() #创建空的类的实例 用来保存委托状态
def init ( C ):
A.stock = C.stockcode + ' . ' + C.market #品种为模型交易界面选择品种
A.acct = account #账号为模型交易界面选择账号
A.acct_type = accountType #账号类型为模型交易界面选择账号
A.amount = 10000 #单笔买入金额 触发买入信号后买入指定金额
A.line1 = 17 #快线周期
A.line2 = 27 #慢线周期
A.waiting_list = [] #未查到委托列表 存在未查到委托情况暂停后续报单 防止超单
A.buy_code = 23 if A.acct_type == ' STOCK ' else 33 #买卖代码 区分股票 与 两融账号
A.sell_code = 24 if A.acct_type == ' STOCK ' else 34
print ( f '双均线实盘示例 { A.stock } { A.acct } { A.acct_type } 单笔买入金额 { A.amount } ' )
def handlebar ( C ):
#跳过历史k线
if not C.is_last_bar():
return
now = datetime.datetime.now()
now_time = now.strftime( ' %H%M%S ' )
# 跳过非交易时间
if now_time < ' 093000 ' or now_time > " 150000 " :
return
account = get_trade_detail_data(A.acct, A.acct_type, ' account ' )
if len (account) == 0 :
print ( f '账号 { A.acct } 未登录 请检查' )
return
account = account[ 0 ]
available_cash = int (account.m_dAvailable)
#如果有未查到成交 查询成交
if A.waiting_list:
found_list = []
deals = get_trade_detail_data(A.acct, A.acct_type, ' deal ' )
for deal in deals:
if deal.m_strRemark in A.waiting_list:
found_list.append(deal.m_strRemark)
A.waiting_list = [i for i in A.waiting_list if i not in found_list]
if A.waiting_list:
print ( f "当前有未查到委托 { A.waiting_list } 暂停后续报单" )
return
holdings = get_trade_detail_data(A.acct, A.acct_type, ' position ' )
holdings = {i.m_strInstrumentID + ' . ' + i.m_strExchangeID : i.m_nCanUseVolume for i in holdings}
#获取行情数据
data = C.get_market_data_ex([ " close " ],[A.stock], period = ' 1d ' , count = max (A.line1, A.line2) + 1 )
close_list = data[A.stock].values
if len (close_list) < max (A.line1, A.line2) + 1 :
print ( ' 行情长度不足(新上市或最近有停牌) 跳过运行 ' )
return
pre_line1 = np.mean(close_list[ - A.line1 - 1 : - 1 ])
pre_line2 = np.mean(close_list[ - A.line2 - 1 : - 1 ])
current_line1 = np.mean(close_list[ - A.line1 : ])
current_line2 = np.mean(close_list[ - A.line2 : ])
#如果快线穿过慢线,则买入委托 当前无持仓 买入
vol = int (A.amount / close_list[ - 1 ] / 100 ) * 100 #买入数量 向下取整到100的整数倍
if A.amount < available_cash and vol >= 100 and A.stock not in holdings and pre_line1 < pre_line2 and current_line1 > current_line2:
#下单开仓 ,参数说明可搜索PY交易函数 passorder
msg = f "双均线实盘 { A.stock } 上穿均线 买入 { vol } 股"
passorder(A.buy_code, 1101 , A.acct, A.stock, 14 , - 1 , vol, ' 双均线实盘 ' , 2 , msg, C)
print (msg)
A.waiting_list.append(msg)
#如果快线下穿慢线,则卖出委托
if A.stock in holdings and holdings[A.stock] > 0 and pre_line1 > pre_line2 and current_line1 < current_line2:
msg = f "双均线实盘 { A.stock } 下穿均线 卖出 { holdings[A.stock] } 股"
passorder(A.sell_code, 1101 , A.acct, A.stock, 14 , - 1 , holdings[A.stock], ' 双均线实盘 ' , 2 , msg, C)
print (msg)
A.waiting_list.append(msg)
警告
对于立刻下单的模型需要用普通的全局变量来保存状态不能ContextInfo对象存详细说明参考 常见问题:系统对象ContextInfo 逐K线保存的机制
更多示例请参见 完整示例
# 五、事件驱动(subscribe)示例
# 实盘示例-基于 subscribe
python
#coding:gbk
class a (): pass
A = a()
A.bought_list = []
account = ' testaccount '
def init ( C ):
#下单函数的参数需要 ContextInfo对象 在init中定义行情回调函数 可以用到init函数的入参 不用手动传入
def callback_func ( data ):
#print(data)
for stock in data:
current_price = data[stock][ ' close ' ]
pre_price = data[stock][ ' preClose ' ]
ratio = current_price / pre_price - 1
print (stock, C.get_stock_name(stock), ' 当前涨幅 ' , ratio)
if ratio > 0 and stock not in A.bought_list:
msg = f "当前涨幅 { ratio } 大于0 买入100股"
print (msg)
#下单函数passorder 安全起见处于注释状态 需要实际测试下单交易时再放开
#passorder(23, 1101, account, stock, 5, -1, 100, '订阅下单示例', 2, msg, C)
A.bought_list.append(stock)
stock_list = [ ' 600000.SH ' , ' 000001.SZ ' ]
for stock in stock_list:
C.subscribe_quote(stock, period = ' 1d ' , callback = callback_func)
# 六、定时任务(run_time)示例
# 实盘示例-基于 run_time
python
#coding:gbk
import time, datetime
class a ():
pass
A = a()
def init ( C ):
A.hsa = C.get_stock_list_in_sector( ' 沪深A股 ' )
A.vol_dict = {}
for stock in A.hsa:
A.vol_dict[stock] = C.get_last_volume(stock)
A.bought_list = []
C.run_time( " f " , " 1nSecond " , " 2019-10-14 13:20:00 " )
def f ( C ):
t0 = time.time()
now = datetime.datetime.now()
full_tick = C.get_full_tick(A.hsa)
total_market_value = 0
total_ratio = 0
count = 0
for stock in A.hsa:
ratio = full_tick[stock][ ' lastPrice ' ] / full_tick[stock][ ' lastClose ' ] - 1
if ratio > 0.09 and stock not in A.bought_list:
msg = f " { now } { stock } { C.get_stock_name(stock) } 当前涨幅 { ratio } 大于5% 买入100股"
#下单示例 安全起见处于注释状态 需要实际测试下单时可以放开
#passorder(23, 1101, account, stock, 5, -1, 100, '示例策略', 2, msg, C)
A.bought_list.append(stock)
market_value = full_tick[stock][ ' lastPrice ' ] * A.vol_dict[stock]
total_ratio += ratio * market_value
total_market_value += market_value
count += 1
total_ratio /= total_market_value
total_ratio *= 100
print ( f ' { now } 当前A股加权涨幅 { round (total_ratio, 2 ) } % 函数运行耗时 { round (time.time() - t0, 5 ) } 秒' )
上次更新:
邀请注册送VIP优惠券
分享下方的内容给好友、QQ群、微信群,好友注册您即可获得VIP优惠券
玩转qmt,上迅投qmt知识库
登录后获取
使用须知
File diff suppressed because one or more lines are too long
@@ -0,0 +1,393 @@
系统函数 | 迅投知识库
迅投知识库
首页
API API
投研新手教程
QMT新手教程
内置Python
XtQuant文档
VBA
数据字典 数据字典
快速开始
股票数据
行业概念数据
指数数据
期货数据
期权数据
场内基金数据
债券数据
常见问题
场景化示例
迅投因子
数据浏览器
策略迁移 策略迁移
聚宽策略
客服QQ: 810315303
联系方式: 18309226715
视频教程 视频教程
投研端教程
迅投学堂
在新窗口打开
投研平台
在新窗口打开
迅投社区
在新窗口打开
迅投官网
在新窗口打开
注册
登录
智能助手加载中...
首页
API API
投研新手教程
QMT新手教程
内置Python
XtQuant文档
VBA
数据字典 数据字典
快速开始
股票数据
行业概念数据
指数数据
期货数据
期权数据
场内基金数据
债券数据
常见问题
场景化示例
迅投因子
数据浏览器
策略迁移 策略迁移
聚宽策略
客服QQ: 810315303
联系方式: 18309226715
视频教程 视频教程
投研端教程
迅投学堂
在新窗口打开
投研平台
在新窗口打开
迅投社区
在新窗口打开
迅投官网
在新窗口打开
内置Python
快速开始
使用须知
界面操作
变量约定
数据结构
系统函数
ContextInfo 对象
init - 初始化函数
after_init - 初始化后函数
handlebar - 行情事件函数
ContextInfo.schedule_run - 设置定时器
ContextInfo.cancel_schedule_run - 取消由schedule_run产生的定时任务
ContextInfo.run_time - 设置定时器
stop - 停止处理函数
ContextInfo.is_last_bar - 是否为最后一根K线
ContextInfo.is_new_bar - 判定是否为新的 K 线
ContextInfo.get_stock_name - 根据代码获取名称
ContextInfo.get_open_date - 根据代码返回对应股票的上市时间
ContextInfo.set_output_index_property - 设定指标绘制的属性
create_sector_folder - 创建板块目录节点
get_sector_list - 获取板块目录信息
reset_sector_stock_list - 设置板块成分股
remove_stock_from_sector - 移除板块成分股
add_stock_to_sector - 添加板块成分股
行情函数
交易函数
成交回报实时主推函数
引用函数
绘图函数
枚举常量
完整示例
常见问题
# ContextInfo 对象
ContextInfo 是策略运行环境对象,是 init, after_init, handlebar 等基本方法的入参,里面包括了终端自带的属性和方法。一般情况下不建议对ContextInfo添加自定义属性,ContextInfo会随着bar的切换而重置到上一根bar的结束状态,建议用自建的全局变量来存储。 详细说明请看这里
在新窗口打开
# init - 初始化函数
初始化函数,只在整个策略开始时调用运行到一次。用于初始订阅行情,订阅账号信息使用。init函数执行完成前部分接口无法使用,如交易日获取函数get_trading_dates。
系统函数 不可被手动调用
参数:
名称 类型 描述
ContextInfo object 策略运行环境对象,可以用于存储自定义的全局变量
返回: 无
示例:
python
def init ( ContextInfo ):
ContextInfo.initProfit = 0
在init函数中订阅行情示例:
python
#coding:gbk
def init ( C ):
#init函数入参为ContextInfo对象 定义时可以选择更简短的形参名 如C
#在init函数中 可以进行 订阅行情的操作
#如需在行情回调函数中下单 下单函数需要传入ContextInfo对象 可以通过在init中定义回调函数 来使用外层的ContextInfo
def my_callback_function ( data ):
#自定义行情回调函数 入参为指数据字典
print (data)
stock = ' 600000.SH '
C.subscribe_quote(stock, period = ' 5m ' , callback = my_callback_function)
#init函数执行完成后
print ( ' init函数执行完成 ' )
# after_init - 初始化后函数
后初始化函数,在初始化函数执行完成后被调用一次。可以用于放置一次性触发的下单,取数据操作代码。
系统会在 init 函数执行完后和执行 handlebar 之前调用 after_init , 有些 init 里不支持的函数比如 ContextInfo.get_trading_dates 可以在 after_init 里调用。
系统函数 不可被手动调用
参数:
名称 类型 描述
ContextInfo object 策略运行环境对象,可以用于存储自定义的全局变量
返回: 无
示例:
python
#coding:gbk
def init ( ContextInfo ):
print ( ' init ' )
def after_init ( ContextInfo ):
print ( ' 系统会在init函数执行完后和执行handlebar之前调用after_init ' )
def handlebar ( ContextInfo ):
if ContextInfo.is_last_bar():
print ( ' handlebar ' )
after_init函数中立刻下单示例:
python
#coding:gbk
def after_init ( C ):
#after_init 函数 可以用于执行运行开始时 需要执行一次的代码 例如下一笔委托
#account变量是模型交易界面 添加策略时选择的资金账号 不需要手动填写 交易模型需要在模型交易界面运行 才有效
#快速交易参数(quickTrade )填2 passorder函数执行后立刻下单 不会等待k线走完再委托。 可以在after_init函数 run_time函数注册的回调函数里进行委托
msg = f "投资备注字符串 用来区分不同委托"
passorder( 23 , 1101 , account, ' 600000.SH ' , 5 , - 1 , 100 , ' 测试下单 ' , 2 , msg, C)
# handlebar - 行情事件函数
系统函数 不可被手动调用
释义: 行情事件函数,每根 K 线运行一次;实时行情获取状态下,先每根历史 K 线运行一次,再在每个 tick 数据来后驱动运行一次
历史k线上,按时间顺序每根K线触发一次调用;盘中,每个新到达的TICK数据驱动运行一次。可以作为行情驱动的函数,实现指标计算,回测,实盘下单的效果。
参数:
名称 类型 描述
ContextInfo object 策略运行环境对象,可以用于存储自定义的全局变量
返回: 无
示例:
def handlebar ( ContextInfo ):
# 输出当前运行到的 K 线的位置
print (ContextInfo.barpos)
# ContextInfo.schedule_run - 设置定时器
说明
该函数是新版设置定时器函数,相比旧版 run_time ,新版 schedule_run 新增了 任务分组 , 任务取消 等多种功能
原型:
python
ContextInfo.schedule_run(
func:Callable, # 回调函数,到达定时器预定时间时触发调用,参数为ContextInfo类型,无需返回值
time_point:Union[dt.datetime, str ], # 表示预定的第一次触发时间,如果设置定时器时已经过了预定时间,会立即执行func以及后续逻辑;当使用str类型时,格式为'yyyymmddHHMMSS'如'20231231235959',需要满足转换dt.datetime.strptime('20231231235959','%Y%m%d%H%M%S')
repeat_times: int = 0 , # 表示在预定时间触发后按interval间隔再触发多少次
interval:datetime.timedelta = None , # 表示预定时间触发后的后续重复执行的时间间隔
name: str = '' # 定时器任务组名,可用于定时器分组,多次设置同名定时任务不会互相覆盖,会计入同一个任务组,按任务组名取消时会全部取消
)
参数:
名称 类型 描述
func Callable 回调函数,到达定时器预定时间时触发调用,参数为ContextInfo类型,无需返回值,定义示例如下:
def on_timer(C:ContextInfo): pass
time_point Union[datetime.datetime,str] 表示预定的第一次触发时间,如果设置定时器时已经过了预定时间,会立即执行func以及后续逻辑;
当使用str类型时,格式为'yyyymmddHHMMSS'如'20231231235959',需要满足转换datetime.datetime.strptime('20231231235959','%Y%m%d%H%M%S')
repeat_times int 表示在预定时间触发后按interval间隔再触发多少次,传 -1 表示不限制次数
interval datetime.timedelta 表示预定时间触发后的后续重复执行的时间间隔
name str 定时器任务组名,可用于定时器分组,多次设置同名定时任务不会互相覆盖,会计入同一个任务组,按任务组名取消时会全部取消
回调函数参数: ContextInfo:策略模型全局对象
返回值:
int 类型,表示本次调用后生成的定时任务号,可用于取消本次定时任务,全局唯一不重复
示例:
python
import datetime as dt
def on_timer ( C : ContextInfo):
print ( ' hello world ' )
def init ( ContextInfo ):
tid = ContextInfo.schedule_run(on_timer, ' 20231231235959 ' , - 1 ,dt.timedelta( minutes = 1 ), ' my_timer ' )
def handlebar ( ContextInfo ):
pass
#此例为自2023-12-31 23:59:59后每60s运行一次on_timer
# ContextInfo.cancel_schedule_run - 取消由schedule_run产生的定时任务
原型:
python
ContextInfo.cancel_schedule_run(
key:Union[seq : int ,name : str ] # 定时任务号或定时任务组名称
)
参数:
名称 类型 描述
key: Union[seq:int,name:str] 类型为int时,表示按任务号取消;类型为str时,表示按任务组取消,会取消组内所有定时任务
返回值:
bool 类型,表示是否取消成功,即是否能按key找到目标定时任务
示例:
示例
ContextInfo.cancel_schedule_run( ' my_timer ' ) #取消my_timer任务组所有定时任务
ContextInfo.cancel_schedule_run( 1 ) #取消任务号为1的定时任务
# ContextInfo.run_time - 设置定时器
设置定时器函数,可以指定时间间隔,定时触发用户定义的回调函数。适用与在盘中,持续判断交易信号的模型。
用法: ContextInfo.run_time(funcName,period,startTime) 定时触发指定的 funcName函数, funcName函数由用户定义, 入参为ContextInfo对象。
参数:
funcName:回调函数名
period:重复调用的时间间隔,'5nSecond'表示每5秒运行1次回调函数,'5nDay'表示每5天运行一次回调函数,'500nMilliSecond'表示每500毫秒运行1次回调函数
startTime:表示定时器第一次启动的时间,如果要定时器立刻启动,可以设置历史的时间
回调函数参数: ContextInfo:策略模型全局对象
示例:
python
import time
def init ( ContextInfo ):
ContextInfo.run_time( " f " , " 5nSecond " , " 2019-10-14 13:20:00 " )
def f ( ContextInfo ):
print ( ' hello world ' )
#此例为自2019-10-14 13:20:00后每5s运行一次函数f
注意
模型回测时无效
定时器没有结束方法,会随着策略的结束而结束。
period有nMilliSecond、nSecond和Day三个周期单元,部分周期下定时器函数在第一次运行之前会先等待一个period
# stop - 停止处理函数
系统函数 不可被手动调用
释义: PY策略模型关闭停止前运行到的函数,复杂策略模型,如中间有起线程可通过在该函数内实现停止线程操作。注意, 当前版本stop函数被调用时交易连接已断开, 不能在stop函数中做报单 / 撤单操作.
参数:
名称 类型 描述
ContextInfo object 策略运行环境对象,可以用于存储自定义的全局变量
示例:
python
def stop ( ContextInfo ):
print ( ' strategy is stop ! ' )
# ContextInfo.is_last_bar - 是否为最后一根K线
用法: ContextInfo.is_last_bar()
释义: 判定是否为最后一根 K 线
参数: 无
返回: bool,返回值含义:True 是右侧最新k线 False不是最新k线
True:是
False:否
示例:
python result
def handlebar ( ContextInfo ):
print (ContextInfo.is_last_bar())
False
False
...
False
True
# ContextInfo.is_new_bar - 判定是否为新的 K 线
用法: ContextInfo.is_new_bar()
释义: 某根 K 线的第一个 tick 数据到来时,判定该 K 线为新的 K 线,其后的tick不会认为是新的 K 线
参数: 无
返回: bool,返回值含义:
True:是
False:否
示例:
python result
def handlebar ( ContextInfo ):
print (ContextInfo.is_new_bar()) #历史k线每根都是新k线 盘中 每根新k线第一个分笔返回True 其他分笔返回False
True
True
...
True
False
# ContextInfo.get_stock_name - 根据代码获取名称
注意
我们计划后续版本抛弃这个函数,不建议继续使用,可以用ContextInfo.get_instrument_detail("stockcode")["InstrumentName"]来实现同样功能
用法: ContextInfo.get_stock_name('stockcode')
释义: 根据代码获取名称
参数: stockcode:股票代码,如'000001.SZ',缺省值 ' ' 默认为当前图代码
返回: stringGBK编码)
示例:
示例 返回值
def handlebar ( ContextInfo ):
print (ContextInfo.get_stock_name( ' 000001.SZ ' ))
平安银行
# ContextInfo.get_open_date - 根据代码返回对应股票的上市时间
用法: ContextInfo.get_open_date('stockcode')
释义: 根据代码返回对应股票的上市时间
参数: stockcode:股票代码,如'000001.SZ',缺省值 ' ' 默认为当前图代码
返回: number
示例:
python result
def init ( ContextInfo ):
print (ContextInfo.get_open_date( ' 000001.SZ ' ))
19910403
# ContextInfo.set_output_index_property - 设定指标绘制的属性
用法: ContextInfo.set_output_index_property(index_name,draw_style=0,color='white',noaxis=False,nodraw=False,noshow=False)
释义: 设定指标绘制的属性,会最终覆盖掉指标对应的属性字段
参数:
index_name:string,指标名称,不可缺省
draw_style,同paint函数的drawstyle,可缺省默认为0
color,同paint函数的color,可缺省默认为'white'
noaxis:bool,是否无坐标,可缺省默认为False
nodraw:bool,是否不画线,可缺省默认为False
noshow:bool,是否不展示,可缺省默认为False
返回: 无
示例:
python python result
def init ( ContextInfo ):
ContextInfo.set_output_index_property( ' 单位净值 ' , nodraw = True ) #使回测指标'单位净值'不画线
# create_sector - 创建板块
用法: create_sector(parent_node,sector_name,overwrite)
释义: 创建板块
参数:
parent_nodestr,父节点,''为'我的'(默认目录)
sector_namestr,要创建的板块名
overwrite:bool,是否覆盖。如果目标节点已存在,为True时跳过,为False时在sector_name后增加数字编号,编号为从1开始自增的第一个不重复的值。
返回: sector_name2:实际创建的板块名
示例:
sector = create_sector( ' 我的 ' , ' 新建板块 ' , False )
新建板块
# create_sector_folder - 创建板块目录节点
用法: create_sector_folder(parent_node,folder_name,overwrite)
释义: 创建板块目录节点
参数:
parent_nodestr,父节点,''为'我的'(默认目录)
sector_namestr,要创建的节点名
overwrite:bool,是否覆盖。如果目标节点已存在,为True时跳过,为False时在folder_name后增加数字编号,编号为从1开始自增的第一个不重复的值。
返回: sector_name2:实际创建的节点名
示例:
python result
folder = create_sector_folder( ' 我的 ' , ' 新建分类 ' , False )
新建分类
# get_sector_list - 获取板块目录信息
用法: get_sector_list(node)
释义: 获取板块目录信息
参数:
nodestr,板块节点名,''为顶层目录
返回: info_list[[s1,s2,...],[f1,f2,...]]s为板块名,f为目录节点名,例如[['我的自选'],['新建分类1']]
示例:
python result
get_sector_list( ' 我的 ' )
[['我的自选', '龙头', '卖出篮子', 'TMP', '震荡', '待分析'], []]
# reset_sector_stock_list - 设置板块成分股
用法: reset_sector_stock_list(sector,stock_list)
释义: 设置板块成分股
参数:
sector:板块名
stock_listlist,品种代码列表,例如['000001.SZ','600000.SH']
返回: resultbool,操作成功为True,失败为False
示例:
python result
reset_sector_stock_list( ' 我的自选 ' ,[ ' 000001.SZ ' , ' 600000.SH ' ])
True / False
# remove_stock_from_sector - 移除板块成分股
用法: remove_stock_from_sector(sector,stock_code)
释义: 移除板块成分股
参数:
sector:板块名
stock_code:品种代码,例如'000001.SZ'
返回: resultbool,操作成功为True,失败为False
示例:
python result
remove_stock_from_sector( ' 我的自选 ' , ' 000001.SZ ' )
True / False
# add_stock_to_sector - 添加板块成分股
用法: add_stock_to_sector(sector,stock_code)
释义: 添加板块成分股
参数:
sector:板块名
stock_code:品种代码,例如'000001.SZ'
返回: resultbool,操作成功为True,失败为False
示例:
python result
add_stock_to_sector( ' 我的自选 ' , ' 000001.SZ ' )
True / False
上次更新:
邀请注册送VIP优惠券
分享下方的内容给好友、QQ群、微信群,好友注册您即可获得VIP优惠券
玩转qmt,上迅投qmt知识库
登录后获取
数据结构 行情函数
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large Load Diff
File diff suppressed because one or more lines are too long
@@ -0,0 +1,140 @@
使用须知 | 迅投知识库
迅投知识库
首页
API API
投研新手教程
QMT新手教程
内置Python
XtQuant文档
VBA
数据字典 数据字典
快速开始
股票数据
行业概念数据
指数数据
期货数据
期权数据
场内基金数据
债券数据
常见问题
场景化示例
迅投因子
数据浏览器
策略迁移 策略迁移
聚宽策略
客服QQ: 810315303
联系方式: 18309226715
视频教程 视频教程
投研端教程
迅投学堂
在新窗口打开
投研平台
在新窗口打开
迅投社区
在新窗口打开
迅投官网
在新窗口打开
注册
登录
智能助手加载中...
首页
API API
投研新手教程
QMT新手教程
内置Python
XtQuant文档
VBA
数据字典 数据字典
快速开始
股票数据
行业概念数据
指数数据
期货数据
期权数据
场内基金数据
债券数据
常见问题
场景化示例
迅投因子
数据浏览器
策略迁移 策略迁移
聚宽策略
客服QQ: 810315303
联系方式: 18309226715
视频教程 视频教程
投研端教程
迅投学堂
在新窗口打开
投研平台
在新窗口打开
迅投社区
在新窗口打开
迅投官网
在新窗口打开
内置Python
快速开始
使用须知
安装路径的选择
下载python库
关于ContextInfo
推荐用法
错误用法
关于线程和进程
主图解析
策略运行无反应/运行报错提示 "run script failed! "
数据下载
界面操作
变量约定
数据结构
系统函数
行情函数
交易函数
成交回报实时主推函数
引用函数
绘图函数
枚举常量
完整示例
常见问题
# 安装路径的选择
在安装 QMT 软件时, 请不要安装在C盘,以避免因权限问题导致的使用问题
若是只能安装到C盘,请在启动时选择 以管理员权限启动
# 下载python库
初次使用 QMT 时,请确保补全所需的 Python 库。安装完毕后,不要忘记重启客户端。
提示
在盘中,下载速度会很慢,建议盘前或盘后更新。
# 关于ContextInfo
由于底层机制的限制, ContextInfo 中存储的变量值将会回滚,即在对 ContextInfo 中的变量进行修改之后,在下一次 handlebar 调用时,这些修改将不会保留。具体细节请参阅 常见问题
在新窗口打开 。因此,在完全理解 ContextInfo 机制之前,请避免在其中存储任何变量。
# 推荐用法
class G (): pass
g = G()
def init ( ContextInfo ):
g.stock_list = [ ' 000001.SZ ' ]
def handlebar ( ContextInfo ):
g.stock_list.append( ' 600000.SH ' )
# 错误用法
警告
下面的示例请勿使用
def init ( ContextInfo ):
ContextInfo.stock_list = [ ' 000001.SZ ' ]
def handlebar ( ContextInfo ):
ContextInfo.stock_list.append( ' 600000.SH ' )
# 关于线程和进程
QMT中,python 无法 使用多线程和多进程,而且 所有策略都在同一线程中执行 ,所以策略中应该尽量避免阻塞类的写法,否则会影响其他策略的执行。
# 主图 解析
如下图所示,策略执行依赖于K线图。这里所说的主图即是K线图,策略正是在K线图上运行,也是由它驱动的(也有非K线驱动的策略写法,详见快速入门)。
K线回放 :策略在客户端运行时会从第一根K线开始,依次调用 handlebar 函数,直至最后一根K线。并且在盘中,每一个新的行情快照都会触发一次 handlebar 函数调用(无论主图的周期如何)。如果想要过滤掉某些K线,可以设置右侧的快速计算,或使用 ContextInfo. is_last_bar () 函数进行过滤。
# 策略运行无反应/运行报错提示 "run script failed! "
最快解决方法是点击右上角 布局 按钮,选择 恢复默认布局
如果策略运行后无任何反应,首先检查客户端是否有其他策略正在运行,如果有,请先将其停止,然后重试。检查方法如下图所示:
提示
最后建议重启客户端
# 数据下载
QMT提供了许多接口来依赖数据下载功能。客户端的数据下载功能如下图所示:
而且,在批量下载中可以设置定时下载,这样可以方便地每天自动下载当日的行情数据。
上次更新:
邀请注册送VIP优惠券
分享下方的内容给好友、QQ群、微信群,好友注册您即可获得VIP优惠券
玩转qmt,上迅投qmt知识库
登录后获取
快速开始 界面操作
File diff suppressed because one or more lines are too long
@@ -0,0 +1,397 @@
变量约定 | 迅投知识库
迅投知识库
首页
API API
投研新手教程
QMT新手教程
内置Python
XtQuant文档
VBA
数据字典 数据字典
快速开始
股票数据
行业概念数据
指数数据
期货数据
期权数据
场内基金数据
债券数据
常见问题
场景化示例
迅投因子
数据浏览器
策略迁移 策略迁移
聚宽策略
客服QQ: 810315303
联系方式: 18309226715
视频教程 视频教程
投研端教程
迅投学堂
在新窗口打开
投研平台
在新窗口打开
迅投社区
在新窗口打开
迅投官网
在新窗口打开
注册
登录
智能助手加载中...
首页
API API
投研新手教程
QMT新手教程
内置Python
XtQuant文档
VBA
数据字典 数据字典
快速开始
股票数据
行业概念数据
指数数据
期货数据
期权数据
场内基金数据
债券数据
常见问题
场景化示例
迅投因子
数据浏览器
策略迁移 策略迁移
聚宽策略
客服QQ: 810315303
联系方式: 18309226715
视频教程 视频教程
投研端教程
迅投学堂
在新窗口打开
投研平台
在新窗口打开
迅投社区
在新窗口打开
迅投官网
在新窗口打开
内置Python
快速开始
使用须知
界面操作
变量约定
函数命名规则
账号类型说明
symbol_code - 代码表示
交易所代码
交易标的代码
symbol示例
期货主力连续合约
期货加权连续合约
mode - 模式选择
调试运行模式
回测模式
模拟信号模式
实盘交易模式
ContextInfo - 上下文对象
ContextInfo.start/ContextInfo.end - 回测开始/结束时间
ContextInfo.capital - 设定回测初始资金
ContextInfo.period - 获取当前周期
ContextInfo.barpos - 获取当前运行到 K 线索引号
ContextInfo.time_tick_size - 获取当前图 K 线数目
ContextInfo.stockcode - 获取当前图代码
ContextInfo.market - 获取当前主图市场
ContextInfo.dividend_type - 获取当前主图复权处理方式
ContextInfo.benchmark - 获取回测基准标的
ContextInfo.do_back_test - 表示当前是否为回测模式
数据结构
系统函数
行情函数
交易函数
成交回报实时主推函数
引用函数
绘图函数
枚举常量
完整示例
常见问题
# 函数命名规则
函数名以 get_ 开头的,表示数据来源于客户端内存
函数名以 query_ 开头的,表示数据是向服务查询
# 账号类型说明
'FUTURE' - 期货账号
'STOCK' - 股票账号
'CREDIT' - 信用账号
'FUTURE_OPTION' - 期货期权
'STOCK_OPTION' - 股票期权
'HUGANGTONG' - 沪港通
'SHENGANGTONG' - 深港通
# symbol_code - 代码表示
迅投代码(symbol_code)是迅投平台统一用于表示交易标的的代码 其格式为: 交易标的代码.交易所代码 ,例如深圳证券交易所的平安银行,迅投代码为 000001.SZ (不区分大小写)。代码表示可以在迅投研终端的行情列表或者按键精灵中查询。
# 交易所代码
目前迅投研支持国内12个交易所,12个交易所的代码缩写如下:
交易所名称 迅投简称 显示后缀
上海证券交易所 SH SH
深圳证券交易所 SZ SZ
北京证券交易所 BJ BJ
香港证券交易所 HK HK
沪港通 HGT HGT
深港通 SGT SGT
中国金融期货交易所 IF CFFEX
上海期货交易所 SF SHFE
大连商品交易所 DF DCE
郑州商品交易所 ZF CZCE
上海国际能源交易中心 INE INE
广州期货交易所 GF GFEX
迅投研系统目前支持一站式获取全球多市场数据,详情链接: 全球市场数据
在新窗口打开
# 交易标的代码
交易标的代码是指交易所给出的交易标的代码, 包括股票(如 600000), 期货(如 rb2011, 期权(如 10002498, 指数(如 000001, 基金(如 510300)等代码。
注意
对于期货合约代码来说,我们仅对 market 做了简化处理, symbol 仍遵守交易所标准命名规则,且 严格区分大小写 ,例如 AP401.ZF 不能写成 ap401.ZF , rb2401.SF 不能写成 RB2401.SF
# symbol示例
市场中文名 市场代码 示例代码 显示后缀 证券简称
上交所 SH 600000.SH SH 浦发银行
深交所 SZ 000001.SZ SZ 平安银行
北交所 BJ 830779.BJ BJ 武汉蓝电
中金所 IF IC2311.IF CFFEX 中证 500 指数 2023 年 11 月期货合约
上期所 SF rb2311.SF SHFE 螺纹钢 2023 年 11 月期货合约
大商所 DF m2311.DF DCE 豆粕 2023 年 11 月期货合约
郑商所 ZF FG305.ZF CZCE 玻璃 2023 年 5 月期货合约
上海国际能源交易中心 INE sc2311.INE INE 原油 2023 年 11 月期货合约
广期所 GF lc2405.GF GFEX 碳酸锂 2024 年 05 月期货合约
上证期权 SHO 10005334.SHO SH 50ETF购12月2650
深证期权 SZO 90002114.SZO SZ 深证100ETF沽12月2700
板块指数 BKZS 290001.BKZS BKZS 工业品期货板块指数
# 期货主力连续合约
仅支持回测模式下交易, 期货主力连续合约 为量价数据的简单拼接,未做平滑处理,如 rb00.SF 螺纹钢主连合约,其他[主连合约代码请参考]( 期货数据 | 迅投知识库 (thinktrader.net) )
# 期货加权连续合约
仅支持回测模式下交易, 期货加权连续合约 为迅投按照一定规则加权合成的连续合约,相比主力连续合约更加平滑,如 rbJQ00.SF ,其他[加权合约代码参考]( 期货数据 | 迅投知识库 (thinktrader.net) )
# mode - 模式选择
迅投研终端中,策略可以以四种模式运行,分别为 调试运行模式 , 回测模式 , 模拟信号模式 , 实盘交易模式 ,模式需要在运行策略时手动选择
# 调试运行模式
调试运行模式需要在策略编辑界面点击编辑栏上方的 运行 ,该模式下策略会以实时行情进行运算,但迅投研终端 不会记录交易信号
# 回测模式
回测模式需要在策略编辑界面点击编辑栏上方的 回测 ,该模式下策略会以右侧栏设定的回测周期推进行情进行运算,回测模式下,发生的交易会被记录在回测结果页面
# 模拟信号模式
模拟信号模式需要在策略交易界面,在左侧策略文件栏中选择要进行计算运行的策略,点击 右侧圆形按钮 选择 模拟 ,点击 三角形运行按钮 后策略会以实时行情进行运算,该模式下调用的下单函数(passorder) 不会产生实际交易,仅会记录交易信号在下方的 策略信号 栏中
# 实盘交易模式
实盘交易模式需要在策略交易界面,在左侧策略文件栏中选择要进行计算运行的策略,点击 右侧圆形按钮 选择 实盘 ,点击 三角形运行按钮 后策略会以实时行情进行运算,该模式下调用的下单函数(passorder) 会对账户实际下单,同时交易信号会记录在下方的 策略信号 栏中
# ContextInfo - 上下文对象
# ContextInfo.start/ContextInfo.end - 回测开始/结束时间
注意
一、此属性只在回测模式生效;
二、仅在init中设置生效,应在init中设置完毕;
三、缺省值为策略编辑界面设定的回测时间范围;
四、回测起止时间也可在策略编辑器的回测参数面板中设置,若两处同时设置,则以代码中设置的值为准;
五、结束时间小于等于开始时间则计算范围为空。
释义
可通过此属性设定回测开始/结束的时间,以 %Y-%m-%d %H:%M:%S 格式传入
原型
内置python
ContextInfo.start # 回测开始时间属性
ContextInfo.end # 回测结束时间属性
返回值 none
示例
内置python 输出值
# coding:gbk
def init ( ContextInfo ):
ContextInfo.start = " 2017-01-01 00:00:00 " # 回测开始时间为 2017-01-01
ContextInfo.end = " 2020-01-01 00:00:00 " # 回测结束时间为 2020-01-01
def handlebar ( ContextInfo ):
# 打印输出当前回测时间
print (timetag_to_datetime(ContextInfo.get_bar_timetag(ContextInfo.barpos), " %Y-%m- %d %H%M%S " ))
2017-01-03 103000
2017-01-03 113000
2017-01-03 140000
2017-01-03 150000
2017-01-04 103000
2017-01-04 113000
2017-01-04 140000
2017-01-04 150000
2017-01-05 103000
2017-01-05 113000
...
# ContextInfo.capital - 设定回测初始资金
注意
此函数只支持回测模式。回测初始资金也可在策略编辑器的回测参数面板中设置,若两处同时设置,则以代码中设置的值为准。
释义 设定回测初始资金, 支持读写 ,默认为 1000000
原型
内置python
ContextInfo.capital = 10000000 # 设定ContextInfo.capital 值为10000000
返回值 float 类型的数值,代表当前策略设定的回测金额
示例
内置python 输出值
# coding:gbk
def init ( ContextInfo ):
ContextInfo.capital = 10000000
def handlebar ( ContextInfo ):
print (ContextInfo.capital)
10000000.0
10000000.0
10000000.0
...
# ContextInfo.period - 获取当前周期
释义 获取当前周期,即基本信息中设置的默认周期, 只读
原型
内置python
ContextInfo.period
返回 string ,返回值含义:
值 含义
'1d' 日线
'1m' 1分钟线
'3m' 3分钟线
'5m' 5分钟线
'15m' 15分钟线
'30m' 30分钟线
'1h' 小时线
'1w' 周线
'1mon' 月线
'1q' 季线
'1hy' 半年线
'1y' 年线
示例
内置python 返回值
# coding:gbk
def init ( ContextInfo ):
pass
def handlebar ( ContextInfo ):
print (ContextInfo.period)
1d
# ContextInfo.barpos - 获取当前运行到 K 线索引号
释义
获取主图当前运行到的 K 线索引号,只读,索引号从0开始
原型
内置python
ContextInfo.barpos
返回值 int 类型值,代表着当前K线的索引号
示例
内置python 返回值
# coding:gbk
def init ( ContextInfo ):
pass
def handlebar ( ContextInfo ):
print (ContextInfo.barpos)
0
1
2
3
4
5
...
# ContextInfo.time_tick_size - 获取当前图 K 线数目
释义
获取当前图 K 线bar的数量,只读
原型
内置python
ContextInfo.time_tick_size
返回值 int
示例
内置python 返回值
# coding:gbk
def init ( ContextInfo ):
pass
def handlebar ( ContextInfo ):
print (ContextInfo.time_tick_size)
5297
5297
5297
5297
5297
5297
...
# ContextInfo.stockcode - 获取当前图代码
释义
获取当前主图代码,只读
原型
内置python
ContextInfo.stockcode
返回值 string :对应主图代码
示例
内置python 返回值
# coding:gbk
def init ( ContextInfo ):
pass
def handlebar ( ContextInfo ):
print (ContextInfo.stockcode)
000300
000300
000300
000300
000300
...
# ContextInfo.market - 获取当前主图市场
释义
获取当前主图市场,只读
原型
内置python
ContextInfo.market
返回值 string :对应主图市场
示例
内置python 返回值
# coding:gbk
def init ( ContextInfo ):
pass
def handlebar ( ContextInfo ):
print (ContextInfo.market)
SH
SH
SH
SH
SH
...
# ContextInfo.dividend_type - 获取当前主图复权处理方式
释义
获取当前主图复权处理方式
原型
内置python
ContextInfo.dividend_type
返回值 string ,返回值含义:
值 含义
'none' 不复权
'front' 向前复权
'back' 向后复权
'front_ratio' 等比向前复权
'back_ratio' 等比向后复权
示例
内置python 返回值
# coding:gbk
def init ( ContextInfo ):
pass
def handlebar ( ContextInfo ):
print (ContextInfo.dividend_type)
front_ratio
front_ratio
front_ratio
front_ratio
front_ratio
...
# ContextInfo.benchmark - 获取回测基准标的
注意
该属性只在回测模式可用
释义 获取回测基准的代码,只读
原型
内置python
ContextInfo.benchmark
返回值 string
示例
内置python 返回值
# coding:gbk
def init ( ContextInfo ):
pass
def handlebar ( ContextInfo ):
print (ContextInfo.benchmark)
000300.SH
000300.SH
000300.SH
000300.SH
000300.SH
...
# ContextInfo.do_back_test - 表示当前是否为回测模式
释义
表示当前是否为回测模式,只读,默认值为 False
原型
内置python
ContextInfo.do_back_test
返回值 bool
上次更新:
邀请注册送VIP优惠券
分享下方的内容给好友、QQ群、微信群,好友注册您即可获得VIP优惠券
玩转qmt,上迅投qmt知识库
登录后获取
界面操作 数据结构
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large Load Diff
File diff suppressed because one or more lines are too long
@@ -0,0 +1,296 @@
xtquant版本下载 | 迅投知识库
迅投知识库
首页
API API
投研新手教程
QMT新手教程
内置Python
XtQuant文档
VBA
数据字典 数据字典
快速开始
股票数据
行业概念数据
指数数据
期货数据
期权数据
场内基金数据
债券数据
常见问题
场景化示例
迅投因子
数据浏览器
策略迁移 策略迁移
聚宽策略
客服QQ: 810315303
联系方式: 18309226715
视频教程 视频教程
投研端教程
迅投学堂
在新窗口打开
投研平台
在新窗口打开
迅投社区
在新窗口打开
迅投官网
在新窗口打开
注册
登录
智能助手加载中...
首页
API API
投研新手教程
QMT新手教程
内置Python
XtQuant文档
VBA
数据字典 数据字典
快速开始
股票数据
行业概念数据
指数数据
期货数据
期权数据
场内基金数据
债券数据
常见问题
场景化示例
迅投因子
数据浏览器
策略迁移 策略迁移
聚宽策略
客服QQ: 810315303
联系方式: 18309226715
视频教程 视频教程
投研端教程
迅投学堂
在新窗口打开
投研平台
在新窗口打开
迅投社区
在新窗口打开
迅投官网
在新窗口打开
xtquant文档
快速开始
XtQuant.XtData 行情模块
XtQuant.Xttrade 交易模块
完整实例
常见问题
xtquant版本下载
更新日期 版本 下载 更新说明
20251219 xtquant_250807 点击下载 token模式,K线全推和全推数据加载模式调整
从xtdc.init()后立刻加载,调整为在第一次使用数据时加载
xttrader支持智能算法
获取智能算法参数配置信息接口get_smart_algo_param
智能算法下单接口smart_algo_order_async
智能算法任务查询接口query_smart_algo_task
智能算法任务撤销接口cancel_smart_algo_task_async
获取当前连接订阅的数据信息
xtdata.get_current_connect_sub_info(需要投研版本)
获取客户端所有订阅信息
xtdata.get_all_sub_info()(需要投研版本)
获取委托在千档队列中的排名
xtdata.get_order_rank() (需要投研版本)
token模式可以设置千档数据源模式
xtdc.set_thousand_source_mode()
token模式同datadir只允许启动一个xtdc进程
get_tabular_data参数不合法返回None
大单统计字段调整,查阅xtdata.md文档
vix市场的订阅和全推支持显示夜盘真实时间
BugFix: 修复期货0点前夜盘复权不生效的问题
20250516 xtquant_250516 点击下载
压缩包目录结构已调整到和之前一致,带有xtquant文件夹 支持python3.13版本
xttrader 支持银证转账
银行信息查询 xttrader.query_bank_info()
银行账户余额查询xttrader.query_bank_amount()
银证转账转入 xttrader.bank_transfer_in()
银证转账转出 xttrader.bank_transfer_out()
银行卡流水记录查询xttrader.query_bank_transfer_stream()
xttrader 支持期货和期权资金划转
权资金转期货 xttrader.ctp_transfer_option_to_future()
期货资金转期权 xttrader.ctp_transfer_future_to_option()
xttrader支持北交所
xtconstant 添加北交所市价报价方式说明
xttrader 交易数据字段调整
委托 XtOrder 新增
股东代码 secu_account
证券名称 instrument_name
成交 XtTrade 新增
股东代码 secu_account
证券名称 instrument_name
持仓 XtPosition 新增
股东代码 secu_account
证券名称 instrument_name
当前价 last_price
盈亏比例 profit_rate
浮动盈亏 float_profit
持仓盈亏 position_profit
开仓日期 open_date(只对期货可用)
账号资金 XtAsset 新增
可取余额 fetch_balance
当前余额 current_balance(当前余额 = 可用资金 + 冻结资金)
xtdata获取数据函数支持以datetime形式传入时间范围
合约信息添加交易日字段 TradingDay
xtdata.get_instrument_detail()
支持获取大单统计数据(需要vip权限)
xtdata.get_transactioncount()
支持获取带有分类信息的板块列表(需要投研版本)
xtdata.get_sector_info()
期权合约信息添加期权预估保证金 OptEstimatedMargin(需要投研版本)
xtdata.get_option_detail_data()
郑商所期货、期权品种提供标准化代码的字段(如 MA2504.ZF)(需要投研版本)
xtdata获取数据函数支持ATM市场
xtdata.get_market_data()
xtdata.get_market_data_ex()
BugFix: 订阅数据问题
20241017 xtquant_241014 点击下载 xtdata.get_option_detail_data()期权多空方向类型判断调整
xtdata.get_trading_calendar() 自动下载所需的节假日数据
xtdata.get_instrument_detail() 字段ExpireDate类型由int调整为str
tick数据增加现手字段(tickvol),即当前tick累计成交量与上条数据的差值
新增函数xtdata.get_formula_result(),用于获取subscribe_formula()的模型结果
修复token模式下启用K线全推后单支订阅数据周期错误的问题
20240926 xtquant_240920a 有已知缺陷,暂不提供下载
在token模式下,启用K线全推后单支订阅数据周期错误 修复xtdata.subscribe_quote()返回订阅号为None的问题
20240923 xtquant_240920 有已知缺陷,暂不提供下载 token模式,可以设置初始化的市场列表
xtdatacenter.set_init_markets()
函数xtdata.get_period_list() 结果结构调整
添加函数 xtdata.get_trading_contract_list()
获取当前主力合约可交易标的列表
添加用于获取交易时段的系列函数
xtdata.get_trading_period()
xtdata.get_all_trading_periods()
xtdata.get_all_kline_trading_periods()
添加函数 xtdata.subscribe_quote2()
支持复权方式参数
20240822 xtquant_240812 点击下载 期货夜盘显示真实时间功能默认开启
未开启时, 周六凌晨的行情数据时间为周一
开启时,周六凌晨的行情数据时间为周六
新增板块:过期上交所、过期深交所
沪港通深港通板块不再显示历史标的
撤单接口的市场参数支持字符串格式,如SH、SF
xttrader.cancel_order_stock_sysid()和xttrader.cancel_order_stock_sysid_async()
添加函数xtdata.get_his_option_list_batch()和xtdata.get_his_option_list()
获取历史上某段时间的指定品种期权信息列表
依赖数据'optionhistorycontract'
期权函数支持商品期权品种 xtdata.get_option_undl_data()和xtdata.get_option_list()
郑商所期权标的代码调整为4位 xtdata.get_option_detail_data()
移除郑商所过滤重复tick的逻辑
20240617 xtquant_240613 点击下载 支持python3.12版本
xtdata支持选择端口范围,在范围内自动连接
添加函数xtdata.get_full_kline() 批量获取当日K线数据(需要开启K线全推)
支持获取新闻公告数据
xtdata.get_market_data()系列函数 数据周期:announcement
支持获取涨跌停连板数据
xtdata.get_market_data()系列函数 数据周期:limitupperformance
支持获取港股通持股明细数据
xtdata.get_market_data()系列函数 数据周期:hktdetails,hktstatistics
支持获取外盘的行情数据(需购买相应服务)
行情订阅xtdata.subscribe_quote()和行情获取xtdata.get_market_data()系列函数,支持美股品种的获取
支持订阅vba模型(连接投研端)
xtdata.subscribe_formula()
token模式下初始化全推市场可选
xtdatacenter.set_wholequote_market_list()
token模式下行情连接优选机制调整
xtdatacenter.set_allow_optmize_address()会使用第一个地址作为全推连接
token模式下期货周末夜盘数据时间模式可选,可以选择展示为周一凌晨时间或真实的周六凌晨时间
xtdatacenter.set_future_realtime_mode()
20240329 xtquant_240329 点击下载 郑商所期货品种支持使用4位年月代码(例如:CF2303.ZF)
xtdata.get_instrument_detail()支持使用4位年月代码获取
历史主力合约数据 新增4位年月代码字段
支持获取etf的iopv数据
分笔数据添加iopv字段(pe
xtdata.get_market_data()系列函数 数据周期:etfiopv1m(分钟级) etfiopv1d(日级)
期权函数xtdata.get_option_detail_data() 新增标的品种代码字段 OptUndlCodeFull
新增板块 上证转债、沪深转债、T+0基金
连接状态监听接口回调数据结构调整
xtdata.watch_quote_server_status()
xtdata.watch_xtquant_status()
新增接口 xtdata.subscribe_formula() 支持连接投研端调用vba
token模式下支持按用户权限放开并行接入数量
本地python回测支持多线程
优化7*24连续交易的问题
20240205 xtquant_240119b 点击下载 修复token模式下偶发的订阅数据异常问题
有问题的版本:240119, 240119a
token模式下并行接入数量放宽至10个
20240129 xtquant_240119a 点击下载 合约信息接口支持参数控制获取全部字段
xtdata.get_instrument_detail(iscomplete = True)并添加以下字段
期货和期权手续费方式(ChargeType)
开仓手续费(率)(ChargeOpen)
平仓手续费(率)(ChargeClose)
开今仓(日内开仓)手续费(率)(ChargeTodayOpen)
平今仓(日内平仓)手续费(率)(ChargeTodayClose)
交割月持仓倍数(OpenInterestMultiple)
添加客户端连接状态监听接口 xtdata.watch_xtquant_status()
支持获取退市可转债数据
xtdata.get_market_data()系列函数 数据周期:delistchangebond
支持获取待发可转债数据
xtdata.get_market_data()系列函数 数据周期:replacechangebond
优化K线全推的断线重连逻辑
20240119 xtquant_240119 点击下载 xtdata.subscribe_quote()和xtdata.get_market_data()系列函数,添加新的K线数据周期
新周期包含:周线(1w)、月线(1mon)、季度线(1q)、半年线(1hy)、年线(1y)
支持获取千档委买委卖队列数据
订阅函数 xtdata.subscribe_l2thousand_queue()
获取函数 xtdata.get_l2thousand_queue()
支持港股lv2数据(待后续迅投lv2数据源上线后可用)
支持获取港股席位数据
订阅函数 xtdata.subscribe_quote(period = 'brokerqueue')
获取函数 xtdata.get_broker_queue_data()
xtdata.get_full_tick()在VIP模式下提供成交笔数字段(transactionNum
xtdata.get_option_detail_data()支持获取商品期权数据
支持获取历史主力合约数据
xtdata.get_market_data()系列函数 数据周期:historymaincontract
修复 获取上证期权、深证期权tick行情数据价格精度错误的问题
修复 期货夜盘分钟线获取不到的问题
优化下载数据流程
支持设置行情源自动连接目标地址范围
xtdatacenter.set_allow_optmize_address()
xtdata.get_local_data()支持指定数据路径
20231228 xtquant_231209a 点击下载 修复xtdata.get_trading_calendar()获取历史范围返回数据重复的问题
添加xtdata.get_trading_calendar()目前仅支持SH,SZ市场的说明(其他市场交易日历陆续对接中)
添加快照指标数据周期 'snapshotindex'(包含量比、涨速、换手等字段)
修复板块指数(BKZS)分钟线获取不到的问题
添加xtdatacenter中的北交所、沪深京A股板块
修复xtdata.subscribe_whole_quote()订阅全推数据中的pvolume字段单位错误
(股票、转债的pvolume单位为股,所有品种volume单位均为手,其余品种情况详见网页文档)
20231209 xtquant_231209 点击下载 添加ETF申赎清单信息相关接口
下载数据 xtdata.download_etf_info()
获取数据 xtdata.get_etf_info()
添加节假日下载的接口 download_holiday_data(获取交易日历依赖节假日)
添加涨跌停数据,数据周期'stoppricedata'
添加连接成功时连接状态日志
添加财务数据文档中十大股东、股东数的字段说明
修复历史st数据获取失败的问题
修复xtdatacenter提供数据时,和接收进程运行目录不同出现获取失败的问题
优化模块退出时的表现
移除xtdata.get_industry()接口
20231124 xtquant_231101c 点击下载 修复xtdata.get_market_data()系列的内存泄漏问题
xtdatacenter.init()在重要市场初始化失败时抛出异常信息
全推数据在第一次使用时订阅,减少不必要的带宽占用
修复xtdatacenter退出时崩溃的问题
修复同目录下xtdatacenter重复启动卡住的问题
补全期货全推的月份连续合约(例如 ag01.SF)和交易合约(例如 ag2401.SF
日志相关优化
20231110 xtquant_231101b 点击下载 修复过期合约板块成分为空的问题
优化xtdatatcenter监听端口后连接接入的时序
xtdata.download_history_data添加增量下载参数,支持指定起始时间的增量下载
修复token无效时调用接口崩溃的问题
20231106 xtquant_231101a 点击下载 修复退出时发生异常的问题
优化初始化过程中行情连接和数据订阅的时序
20231101 xtquant_231101 点击下载 添加xtdatacenter,支持以token方式登录行情服务
添加xtdata.QuoteServer,支持通过xtdata控制、监控行情连接
补充xtdata中对转债交易场景、ETF交易场景的数据支持
调整了一些底层数据交互的实现方式
完善xttrader期货交易场景下的开平仓方向字段
20230920 xtquant_230825b 点击下载 对应当前QMT券商版公版的下载python库
券商版会有版本升级跟不上的情况,通常请使用这个公版版本以保证兼容性
20230905 xtquant_230825a 点击下载 -
20230825 xtquant_230825 点击下载 -
20230301 xtquant_230301 点击下载 -
20220817 xtquant_220817 点击下载 -
上次更新:
邀请注册送VIP优惠券
分享下方的内容给好友、QQ群、微信群,好友注册您即可获得VIP优惠券
玩转qmt,上迅投qmt知识库
登录后获取
常见问题
File diff suppressed because one or more lines are too long
@@ -0,0 +1,96 @@
快速开始 | 迅投知识库
迅投知识库
首页
API API
投研新手教程
QMT新手教程
内置Python
XtQuant文档
VBA
数据字典 数据字典
快速开始
股票数据
行业概念数据
指数数据
期货数据
期权数据
场内基金数据
债券数据
常见问题
场景化示例
迅投因子
数据浏览器
策略迁移 策略迁移
聚宽策略
客服QQ: 810315303
联系方式: 18309226715
视频教程 视频教程
投研端教程
迅投学堂
在新窗口打开
投研平台
在新窗口打开
迅投社区
在新窗口打开
迅投官网
在新窗口打开
注册
登录
智能助手加载中...
首页
API API
投研新手教程
QMT新手教程
内置Python
XtQuant文档
VBA
数据字典 数据字典
快速开始
股票数据
行业概念数据
指数数据
期货数据
期权数据
场内基金数据
债券数据
常见问题
场景化示例
迅投因子
数据浏览器
策略迁移 策略迁移
聚宽策略
客服QQ: 810315303
联系方式: 18309226715
视频教程 视频教程
投研端教程
迅投学堂
在新窗口打开
投研平台
在新窗口打开
迅投社区
在新窗口打开
迅投官网
在新窗口打开
xtquant文档
快速开始
XtQuant 能提供哪些服务
XtQuant 运行依赖环境
XtQuant 运行逻辑
XtQuant.XtData 行情模块
XtQuant.Xttrade 交易模块
完整实例
常见问题
xtquant版本下载
# XtQuant 能提供哪些服务
XtQuant是基于迅投MiniQMT衍生出来的一套完善的Python策略运行框架,对外以Python库的形式提供策略交易所需要的行情和交易相关的API接口。
# XtQuant 运行依赖环境
XtQuant 目前提供的库包括 64 位 Python 3.6 、 3.7 、 3.8 、 3.9 、 3.10 、 3.11 、 3.12 版本,不同版本的 Python 导入时会自动切换。 在运行使用 XtQuant 的程序前需要先启动 MiniQMT 客户端。
# XtQuant 运行逻辑
Xtdata 作为行情模块,本模块旨在提供精简直接的数据满足量化交易者的数据需求,主要提供行情数据(历史和实时的K线和分笔)、财务数据、合约基础信息、板块和行业分类信息等通用的行情数据。
Xttrader 作为交易模块,封装了策略交易所需要的 Python API 接口,可以和 MiniQMT 客户端交互进行报单、撤单、查询资产、查询委托、查询成交、查询持仓以及接收资金、委托、成交和持仓等变动的主推消息。
上次更新:
邀请注册送VIP优惠券
分享下方的内容给好友、QQ群、微信群,好友注册您即可获得VIP优惠券
玩转qmt,上迅投qmt知识库
登录后获取
XtQuant.XtData 行情模块
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large Load Diff
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large Load Diff
+104
View File
@@ -0,0 +1,104 @@
# Changelog
本项目遵循 [Keep a Changelog](https://keepachangelog.com/) 和 [语义化版本](https://semver.org/)。
## [0.2.2] - 2026-08-19
### 修复
- **server_error 污染后续查询**Issue #43):`_last_server_error` 是实例状态,但每个成功响应都会读取它,而只有下单路径会重置。一次静默拒绝的委托会把错误盖到之后**所有** ping 和查询上,直到下一次下单。改为在 `handle()` 中每请求清空,且清空发生在方法校验之前,因此被拒绝的方法也不会携带上一次的诊断。
- **order_remark 匹配的模糊兜底**Issue #41):那段 `stock_code + action` 的兜底并非用于*识别*委托,而是**告警闸门**——问题比报告描述的更严重。`order_tag` 是我们生成的唯一 id,匹配不上即真未进系统;模糊兜底唯一的作用是**压制真实告警**:账户中若有一笔无关的同股票同方向委托(手动下的或上一笔未成交的),会导致 `order_sys_id` 未回填、`server_error` 为空,客户端看到一次干净的成功,而该委托从未进入系统。已移除。
- **order_stock_async 阻塞 QMT 主线程**Issue #44):`_handle_submit_order` 中的 `sleep(0.5)` 在 adjust 主线程执行(下单方法不在 `listener_methods` 中,走 deferred 路径),使其余请求串行等待,吞吐上限约 2 单/秒。改为**推迟响应而非推迟工作**:提交后登记 `OrderSettlement` 并停放响应,由每次 adjust drain 重试查询,委托号就绪即在同一 tick 内回复(零 sleep)。后台线程方案不可行——`get_trade_detail_data` 在非主策略线程返回空,会把每笔都误判为静默拒绝。
- **order_stock_async 未立即返回**Issue #50):客户端内部同步调用 `order_stock`,阻塞整个 RPC 往返,加上 #44 后的结算等待,每笔 0.5~1 秒。服务端新增 `wait_settlement` 参数(false 时 passorder 一返回即回复,委托号由 `order_callback` 推送);客户端提交移至工作线程,`order_stock_async` 不碰网络直接返回 seq。`on_order_error` 现在也携带 `seq`,此前无法判断是哪一笔异步委托失败。
- **未复权下载实为空跑**(Issue #47,亦是 #39 的真正原因):服务端下载此前只在请求复权时执行,未复权路径仅调用 `get_market_data_ex` 读取已有数据,却照常通过 callback 报告 `{finished: N}`——为一件没发生的事显示进度。而 1d/tick 默认即 `dividend_type="none"`。现在所有 `dividend_type` 都执行下载。实盘验证:601398.SH 本地日线从 0 根变为有数据。
- **query_stock_orders 缺少 order_time**Issue #48):大 QMT 的 ORDER 行提供报单日期与时间,但三层均未读取。已贯通 `OrderSnapshot``order_bigqmt``_order_from_dict`,按 MiniQMT `XtOrder.order_time` 语义输出 Unix 秒。实盘验证:11 笔真实委托全部有值。
### 新增
- **qmt_launcher**Issue #45):`open` / `close` / `restart` / `status` 四个命令管理 QMT 终端。按 `bin.x64` 路径隔离(同机多实例并存时不会误关其他账户)、以 FormulaServer 端口可连接为就绪判据而非固定 sleep、窗口标题前缀匹配(不再写死版本号)、先优雅终止 20 秒后才强杀。登录路径用 `SendMessage` 投递窗口句柄,不依赖窗口置于前台。
- **get_market_data_ex 分批**Issue #47 评论):宽 `stock_list` 此前共用一个 RPC 超时,要么装得下要么整批丢失。改为按 100 个代码一批,单批失败只损失自身代码,全部失败才抛异常。`chunk_size=0` 恢复原行为。
- **bar driver 观测埋点**`adjust()` 按触发来源分别计数、`tick_app` 全量耗时直方图、init 报告策略品种/周期/订阅能力。用于定位 RPC 读延迟的来源。
### 变更
- `AssetSnapshot` 补齐 `frozen_cash` / `market_value`,对齐 MiniQMT `XtAsset``market_value` 优先取 `m_dInstrumentValue`,仅在服务端未上报时才推导(推导会扣除冻结金额,此前未扣导致市值虚高)。
- ZMQ 传输改为精确绑定配置端口,冲突时报错而非向上扫描——端口静默漂移会让客户端连不上。
### 已知限制
- Issue #44 / #50 的实盘下单验证尚未完成(单测已量化非阻塞行为:drain < 0.2s、20 单 < 1.0s)。
- Issue #47 评论所述的 `get_market_data_ex` 超时未能复现;三组压测(300 只 × count=3、300 只 × 全历史、50 只 × 1m 全天)最慢 718ms,远在默认 6s 超时内。分批目前是防御性改动。
- RPC 读延迟受 QMT 主线程 GIL 制约,延迟 ≈ 基础 + N × `schedule_adjust_interval`。实测该间隔 200ms → 100ms 可使 p50 从 374ms 降至 172ms,代价是 CPU 占用上升。
---
## [0.2.1] - 2026-08-17
### 修复
- **正常下单误报 on_order_error(-1)**Issue #38):passorder 提交成功但委托号异步分配,客户端把「暂无 order_sys_id」误判为失败。服务端 `_handle_submit_order` 按唯一 `user_order_id`(remark) 匹配并回填 `order_sys_id`;顺带修掉校验代码对无 `.get()` 方法的 `OrderSnapshot``.get()` 的死代码(server_error 之前从未生效)。客户端 `call()` 不再丢弃 `server_error`,委托未进系统时转成异常,`order_stock_async` 携带真实原因回调 `on_order_error`。实盘验证:async 下单回调带真实委托号、提交阶段零误报(302 个测试通过,新增 5 个)。
- **query_stock_orders 查不到委托**strategy_name 陷阱):客户端别名默认 `"bigqmt_signal_trader"` 与服务端默认 `""` 不一致,改用其他策略名下单后别名查询返回空。默认改 `""`(返回全部)并对齐测试。
### 新增
- **qmt-trader skill 首次部署引导**:客户端装包、QMT 端文件同步、私有配置模板、入口启动验证、部署排错速查,零上下文也能从零跑通。
- **PyPI 发布**`BIGQMT_REDIS_DRYRUN` 入口模块补进 py-modules`pip install xtquant-big-convert` 即可获得完整包(wheel/sdist 均通过 twine check)。
### 变更
- README 头部加 PyPI / Python 版本 / License 徽章,新增「AI 助手 Skillqmt-trader」专节(启用方式、命令概览、安全设计)。
---
## [0.2.0] - 2026-08-15
### 新增(Features
- **qmt-trader skill**:统一 CLI 驱动全部 QMT API`qmt-trader/scripts/qmt.py`),46 个子命令覆盖行情/持仓/委托/下单/撤单/财务/期权/两融/北向/龙虎榜等,含通用 `rpc` 兜底命令 + 25 个高频快捷命令。
- **异步回报回调**`XtQuantTraderCallback` 全链路(`on_account_status` / `on_order_stock_async_response` / `on_stock_order` / `on_stock_trade` / `on_order_error` / `on_cancel_error` / `on_cancel_order_stock_async_response`),对齐 MiniQMT 原生语义,实盘验证。
- **全推行情订阅**`subscribe_whole_quote` 真推送):服务端引用计数管理 + PUB/SUB 数据面通道(redis/zmq)+ 客户端心跳 + 推送静默检测 + 服务端重启恢复。
- **完整 xtconstant 枚举**:91 个常量全量覆盖(账号类型/委托类型-股票期货信用期权/报价类型/委托状态/账号状态/`ORDER_TYPE_SET`),值对齐原生 MiniQMT。
- **文件日志系统**`logging_setup.py`):TimedRotatingFileHandler 按天轮转、保留 7 天(`BIGQMT_LOG_RETENTION_DAYS` 可配),双输出(文件 + QMT 面板),线程安全。
- **启动自动诊断**`init()` 打印服务状态、关键函数绑定、行情链路,方便排错。
- **server_error 字段**`submit_order` 校验委托是否进系统,静默失败时返回原因给客户端。
- **统一测试入口**`run_all_tests.py` 分组跑全部测试(signal_trader 274 + backtest 16)。
- **端到端测试**`test_all_apis.py` 验证真实 QMT 返回(transport 一致性/持仓空/委托空/下单未进系统/server_error)。
- **生产失败场景单元测试**:7 个测试覆盖返回空/全 0/拒绝的 QMT 边界(非 happy-path)。
- **官方交易查询函数**`get_value_by_order_id` / `get_last_order_id` / `get_ipo_data` / `get_new_purchase_limit` / `get_history_trade_detail_data` / 融资融券 5 个 / 期权持仓 2 个 / 港股通汇率。
- **无 redis 版本**`bigqmt_no_redis/`):自包含 ZMQ transport + 无 redis DRYRUN,解决 QMT 沙箱 `import redis` 报错。
- **多账号使用文档**:README 加「多账号使用」章节(多策略实例 + 多 client)。
- **MiniQMT→BigQMT 转换 skill**docs + scripts + templatesPR #37)。
### 修复(Bug Fixes
- **QMT 自动退出**`ZmqQuotePushChannel.stop()` 跨线程关 SUB socket 触发 Windows signaler abort → 进程崩溃。改为订阅线程自己关 socket。
- **QMT 自动退出(系列)**`_adjust_phase` 无 exceptredis 故障崩策略)、`_publish_response` 逃出、deal_callback/forward_order_event/forward_trade_event/sync_positions_app 无防护、pending 队列满(queue.Full)、init() 无防护、socket_timeout=None 永久阻塞主线程、reset_app 不清理 quote-push/whole-quote(重启泄漏)、exec 事件每次回调新建 redis client(连接池泄漏)。
- **download_history_data 下载不了**Issue #32):`download_history_data` 是 QMT 全局函数不是 ContextInfo 方法,改走 `qmt_api` 注入。
- **复权数据返回全 0**(front/back):服务端需先下载原始数据 + 除权因子。下载类(`download_history_data2`)自动预下载;读取类(`get_market_data_ex`/`get_market_data`)自愈(检测全 0 → 服务端下载 → 重试)。
- **卖出方向误判**exec_events):QMT 回调 `m_nDirection` 恒为 48,改仲裁链(offset_flag > direction > op_type)。
- **query_orders/query_trades 返回空**`strategy_name` 过滤不匹配,默认改 `""` 返回全部。
- **get_financial_data 返回 None**:参数顺序错误(stock_list/table_list 反了)。
- **position_events 内存无限增长**Issue #21):xadd 无 maxlen,加 maxlen=2000。
- **异步回调签名错误**`on_order_stock_async_response`/`on_cancel_order_stock_async_response` 原生签名 1 参数(response 带 seq),之前传 2 参数导致 TypeError 被吞。
- **order_stock 返回 -1**`order_stock_async``result.get()` 崩,改为触发 `on_order_error`
- **客户端 transport 不匹配**Issue #24):`query_stock_asset` 返回 None 的根因是客户端 redis / 服务端 zmq 不匹配。
- **DRYRUN 硬编码路径**`_known_qmt_python_dir` 改 sys.path 扫描(paste-run 模式)。
- **ZMQ bind 冲突提示**:加端口占用检测 + 解决步骤提示。
### 变更(Changed
- 包发布:`pip install xtquant-big-convert`pyproject.toml 完善元数据 + LICENSE)。
- README 重写:依赖安装分客户端/服务端、API 总览、传输层对比、FormulaServer 直连、异步回调、无 redis 版本、日志排错、多账号、复权陷阱等章节。
---
## [0.1.0] - 2026-07-02
初始版本:Big QMT Redis RPC 桥接 + MiniQMT 兼容层。
### 新增
- Redis RPC 服务(rpush/blpop/brpop+ 可插拔传输层(redis/zmq/mysql/shm)。
- 客户端兼容层(`xtquant_compat`):`xt_trader` / `xtdata` 方法名映射。
- 行情/持仓/委托/下单基础 RPC 接口。
- `BIGQMT_REDIS_DRYRUN.py` QMT 编辑器入口。
+21
View File
@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2026 litaolemo
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
+998
View File
@@ -0,0 +1,998 @@
# xtquant_big_convert
[![PyPI](https://img.shields.io/pypi/v/xtquant-big-convert.svg)](https://pypi.org/project/xtquant-big-convert/)
[![Python](https://img.shields.io/pypi/pyversions/xtquant-big-convert.svg)](https://pypi.org/project/xtquant-big-convert/)
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE)
大 QMT 运行环境里的 RPC 桥接包:把大 QMT 内置 Python(行情查询、交易、持仓)封装成**可远程调用的服务**,并兼容一组 MiniQMT 方法名,让外部程序无需 XtQuantServer 权限就能驱动大 QMT。
支持 **Redis / ZMQ / MySQL / 共享内存** 四种可插拔传输,切换只需改一个配置字段。
已发布 PyPI,客户端一行安装:`pip install xtquant-big-convert`(详见下文「环境要求与依赖安装」)。
另附 [qmt-trader skill](qmt-trader/):让 Claude Code / ZCode / Cursor 等 AI 助手通过统一 CLI(46 个子命令)直接查行情、查持仓、下单撤单,详见下文「AI 助手 Skillqmt-trader」。
---
## 功能一览
### RPC 接口(远程可调用)
通过 RPC 可调用的大 QMT 能力(**白名单 117 个只读方法 + 2 个下单方法 + 12 个 MiniQMT 风格别名**,覆盖官方文档全部交易/查询函数):
| 类别 | 方法 |
|------|------|
| **系统** | `ping` |
| **行情快照** | `get_ticks` / `get_full_tick`(五档盘口)|| **合约/品种** | `get_instrument` / `get_instrument_type` / `get_stock_name` / `get_stock_type` / `get_last_close` / `get_last_volume` / `get_open_date` / `get_contract_expire_date` / `get_contract_multiplier` / `get_float_caps` / `get_total_share` / `get_turn_over_rate` / `get_weight_in_index` / `get_svol` / `get_bvol` / `get_risk_free_rate` / `is_stock_type` / `get_cb_info` |
| **K线/历史** | `get_market_data` / `get_market_data_ex` / `get_local_data` / `get_close_price` / `get_index_weight` |
| **L2 行情** | `get_l2_quote` / `get_l2_order` / `get_l2_transaction` / `subscribe_l2thousand`(需 L2 权限)|
| **板块** | `get_stock_list_in_sector` / `get_sector_list`* / `get_sector_info` / `create_sector` / `add_sector` / `remove_sector` |
| **交易日历/时段** | `get_trading_dates` / `get_holidays`* / `get_markets`* / `get_market_last_trade_date`* / `get_date_location` / `get_trading_calendar` / `get_trade_times` |
| **数据下载** | `download_history_data` / `download_history_data2` / `download_holiday_data` / `download_etf_info` / `download_cb_data` / `download_history_contracts` / `download_index_weight` / `download_sector_data` |
| **财务/因子** | `get_financial_data` / `download_financial_data` / `download_financial_data2` / `get_raw_financial_data` / `get_factor_data` |
| **ETF/期权/期货** | `get_etf_info` / `get_ipo_info` / `get_option_list` / `get_his_option_list` / `get_his_option_list_batch` / `get_option_detail_data` / `get_option_undl_data` / `get_option_undl` / `get_ETF_list` / `get_main_contract` / `get_his_contract_list` |
| **期权定价** | `bsm_price` / `bsm_iv` / `get_option_iv` |
| **龙虎榜/股东** | `get_longhubang` / `get_top10_share_holder` / `get_holder_num` / `get_turnover_rate`(区间换手率)/ `get_industry` / `get_his_st_data` / `get_his_index_data` |
| **资金流** | `get_north_finance_change`(北向)/ `get_hkt_statistics`(港股通)/ `get_hkt_details` / `get_hkt_exchange_rate` |
| **因子/模型** | `call_formula` / `subscribe_formula` / `unsubscribe_formula` / `get_formula_result` / `gen_factor_index` |
| **时间转换** | `datetime_to_timetag` / `timetag_to_datetime` / `timetagToDateTime`(纯本地计算)|
| **账户查询** | `get_asset`(资金)/ `get_positions`(持仓)/ `query_stock_position`(单股持仓)/ `query_orders`(委托)/ `query_trades`(成交)/ `get_history_trade_detail_data`(历史成交)/ `get_value_by_order_id` / `get_last_order_id` |
| **新股/打新** | `get_ipo_data` / `get_new_purchase_limit` |
| **融资融券** | `get_assure_contract`(担保品)/ `get_enable_short_contract`(融券标的)/ `get_unclosed_compacts`(未平仓)/ `get_closed_compacts`(已平仓)/ `get_debt_contract`(负债)—— 需两融权限,普通账户降级为空 |
| **期权持仓** | `get_option_subject_position`(标的持仓)/ `get_comb_option`(组合期权)|
| **持仓同步** | `sync_positions`(写回 Redis 供客户端缓存)|
| **下单/撤单** | `submit_order` / `cancel_order`(默认关闭,需显式开启)|
> 客户端兼容层 `BigQmtXtData` 对常用方法有显式封装(`xtdata.get_longhubang(...)`、`xtdata.bsm_price(...)` 等),其余通过万能入口 `xtdata.call_method("get_float_caps", stockcode="000001.SZ")` 调用。
> `*` 标记的方法在大 QMT(完整交易端)环境下用 **fallback** 实现(非原生数据):`get_sector_list` 返回常用板块名清单,`get_holidays` 从交易日历反推,`get_markets` 返回固定市场集合,`get_market_last_trade_date` 从日历派生。详见 [docs/RPC_API_REFERENCE.md](docs/RPC_API_REFERENCE.md) 第 8 节「大 QMT 环境的能力边界」。
### 客户端兼容层
- `bigqmt_signal_trader.xtquant_compat`:把旧代码的 `xt_trader` / `xtdata` 调用转成 RPC,无需改业务代码。
- 兼容 MiniQMT 方法名:`query_stock_asset` / `query_stock_positions` / `query_stock_orders` / `get_full_tick` / `order_stock` 等。
- **完整 xtconstant 枚举**91 个常量,对齐原生 MiniQMT):账号类型、委托类型(股票/期货/信用/期权)、报价类型、委托状态、账号状态、`ORDER_TYPE_SET`
```python
# 旧代码零改动(自动命中 shim
from xtquant.xtconstant import STOCK_BUY, FIX_PRICE, ORDER_SUCCEEDED
# 或直接从 compat 导入
from bigqmt_signal_trader.xtquant_compat import (
SECURITY_ACCOUNT, STOCK_BUY, FIX_PRICE, CREDIT_FIN_BUY,
FUTURE_OPEN, ACCOUNT_STATUS_OK, ORDER_SUCCEEDED,
)
```
### 异步回报回调(MiniQMT 风格,实盘验证)
客户端注册 `XtQuantTraderCallback` 子类,`connect()`/`subscribe()` 后实时接收委托/成交/错误回报(通过 Redis pubsub 推送):
```python
from bigqmt_signal_trader.xtquant_compat import (
StockAccount, XtQuantTraderCallback, configure, xt_trader,
)
class MyCallback(XtQuantTraderCallback):
def on_stock_order(self, order):
print("委托回报:", order.stock_code, order.order_status, order.order_sysid)
def on_stock_trade(self, trade):
print("成交回报:", trade.stock_code, trade.order_id, trade.traded_volume, trade.traded_price)
def on_order_error(self, order_error):
print("委托失败:", order_error.order_id, order_error.error_id, order_error.error_msg)
def on_cancel_error(self, cancel_error):
print("撤单失败:", cancel_error.order_id, cancel_error.error_id, cancel_error.error_msg)
def on_order_stock_async_response(self, response):
print("异步下单回报:", response.account_id, response.order_id, response.seq)
def on_account_status(self, status):
print("账户状态:", status.account_id, status.account_type, status.status)
configure()
xt_trader.register_callback(MyCallback())
acc = StockAccount(xt_trader.client.account_id, "STOCK")
xt_trader.connect()
xt_trader.subscribe(acc)
# 异步下单(返回 seq,回报走回调)
seq = xt_trader.order_stock_async(acc, "600654.SH", 23, 100, 11, 2.95, "rpc_test", "备注")
```
**完整的回调链**(对齐 MiniQMT 原生语义,实盘验证):
| 回调 | 触发时机 | 已验证 |
|------|---------|--------|
| `on_account_status` | `connect()`/`subscribe()` 后 | ✅ |
| `on_order_stock_async_response(seq, resp)` | 异步下单提交成功 | ✅(实盘)|
| `on_stock_order(order)` | 委托状态变化(已报 50 / 已成 56 / 废单 57)| ✅(实盘)|
| `on_stock_trade(trade)` | 成交回报 | ✅ |
| `on_order_error(err)` | 废单/拒单(服务端检测 status=57 推送)| ✅(实盘)|
| `on_cancel_error(err)` | 撤单失败 | ✅ |
| `on_cancel_order_stock_async_response` | 异步撤单回报 | ✅ |
**`*_async` 查询方法**(对齐 MiniQMT 签名,callback 可选):
```python
# 方式 1callback 接收结果(MiniQMT 原生语义,返回 None
xt_trader.query_stock_asset_async(acc, lambda asset: print(asset.cash, asset.total_asset))
xt_trader.query_stock_positions_async(acc, lambda positions: print(len(positions)))
# 方式 2:不传 callback,返回 seq(我们的扩展)
seq = xt_trader.query_stock_orders_async(acc)
```
**注意**:QMT 必须运行在**实盘模式**(非模拟/模型交易)才能收到完整回报。模拟模式下委托进 QMT 界面但不在真实委托队列,`query_orders` 查不到、`order_stock` 返回 -1(触发 `on_order_error`)。
### 全推行情订阅(subscribe_whole_quote 真推送)
`subscribe_whole_quote` 是**服务端真推送**——对齐 MiniQMT 全推行情订阅。服务端引用计数管理 `ContextInfo.subscribe_whole_quote` 回调,通过独立 PUB/SUB 通道向客户端**增量推送**行情(不是一次性快照):
**架构(三通道)**
1. **控制面 RPC**——`subscribe_whole_quote` / `unsubscribe_whole_quote` / `quote_keepalive` 方法(复用现有 transport
2. **数据面推送**——`QuotePushChannel` 单向 PUB/SUBredis pub/sub 或 zmq PUB/SUB,按部署 transport 选择;msgpack 编码 + json 兜底)
3. **Big-QMT 行情源**——`QuoteSubscriptionManager` 按组合键归一化共享(大写/去空格/排序),多客户端共享一个底层订阅
**关键设计**
- **组合键去重**:不同客户端订阅相同标的组合,只占一个 big-QMT 订阅
- **引用计数**:按 `(client_id, sub_id)` 计数,全部退订或 30s keepalive 超时才销毁
- **客户端心跳**:周期 `quote_keepalive`;检测推送静默(默认 10 轮心跳)自动重放订阅,**服务端重启后自动恢复**
- **初始快照**:客户端用 `get_full_tick` 预拉快照(big-QMT 回调是增量的)
**用法**
```python
from bigqmt_signal_trader.xtquant_compat import configure, xtdata
configure()
# 订阅全推行情(callback 收到增量推送)
def on_quote(data):
for code, tick in data.items():
print(code, tick.get("lastPrice"))
seq = xtdata.subscribe_whole_quote(["600000.SH", "000001.SZ"], callback=on_quote)
# 退订
xtdata.unsubscribe_quote(seq)
```
**验证**:实盘交易日验证 1/20/50/100 只标的,3s 推送节奏稳定,零丢失零乱序;多客户端共享/退订隔离/同客户端多 sub_id 全过;服务端重启恢复(42s 中断后验证两次)。详见 [docs/SUBSCRIBE_WHOLE_QUOTE_PUSH.md](docs/SUBSCRIBE_WHOLE_QUOTE_PUSH.md) 和 [docs/SUBSCRIBE_WHOLE_QUOTE_LIVE_VERIFICATION.md](docs/SUBSCRIBE_WHOLE_QUOTE_LIVE_VERIFICATION.md)。
### 可插拔传输层
| 传输 | 同机 p50 | 跨机 | 适用场景 |
|------|---------|------|---------|
| **redis**(默认)| ~13ms | ✅ | 生产默认,稳定 |
| **zmq** | ~0.7ms* | ✅ | 同机低延迟 |
| **mysql** | ~105ms | ✅ | 兼容兜底 |
| **shm** | — | ❌ | 接口预留(未实现)|
*zmq fast-path;约 30% 请求会撞 QMT 的 GIL 调度尖峰(~500ms)。
### FormulaServer 直连快速路径(只读行情,默认开启)
大 QMT 的 `58600` 端口是 **FormulaServer**——QMT 内置的 C++ 行情/参考数据服务(端口取自
`config/formulaserver/formulaserver.ini``[server_formula] address`)。QMT 自带 Python
`qmt_api` 包就是它的客户端。
客户端对这些方法会**绕开整条 RPC 链路**(不经过 QMT 的 python 策略线程,也不抢 GIL),
实测 **p50 0.07ms**,穿过完整客户端栈是 **0.145ms/次**
| 对比 | p50 |
|------|-----|
| redis RPC | ~13ms |
| zmq RPC | ~0.7ms30% 撞 500ms GIL 尖峰)|
| **FormulaServer 直连** | **0.07ms**(无 GIL 竞争)|
直连覆盖 10 个方法:`get_instrument` / `get_instrument_detail` / `get_instrumentdetail` /
`get_last_volume` / `get_total_share` / `get_contract_multiplier` / `get_main_contract` /
`get_weight_in_index` / `get_stock_list_in_sector` / `get_market_data_ex`
**能力边界(重要)**FormulaServer 只有行情/参考数据。所有账户、持仓、委托、成交、下单
方法一律返回 `ErrorID 200005 未找到该服务``getFullTick`/`getQuote` 也不存在。所以它是
**只读快速路径,不是 RPC 桥的替代品**——交易、账户查询、五档盘口仍然走 RPC。
以下方法**刻意不走**直连,因为参数语义与我们的调用方不一致,宁慢勿错:
- `get_trading_dates` —— FormulaServer 要**股票代码**`000001.SZ`),传市场代码(`SH`)静默返回 `[]`,而我们的调用方传的是市场。
- `get_divid_factors` / `get_risk_free_rate` —— 参数语义不同(区间 vs 单日、index vs timetag)。
- **复权 K 线** —— 实测 `dividendType``none``front` 返回完全相同,复权未生效。因此只有
`dividend_type="none"` 才走直连,其余回退 RPC,避免静默返回未复权价格。
(复权数据还需**先在服务端下载原始数据**,见下文「复权数据下载陷阱」。)
配置(客户端侧,默认就是开启,通常不用写):
```python
BIGQMT_REDIS_CONFIG = {
"formula_server": {
"enabled": True, # 或环境变量 BIGQMT_FORMULA_ENABLED=0 关闭
# "host": "127.0.0.1", # 默认本机;FormulaServer 绑 0.0.0.0,跨机需放行防火墙
# "port": 58600, # 不写则从 qmt_root 的 ini 读,再退回 58600
# "qmt_root": r"D:\国金证券QMT交易端",
# "timeout_seconds": 3.0,
# "methods": ["get_instrument"], # 只路由白名单里的方法
# "failure_cooldown_seconds": 30.0, # 连不上后停用多久再重试
},
}
```
**失败一律自动回退 RPC**:方法未映射、参数translate 不了、服务没起、连接断——都退回原路径,
所以连不上 58600 的客户端行为与改动前完全一致。BSON 编解码内置了无依赖实现(可选用
pymongo 的 `bson`,两者输出实测逐字节一致),客户端不需要额外装包。
### QMT 启停 / 自动重启(qmt_launcher
大 QMT 基本每天早上要重启一次,卡点在登录框。两条路绕过它:
```bash
python -m bigqmt_signal_trader.qmt_launcher status --dir "D:\国金证券QMT交易端_lemo"
python -m bigqmt_signal_trader.qmt_launcher restart --dir "D:\国金证券QMT交易端_lemo"
```
| mode | 做什么 | 需要登录框交互 |
|------|--------|---------------|
| `linkmini`(默认优先)| `XtMiniQmt.exe linkMini`MiniQMT 免密启动 | 否 |
| `bat` | 跑指定批处理(如 `免密登录qmt.bat`| 否 |
| `exe` | 直接起 `XtItClient.exe`,靠终端自身恢复会话 | 否 |
| `login` | 起 exe 后向登录框输入账号密码 | 是,需 pywin32 |
**关于「pywinauto/pyautogui 要求 Windows 处于登录状态」**`login` 模式用的是
`win32api.SendMessage` 直接投递到窗口句柄,不是 pyautogui 那种按屏幕坐标重放物理输入。
前者不要求窗口置于前台,锁屏下也能工作(会话还在即可,完全注销则不行)。密码从
环境变量 `BIGQMT_LOGIN_USER` / `BIGQMT_LOGIN_PASSWORD` 读,不走命令行参数——argv
对同机任何进程可见。
两个设计要点:
- **按安装目录隔离**。同机常并行跑多个 QMT,`taskkill /im XtItClient.exe` 会误杀别人的
实盘。这里只终结 `--dir` 对应 `bin.x64` 下的进程;拿不到 exe 路径的进程直接跳过而不是
猜。
- **等就绪而不是 sleep 固定秒数**。启动完成的判据是 FormulaServer 端口(58600)能接受连接,
超时抛 `QmtLauncherError` 而不是静默返回,避免定时任务在没起来的终端上继续跑。
`restart` 默认在关闭后等 5 秒再启动:ZMQ 传输是精确绑定配置端口(不扫描),socket 没
完全释放就重启会绑定失败。
### 独立 ZMQ 回测桥接
`bigqmt_backtest` 与实盘 RPC 桥接完全分离,提供两个明确隔离的后端:
- `QMT_NATIVE``BIGQMT_ZMQ_BACKTEST.py` 运行在 QMT 回测进程内。QMT 负责历史
行情推进、资金持仓、`passorder/cancel` 和原生撮合;ZMQ 只桥接 Bar、订单意图及
QMT 委托/成交结果。
- `LOCAL_SIM`:端口 `16661` 的独立 CSV 工具,仅用于脱离 QMT 验证协议和策略逻辑,
使用本地撮合并输出本地结果文件。
QMT 原生入口使用独立端口 `16662`、独立 `run_id/client_id`,强制验证
`ContextInfo.do_back_test=true`,固定 `live_ready=false`,不会导入或修改
`bigqmt_signal_trader`
启动 CSV 独立测试服务:
```powershell
python -m pip install -e .
python -m bigqmt_backtest.server `
--data examples/backtest_bars.example.csv `
--config examples/backtest_config.example.json `
--run-id demo-001 `
--bind tcp://127.0.0.1:16661
```
另开一个终端运行外部策略:
```powershell
python examples/zmq_backtest_strategy.py `
--endpoint tcp://127.0.0.1:16661 `
--run-id demo-001 `
--symbol 600000.SH `
--fast 2 `
--slow 3
```
QMT 原生安装、逐 Bar 同步协议、CSV 备用模式和安全边界见
[docs/ZMQ_BACKTEST_BRIDGE.md](docs/ZMQ_BACKTEST_BRIDGE.md)。
### 无 redis 版本(QMT 沙箱拒绝 import redis 时用)
如果你的 QMT 环境**拒绝 `import redis`**(券商白名单拦截),用 `bigqmt_no_redis/` 目录下的无 redis 版本:
- `bigqmt_no_redis/zmq_transport.py` — 自包含的 ZMQ transport,内联所有编码函数,**完全不 import redis_common/redis_rpc**,去掉 redis 服务发现(用静态派生端口)
- `bigqmt_no_redis/DRYRUN_no_redis.py` — 无 redis 的 DRYRUN 入口,强制 `transport=zmq` + `background_threads=True`,只加载 zmq transport
**用法**QMT 策略编辑器加载 `BIGQMT_DRYRUN_NO_REDIS.py`(同步到 QMT 目录时用这个文件名),RPC 走纯 ZMQ,零 redis 依赖。其余功能(行情/交易/持仓查询)与标准版一致。
### 委托/成交查询的 strategy_name 陷阱(重要)
`get_trade_detail_data``strategy_name` 过滤委托/成交——**下单时用的 strategy_name 必须和查询时一致**,否则查不到。
- 下单时传 `strategy_name="rpc_test"` → 委托记在 `rpc_test`
- 查询时传 `strategy_name="bigqmt_signal_trader"` → 返回空(不匹配)
**修复**`query_orders` / `query_trades` 默认传**空字符串 `""`**,返回该账户的**全部**委托/成交(不按 strategy_name 过滤)。如需过滤,显式传 `strategy_name`
实测验证(`get_trade_detail_data` 探测):
- `st=""` → ORDER=9, DEAL=9(全部)
- `st="rpc_test"` → ORDER=3, DEAL=1(只有 rpc_test 的)
- `st="bigqmt_signal_trader"` → ORDER=0, DEAL=0(空)
### 复权数据下载陷阱(重要)
**前/后复权 K 线必须先在服务端下载原始数据,否则返回全 0**
Big QMT 的复权(`dividend_type='front'`/`'back'`)是**服务端现场计算**的——需要原始 K 线 + 除权因子已经在服务端存在。直接请求 front 而服务端没下载过原始数据时,返回的 close 全是 `0.0`(只有最后一根有价)。
实测复现(600654.SH / 600227.SH):
- 直接 `get_market_data_ex(dividend_type='front')` → 634 行全 0
-`download_history_data` 后再请求 → 真实复权价(front ≠ none,复权生效)
**已修复**`xtdata.download_history_data2(codes, period, dividend_type='front')` 现在会**自动先触发服务端原始数据下载**(拉原始 K 线 + 除权因子),再拉复权数据到本地缓存。用法不变:
```python
# 前复权下载(自动先服务端下载原始数据 + 除权因子)
xtdata.download_history_data2(["600654.SH"], period="1d",
start_time="20240101", dividend_type="front")
# 之后本地读取(零 RPC
xtdata.get_local_data(["close"], ["600654.SH"], period="1d",
start_time="20240101", dividend_type="front")
```
**读取类 API 也自愈**`get_market_data_ex` / `get_market_data` 带复权参数时,若检测到返回全 0(服务端缺原始数据),会自动触发服务端下载、等待落盘、重试一次,拿到真实复权价。`get_local_data` 的 fallback 拉取同样受益。无需手动等待。
注意:QMT 服务端下载是**异步落盘**的,自愈路径内置了等待 + 一次重试;极端大区间若一次重试仍全 0,可稍后重读或先显式 `download_history_data2`
### 实盘卖出方向误判修复(exec_events
实盘发现:QMT 回调里 `m_nDirection` **恒为 48**(即使是卖出),导致卖出被误判为买入。
修复(`exec_events._extract_direction`)改为仲裁链:
1. `m_nOffsetFlag`(最可靠,匹配 `query_orders`
2. `m_nDirection`(传统 EEntrustBS,但实盘可能恒为 48
3. 当 direction≠offset(期货:卖+开仓=49+48),用 `m_nOpType`23=买/24=卖)仲裁
4. `m_nOpType`/`order_type`(兜底)
对股票现货,direction=offset48=买/49=卖);对期货,direction≠offset,仲裁保正确。
### 多账号使用(股票+期货 / 普通+信用)
当前架构是**单账号单实例**——一个 QMT 策略进程绑定一个账号,RPC channel 按 `account_id` 隔离(`bigqmt:rpc:req:{account_id}`)。多账号场景(如股票+期货、普通+信用账户同时交易)的推荐方案是**在 QMT 里跑多个策略实例**,每个实例绑一个账号。
#### 方案:多策略实例(推荐,不改代码)
**服务端(QMT 内)**:为每个账号创建一个独立的配置文件和 DRYRUN 入口。
```python
# bigqmt_signal_trader_local_config_stock.py — 股票账号
BIGQMT_ACCOUNT_ID = "你的股票账号"
BIGQMT_REDIS_CONFIG = {
"host": "...", "port": 6379, "db": 5, "password": "...",
"transport": "redis", # 或 "zmq"
"account_type": "STOCK", # 股票
# ...
}
# bigqmt_signal_trader_local_config_credit.py — 信用账号
BIGQMT_ACCOUNT_ID = "你的信用账号"
BIGQMT_REDIS_CONFIG = {
"host": "...", "port": 6379, "db": 5, "password": "...",
"transport": "redis",
"account_type": "CREDIT", # 信用(两融)
# ...
}
```
然后在 QMT 策略编辑器里加载两个 DRYRUN 文件(每个指向不同的配置),分别运行。两个实例的 RPC channel 自动隔离(按 account_id)。
> **zmq 模式注意**:每个实例的 zmq 端口从 account_id 派生(`15560 + account_id mod 100`),不同账号自动不冲突。
**客户端(外部程序)**:为每个账号创建独立的 client/trader 对象。
```python
from bigqmt_signal_trader.xtquant_compat import BigQmtRpcClient, BigQmtXtTrader, StockAccount
# 股票账号
stock_client = BigQmtRpcClient(account_id="股票账号", redis_config={...})
stock_trader = BigQmtXtTrader(account_id="股票账号", redis_client=stock_client.redis_client)
stock_acc = StockAccount("股票账号", "STOCK")
# 信用账号
credit_client = BigQmtRpcClient(account_id="信用账号", redis_config={...})
credit_trader = BigQmtXtTrader(account_id="信用账号", redis_client=credit_client.redis_client)
credit_acc = StockAccount("信用账号", "CREDIT")
# 分别查询/下单
stock_asset = stock_trader.query_stock_asset(stock_acc)
credit_positions = credit_trader.query_stock_positions(credit_acc)
```
> **跨账号隔离**:每个账号的 RPC channel、持仓查询、委托回报完全隔离(按 `account_id` 路由),互不影响。
---
## 环境要求与依赖安装
本系统分两部分,各自需要自己的 Python 环境和依赖:
| 部分 | 运行位置 | Python | 装什么 |
|------|---------|--------|--------|
| **客户端**(外部程序)| 你的开发机 | 3.8+(推荐)| `pip install xtquant-big-convert` |
| **服务端**QMT 内)| QMT 的 `bin.x64/python.exe` | 3.6(QMT 自带)| 按传输装 1 个包 |
### A. 客户端(外部程序,推荐 pip 安装)
客户端就是**写策略/调接口的那台电脑**(也叫「开发机」)。直接 pip 安装:
```powershell
# 基础安装(含 pyzmq,zmq 传输必需)
pip install xtquant-big-convert
# 含 redis 支持(redis 传输)
pip install xtquant-big-convert[redis]
# 含 mysql 支持(mysql 传输)
pip install xtquant-big-convert[mysql]
# 开发环境(含测试工具)
pip install xtquant-big-convert[dev]
# 从源码安装(开发模式)
git clone https://github.com/litaolemo/xtquant_big_convert.git
cd xtquant_big_convert
pip install -e .
```
安装后可直接 import
```python
from bigqmt_signal_trader.xtquant_compat import configure, xt_trader, xtdata
from bigqmt_signal_trader.transports.factory import build_transport
configure()
print(xtdata.get_full_tick(["000001.SZ"]))
```
### B. 服务端(QMT 内 Python 3.6
QMT 自带 Python 3.6`bin.x64/python.exe`),**只需按你选的传输装对应依赖**:
| 传输 | 服务端需要的包 | 客户端需要的包 |
|------|--------------|--------------|
| **redis**(默认)| `redis`QMT 通常已内置)| `redis` |
| **zmq** | `pyzmq` | `pyzmq`(基础安装已含)|
| **mysql** | `pymysql` + `DBUtils` | `pymysql` + `DBUtils` |
> ⚠️ **用 redis 传输就不需要装 pyzmq / pymysql / DBUtils**——下面的安装说明是按需的,你用什么传输装什么。
**安装到 QMT 的 Python(以 zmq / mysql 为例):**
QMT 的 Python 3.6 用旧 OpenSSLpip 直连 HTTPS 镜像会报 SSL 错误。有两种方法:
```powershell
# 方法 A:从开发机拷贝纯 Python 包(推荐,绕过 SSL 问题)
# pymysql / DBUtils 是纯 Python,可直接拷贝;在开发机(已装这些包)执行:
$QMT_SITE = "D:\国金证券QMT交易端\bin.x64\Lib\site-packages"
Copy-Item -Recurse "C:\Users\<你>\anaconda3\Lib\site-packages\pymysql" "$QMT_SITE\pymysql"
Copy-Item -Recurse "C:\Users\<你>\anaconda3\Lib\site-packages\dbutils" "$QMT_SITE\dbutils"
# 方法 B:用 QMT python pip 装(可能因 SSL 失败,需配置信任)
cd D:\国金证券QMT交易端
.\bin.x64\python.exe -m pip install --trusted-host mirrors.aliyun.com pymysql DBUtils
```
验证安装:
```powershell
.\bin.x64\python.exe -c "import pymysql; from dbutils.pooled_db import PooledDB; print('OK')"
```
> **pyzmq 特殊说明**:包含 C 扩展,不能直接拷贝。Python 3.6 需装 `pyzmq==19.0.2`(最后一个支持 3.6 的版本)。如果 SSL 装不上,可下载对应 wheel 手动 `pip install xxx.whl`。
---
## 快速开始
> 前置:客户端已按上面「A. 客户端」装好包;服务端按「B. 服务端」装好所选传输的依赖。下面是从零跑通整套流程的步骤。
### 第 1 步:同步代码到 QMT 的 python 目录
把以下内容复制到大 QMT 的 `python` 目录(如 `D:\国金证券QMT交易端\python\`):
```
src/bigqmt_signal_trader/ (整个核心包,含 transports/
src/bigqmt_signal_trader_strategy.py
src/bigqmt_signal_trader_redis_rpc_runtime.py
src/BIGQMT_REDIS_DRYRUN.py (★ QMT 编辑器入口,GBK 编码,在 QMT 里加载这个)
```
> **在 QMT 策略编辑器里只加载 `BIGQMT_REDIS_DRYRUN.py` 一个文件**。它会自动 import 上面其余文件。其余 `.py``bigqmt_signal_trader_*`)是它依赖的模块,不是直接运行的入口。
### 第 2 步:创建 QMT 端私有配置
在 QMT 的 `python` 目录创建 `bigqmt_signal_trader_local_config.py`**不要提交此文件**):
```python
# coding: utf-8
BIGQMT_ACCOUNT_ID = "你的资金账号" # 如 "1234567890"
BIGQMT_REDIS_CONFIG = {
"host": "你的Redis地址", # 如 "192.168.1.100"
"port": 6379,
"db": 5,
"password": "你的Redis密码",
# === 传输选择(默认 redis,生产推荐)===
# "transport": "redis", # 不写就是 redis
# 切 zmq(同机低延迟,实测 p50~0.3ms):装了 pyzmq 后只需这一行。
# 非 redis 传输会自动开 background_threads;端口按账号派生 127.0.0.1:1556x。
# "transport": "zmq",
# 切 mysql(兼容兜底):需装 pymysql+DBUtils,同样自动开 background_threads。
# "transport": "mysql",
# "mysql": {"driver":"pymysql","host":"...","port":3306,"user":"root",
# "password":"...","database":"bigqmt_rpc","charset":"utf8mb4"},
"rpc_allow_order_methods": False, # 下单默认关闭
"rpc_process_in_listener": True, # 只读请求在收包线程直接处理(低延迟)
"rpc_listener_methods": ("*",), # * = 所有只读方法
"rpc_background_threads": False, # redis 用 QMT adjust 线程 drain
"schedule_adjust": True,
"schedule_adjust_interval": "500nMilliSecond",
}
```
> **重要**:切到 zmq 或 mysql 时,必须同时设 `"rpc_background_threads": True`(这两种传输用自己的后台线程,不走 QMT 回调 drain)。
### 第 3 步:在 QMT 里运行策略(BIGQMT_REDIS_DRYRUN.py
**入口文件是 `src/BIGQMT_REDIS_DRYRUN.py`**(GBK 编码,QMT 友好)。在 QMT 策略编辑器加载并运行它。
#### 这个文件做什么
它是 QMT 编辑器入口的"外壳"(shell),按顺序做 5 件事:
1. **定位 python 目录**:把 QMT 的 `python` 目录加到 `sys.path`,让 `bigqmt_signal_trader` 包能 import。
2. **reload 模块**`importlib.reload` 刷新 `redis_common` / `redis_rpc` / `strategy` / `runtime` —— QMT 在编辑器里重跑策略时,进程不退出,reload 确保新代码立即生效。
3. **注入 Redis 配置**:读 `bigqmt_signal_trader_local_config.py` 里的 `BIGQMT_REDIS_CONFIG`,调 `configure_runtime_redis()`
4. **注入账号**:读 `BIGQMT_ACCOUNT_ID`,调 `configure_runtime_account()`。如果配置没给,fallback 用 QMT 全局变量 `account`
5. **绑定 QMT 原生 API**:把 QMT 内置的 `passorder` / `cancel` / `get_trade_detail_data` 函数绑进 runtime(用 `try/except NameError` 包住,因为这些名字只在大 QMT 进程内存在)。
6. **导出 QMT 回调**`init = _runtime.init` / `handlebar = _runtime.handlebar` / `adjust = _runtime.adjust` 等,让 QMT 能回调到我们的策略逻辑。
#### ⚠️ 硬编码路径(重要)
`BIGQMT_REDIS_DRYRUN.py` 里有**一处写死的 QMT python 目录路径**,作为 `__file__` 找不到时的 fallback
```python
def _known_qmt_python_dir():
root = "".join(chr(value) for value in (0x56fd, 0x91d1, 0x8bc1, 0x5238)) # 国金证券
suffix = "".join(chr(value) for value in (0x4ea4, 0x6613, 0x7aef)) # 交易端
return "D:\\" + root + "QMT" + suffix + "\\python"
# 解码后 = D:\国金证券QMT交易端\python
```
- **`chr()` 编码**是为了规避 QMT 用 GBK 保存策略文件时中文乱码(用 Unicode 码点拼出"国金证券交易端")。
- **路径优先级**:先用 `__file__` 所在目录(脚本实际位置),找不到才用这个硬编码 fallback。
- **如果你的 QMT 装在别的路径**(比如 `D:\华泰QMT\python`):通常不用改,因为 `__file__` 优先。但如果你用 `exec` 方式加载(`__file__` 未定义),需要把 `_known_qmt_python_dir()` 改成你的路径,或直接硬编码:
```python
def _known_qmt_python_dir():
return r"D:\你的券商QMT\python"
```
#### 启动成功标志(QMT 输出面板)
```
[bigqmt_shell] reload entry paths=['D:\\国金证券QMT交易端\\python']
[bigqmt_shell] local redis config loaded keys=['host', 'port', 'db', ...]
[bigqmt_shell] local account config loaded=True
[bigqmt_rpc] transport=redis mode process_in_listener=True listener_methods=('*',) ...
[bigqmt_rpc] started channel=bigqmt:rpc:req:你的账号
[bigqmt_signal_trader] init ok
```
> **为什么是 GBK 编码?** QMT 的策略编辑器用本地代码页(中文 Windows 是 GBK)保存文件。文件头 `#coding:gbk` 声明编码,避免 QMT 保存时破坏 UTF-8 内容。源码本身是 ASCII(中文用 `chr()` 拼),所以实际不会乱码。
> **为什么不直接用 `bigqmt_signal_trader_redis_rpc_runtime.py`?** 那个文件是纯逻辑入口,不包含 reload 和 QMT API 绑定。`BIGQMT_REDIS_DRYRUN.py` 是给 QMT 编辑器专用的外壳,处理了 QMT 进程不退出导致模块缓存、API 绑定等坑。在 QMT 里**只加载 `BIGQMT_REDIS_DRYRUN.py`**。
### 第 4 步:客户端调用
**方式 A:用兼容层(推荐,旧代码零改动)**
客户端创建配置文件 `bigqmt_signal_trader_client_config.py`(与上面类似但用客户端视角),然后:
```python
from bigqmt_signal_trader.xtquant_compat import StockAccount, configure, xt_trader, xtdata
configure()
acc = StockAccount(xt_trader.client.account_id, "STOCK")
# 行情
ticks = xtdata.get_full_tick(["000001.SZ"])
print(ticks["000001.SZ"]["lastPrice"])
# 持仓 / 资金
positions = xt_trader.query_stock_positions(acc)
asset = xt_trader.query_stock_asset(acc)
print(asset.cash, asset.total_asset)
# K线(自动还原成 pandas DataFrame
klines = xtdata.get_market_data_ex(
field_list=["close"], stock_list=["000001.SZ"], period="1d", count=5
)
```
**方式 B:直接 RPC 调用**
```python
from bigqmt_signal_trader.redis_rpc import call_redis_rpc
import redis
r = redis.Redis(host="192.168.1.100", port=6379, db=5, password="...")
resp = call_redis_rpc(r, "你的账号", "get_full_tick", {"codes": ["000001.SZ"]})
print(resp["data"]["000001.SZ"]["lastPrice"])
```
**方式 C:无缝替换旧 xtquant(最终切换)**
把仓库 `src` 放到 `PYTHONPATH` 最前面,旧代码的 `from xtquant import xtdata` 自动命中本仓库 shim
```powershell
$env:PYTHONPATH = "D:\gjzqqmt\xtquant_big_convert\src;$env:PYTHONPATH"
```
```python
# 旧代码完全不改
from xtquant import xtdata
ticks = xtdata.get_full_tick(["600000.SH"]) # 走 RPC 到大 QMT
```
---
## 切换传输层
### 只需改一个字段
服务端 + 客户端的配置文件里,`transport` 字段保持一致即可:
```python
BIGQMT_REDIS_CONFIG = {
"transport": "zmq", # redis / zmq / mysql / shm
"zmq": {"host": "127.0.0.1"}, # 各传输子配置
# redis 配置保留(zmq 服务发现、mysql 不需要时的 fallback 都用它)
}
```
### 各传输配置示例
**Redis(默认)**
```python
{"transport": "redis"} # 或省略 transport 字段
```
**ZMQ**(同机低延迟,需 pyzmq):
```python
{
"transport": "zmq",
"rpc_background_threads": True, # 必须!
"zmq": {
"host": "127.0.0.1", # 默认端口从 account_id 派生
# "port": 5560, # 可显式指定
# 端口冲突时自动找空闲端口 + 通过 Redis 服务发现告知客户端
},
}
```
**MySQL**(兼容兜底,需 pymysql + DBUtils):
```python
{
"transport": "mysql",
"rpc_background_threads": True, # 必须!
"mysql": {
"driver": "pymysql",
"host": "192.168.1.100", "port": 3306,
"user": "root", "password": "...",
"database": "bigqmt_rpc", "charset": "utf8mb4",
"poll_interval_seconds": 0.01,
"pool_config": {"mincached": 1, "maxcached": 3, "maxshared": 0, "maxconnections": 4},
},
}
```
### ZMQ 端口与服务发现
- 默认端口从 account_id 派生:`15560 + (账号数字 mod 100)`,不同账号自动不冲突。
- 端口被占时,server 自动往上扫描找空闲端口,把真实地址写到 Redis key `bigqmt:zmq:addr:{account_id}`TTL 300s)。
- 客户端连接时按优先级解析地址:显式 `connect_address` > Redis 服务发现 > 默认派生端口。
- server 退出时自动清理 discovery key。
- 服务发现是可选的(没配 Redis client 时退化为静态派生端口)。
完整传输层文档见 [docs/RPC_TRANSPORTS.md](docs/RPC_TRANSPORTS.md)。
---
## 实测延迟对比(真实直连 QMT)
三种传输全部实测,端到端连接真实 QMT 进程,n=15/方法:
| 传输 | ping p50 | get_full_tick p50 | 成功率 | 尖峰来源 |
|------|---------|------------------|--------|---------|
| **Redis** | 13ms | 15ms | 100% | 偶发 245ms(网络抖动)|
| **ZMQ** | 0.7ms* | 0.7ms* | 100% | 30% 撞 500msQMT adjust GIL|
| **MySQL** | 104ms | 110ms | 100% | 轮询开销 |
*ZMQ fast-path(避开 GIL 尖峰的请求);overall p90 ~498ms。
**生产推荐 Redis**:稳定、跨机、无 GIL 问题、QMT 端零额外依赖。ZMQ 理论最快但受 QMT 主线程 GIL 调度影响。MySQL 仅作兜底。
复现基准:
```powershell
python bench_latency.py # Redis 单传输延迟
python bench_transports.py -n 100 # Redis vs ZMQ 对比
```
---
## 目录结构
```
src/bigqmt_signal_trader/
├── transports/ 可插拔传输层
│ ├── base.py RpcTransport 抽象接口
│ ├── redis_transport.py Redis(默认,rpush/blpop/brpop
│ ├── zmq_transport.py ZMQROUTER/DEALER + 服务发现)
│ ├── mysql_transport.py MySQL(轮询 + DBUtils 连接池)
│ ├── shm_transport.py 共享内存(stub)
│ └── factory.py build_transport 工厂
├── adapters/ QMT API 适配器
│ ├── market_bigqmt.py 行情(ContextInfo 封装)
│ ├── order_bigqmt.py 下单(passorder
│ ├── position_bigqmt.py 持仓(get_trade_detail_data
│ └── redis_common.py Redis 连接/编解码
├── redis_rpc.py RPC 服务(handlers + service + transport 集成)
├── xtquant_compat.py 客户端兼容层(xt_trader / xtdata + 异步回调)
├── exec_events.py 委托/成交/错误事件推送(Redis pubsub
├── quote_push_channel.py 全推行情推送通道(redis/zmq PUB/SUB
├── quote_subscription_manager.py 服务端全推订阅管理(引用计数 + 组合键去重)
├── whole_quote_session.py 客户端全推订阅会话(心跳 + 重启恢复)
├── full_tick_cache.py 全市场行情快照缓存(可选降载)
├── strategy.py 之类 策略骨架、风控、价格引擎等
bigqmt_no_redis/ 无 redis 版本(QMT 沙箱拒绝 import redis 时用)
│ ├── zmq_transport.py 自包含 ZMQ transport(内联编码,零 redis 依赖)
│ └── DRYRUN_no_redis.py 无 redis DRYRUN 入口
src/xtquant/ 可选 xtquant import shim
src/bigqmt_signal_trader_strategy.py 策略入口(init/handlebar/adjust + 启动诊断)
src/bigqmt_signal_trader_redis_rpc_runtime.py Redis RPC runtime 入口
src/BIGQMT_REDIS_DRYRUN.py QMT 编辑器加载入口(GBK)
src/BIGQMT_ZMQ_BACKTEST.py 独立 QMT 回测 ZMQ 入口(GBK
src/bigqmt_backtest/ 独立历史驱动、模拟撮合、ZMQ 协议与客户端
tests/bigqmt_signal_trader/ 单元测试(无 QMT 环境可跑)
tests/bigqmt_backtest/ 回测、确定性、隔离和 ZMQ 往返测试
qmt-trader/ AI 助手 Skill(大模型直接操作 QMT,见下文专节)
│ ├── SKILL.md skill 说明书(命令速查 + 工作流 + 安全须知)
│ ├── scripts/qmt.py 统一 CLI46 子命令 + rpc 兜底)
│ └── references/api_reference.md 完整 API 参考
docs/ 详细文档
test_all_apis.py 端到端 API 测试(发现生产问题)
bench_latency.py / bench_transports.py 延迟基准脚本
```
---
## 本地测试
```powershell
python -m pytest tests/bigqmt_signal_trader/ -q
```
当前覆盖 **199 个用例**(含传输层往返、Redis RPC、客户端兼容、持仓/行情/下单 handlers、异步回调、执行事件)。
### 端到端 API 测试(发现生产问题)
`test_all_apis.py` 是**端到端验证**测试——不只测「调用成功」,还测「结果正确」,能发现这些生产问题:
| 验证项 | 检测什么 | 为什么重要 |
|--------|---------|-----------|
| **客户端/服务端一致性** | ping 超时 → transport 不匹配 | Issue #24 根因:客户端 redis / 服务端 zmq 连不上 |
| **持仓查询** | `get_positions` 返回空但账户有持仓 | 容错设计把「失败返回空」当成「正常」 |
| **委托查询** | `query_orders` 返回空 | strategy_name 不匹配(默认应为 `""` 返回全部) |
| **买入/卖出** | `submit_order` 成功但委托没进系统 | 静默失败(passorder 被 QMT 拒绝但没报错) |
| **server_error** | 显示 QMT 端拒绝原因 | 委托被 QMT 静默拒绝时返回具体原因 |
**用法**
```powershell
# 方式 A:用环境变量
$env:BIGQMT_ACCOUNT_ID="你的账号"
$env:BIGQMT_REDIS_HOST="你的Redis地址"
$env:BIGQMT_REDIS_PORT="6379"
$env:BIGQMT_REDIS_DB="5"
$env:BIGQMT_REDIS_PASSWORD="你的密码"
python test_all_apis.py
# 方式 B:用 QMT 端配置(需 bigqmt_signal_trader_local_config.py 在 PYTHONPATH
$env:PYTHONPATH="D:\国金证券QMT交易端\python;$env:PYTHONPATH"
python test_all_apis.py
```
**示例输出**(发现问题时):
```
--- 端到端验证: 客户端/服务端一致性 ---
客户端配置 transport: redis
❌ ping 失败: redis rpc timeout: ping
可能原因: 客户端 transport 和服务端不匹配
- 客户端配置 transport=redis
- 如果服务端是 zmq, 客户端也要设 transport=zmq
--- 端到端验证: 持仓查询 ---
⚠️ get_positions 返回空 — 账户可能真的没持仓, 或查询失败 (检查 QMT 上下文)
--- 端到端验证: 买入/卖出 ---
✅ submit_order OK
❌ 委托没进系统 — submit_order 成功但 query_orders 找不到
这是静默失败 (passorder 被 QMT 拒绝但没报错)
检查: 1) 价格是否超出范围 2) 账户权限 3) QMT 风控
```
---
## 日志与排错(出错去哪看)
系统自带**文件日志**——所有报错/异常同时写 QMT 输出面板和本地日志文件,重启/崩溃后也能回溯。
### 日志位置
| 环境 | 日志文件 |
|------|---------|
| **QMT 内(服务端)** | `<QMT python 目录>\logs\bigqmt.log`(如 `D:\国金证券QMT交易端_lemo\python\logs\bigqmt.log`|
| **外部客户端** | `~\.cache\bigqmt\logs\bigqmt.log`(用户目录下)|
- **按天轮转**(午夜),**默认保留最近 7 天**。
- 每行带时间戳 + 级别 + 模块标签:`2026-08-14 21:45:59 [ERROR] [bigqmt.quote_push] publisher start failed: ...`
### 查看方式
```powershell
# 实时跟踪日志
Get-Content "D:\国金证券QMT交易端\lempython\logs\bigqmt.log" -Wait -Tail 50
# 只看错误
Get-Content "D:\...\python\logs\bigqmt.log" | Select-String "ERROR|WARN"
```
### 配置
| 环境变量 | 默认 | 说明 |
|---------|------|------|
| `BIGQMT_LOG_ENABLED` | `1` | 置 `0` 关闭文件日志 |
| `BIGQMT_LOG_TO_STDOUT` | `1` | 置 `0` 不输出到 QMT 面板 |
| `BIGQMT_LOG_RETENTION_DAYS` | `7` | 日志保留天数 |
> **排错首选看日志文件**:QMT 面板内容重启/清空后丢失,日志文件保留 7 天,包含启动诊断(`[bigqmt_diag]`)、崩溃原因、端口冲突等。
---
## 安全默认值
- `rpc_allow_order_methods` 默认 `False`:远程 `order_stock` / `cancel_order` 被拒绝。确认接入方、账号、风控后再显式开启。
- 回测桥接永久 `live_ready=false`,协议中没有真实账户和实盘下单方法。
- 配置文件含资金账号和密码,`bigqmt_signal_trader_local_config.py` / `bigqmt_signal_trader_client_config.py` 已在 `.gitignore`**不要提交**。
- 请求负载经过 base64 + 数字混淆编码(`encode_rpc_request_payload`),避免 QMT 的 Redis 客户端拦截含股票代码的明文。
---
## AI 助手 Skillqmt-trader(大模型直接操作 QMT
仓库内置一个 **Agent Skill**——[qmt-trader/](qmt-trader/),让支持 SKILL.md 约定的 AI 编程助手(Claude Code / ZCode / Cursor / Codex 等)**直接用命令行驱动 QMT 的全部交易与行情能力**,无需每次现场写 Python 调用代码。人也可以脱离 AI 手动执行其中的 CLI 脚本。
### 目录结构
```
qmt-trader/
├── SKILL.md skill 说明书(触发条件 + 命令速查 + 典型工作流 + 安全须知)
├── scripts/qmt.py 统一 CLI 入口(46 个子命令 + 通用 rpc 兜底,约 1000 行)
└── references/api_reference.md 完整 API 参考(参数/返回值/常量/已知陷阱)
```
### 工作原理
- AI 助手匹配到 `SKILL.md` 里的 `description`("查行情 / 查持仓 / 下单 / 龙虎榜 / 北向资金…时触发")后自动加载本 skill
- 之后助手调用 `python qmt-trader/scripts/qmt.py <子命令>` 执行**确定性命令**,不再临时生成 RPC 调用代码,避免参数写错;
- 所有命令默认输出 JSON`ok` / `data` / `ts` 三字段,便于模型解析),加 `--table` 切换人类可读表格;出错时返回 `ok: false` + `error` / `detail` / `code`,退出码 1
- `qmt.py` 自动把仓库 `src/` 加入 `sys.path`(开发模式免 pip install),并自动发现 QMT 的 python 目录读取客户端配置。
### 启用方式
**方式 A:安装到 AI 助手的 skills 目录**(推荐,全局生效):
```powershell
# Claude Code
cp -r qmt-trader ~/.claude/skills/qmt-trader
# ZCode / 其他遵循 agents skills 约定的助手
cp -r qmt-trader ~/.agents/skills/qmt-trader
```
安装后正常提需求即可,例如"帮我看下工商银行最近的走势""我账户现在什么持仓",助手会自动触发。
**方式 B:不安装,对话里显式指定**:
> 阅读 qmt-trader/SKILL.md,之后用里面的 qmt.py 命令帮我查行情 / 持仓 / 下单。
**方式 C:纯手动**(不经过 AI,人直接当 CLI 用):
```powershell
python qmt-trader/scripts/qmt.py ping
python qmt-trader/scripts/qmt.py snapshot --table
```
### 前置条件
与「快速开始」的客户端一致:
1. QMT 端 RPC 服务已启动(`BIGQMT_REDIS_DRYRUN.py` 运行中,输出面板/日志看到启动诊断 OK);
2. 客户端配置就绪——环境变量(`BIGQMT_ACCOUNT_ID` / `BIGQMT_REDIS_HOST` / `BIGQMT_REDIS_PORT` / `BIGQMT_REDIS_DB` / `BIGQMT_REDIS_PASSWORD`)或配置文件;
3. 先 `ping` 确认连通:redis 约 13ms / zmq 约 0.7ms 为正常,超时说明 transport 或配置不匹配。
### 一分钟上手
```powershell
# 0. 连通性检测(含延迟测量)
python qmt-trader/scripts/qmt.py ping
# 1. 账户全景:资产 + 持仓 + 委托 + 成交(一次往返)
python qmt-trader/scripts/qmt.py snapshot
# 2. 实时五档盘口(含涨跌幅)
python qmt-trader/scripts/qmt.py tick 600000.SH
# 3. 前复权日 K 60 根(含 MA5/20/60 统计)
python qmt-trader/scripts/qmt.py kline 600000.SH --period 1d --count 60 --dividend front
# 4. 干跑下单(只打印不提交,确认参数)
python qmt-trader/scripts/qmt.py buy 600000.SH 100 --price 7.50 --dry-run
```
### 命令概览
| 分类 | 命令 |
|------|------|
| **连通/全景** | `ping` / `snapshot` |
| **账户** | `account`(资产)/ `positions`(持仓含浮动盈亏)/ `orders`(委托含语义化状态)/ `trades`(成交) |
| **行情** | `tick` / `kline` / `instrument` / `sector` / `trading-dates` / `north`(北向)/ `longhubang`(龙虎榜)/ `financial`(财务)/ `download`(历史数据下载)/ `quote-subscribe`(全推订阅) |
| **扩展查询(25 个快捷命令)** | `holiday` / `stock-name` / `instrument-type` / `divid-factors` / `market-times` / `trading-calendar` / `option-list` / `bsm-price` / `bsm-iv` / `hkt-stats` / `hkt-details` / `hkt-rate` / `top10-holder` / `holder-num` / `ipo` / `ipo-limit` / `credit-assure` / `credit-short` / `credit-debt` / `his-st` / `index-weight` / `industry` / `sector-info` / `local-data` / `timetag2dt` / `dt2timetag` |
| **交易** | `buy` / `sell` / `cancel`(均支持 `--dry-run`buy/sell 支持 `--latest` / `--strategy` / `--remark` |
| **通用兜底** | `rpc <method> [json]` — 调用白名单内**任意**方法(如 `rpc get_l2_quote '{"stock_code":"600000.SH"}'`),未列出的方法都能这样调 |
### 安全设计
- 下单三命令(`buy` / `sell` / `cancel`)受服务端白名单控制,`rpc_allow_order_methods` 默认 `False`,未显式开启时返回 `ORDER_DISABLED`
- 下单前先用 `tick` 看价 + `--dry-run` 确认参数;
- 报 `ORDER_TIMEOUT` 时**不要直接重试**,先 `orders` 查询确认委托是否已进系统,避免重复下单;
- 下单的 `--strategy` 与查询的 `--strategy` 需一致;查全部委托用 `orders --strategy ""`(空 = 不过滤)。
完整命令表、四个典型工作流(行情分析 / 持仓监控 / 下单交易 / 批量分析)和 API 参数细节见 [qmt-trader/SKILL.md](qmt-trader/SKILL.md) 与 [qmt-trader/references/api_reference.md](qmt-trader/references/api_reference.md)。
---
## 相关文档
- [CHANGELOG.md](CHANGELOG.md) — **版本变更记录**(新增/修复/变更)
- [docs/RPC_API_REFERENCE.md](docs/RPC_API_REFERENCE.md) — **全部 RPC 方法参考**(参数、返回值、别名、大 QMT 能力边界)
- [docs/FORMULA_SERVER_FASTPATH.md](docs/FORMULA_SERVER_FASTPATH.md) — FormulaServer(58600) 直连快速路径:协议、映射表、能力边界与回退行为
- [docs/SUBSCRIBE_WHOLE_QUOTE_PUSH.md](docs/SUBSCRIBE_WHOLE_QUOTE_PUSH.md) — 全推行情订阅推送机制设计
- [docs/SUBSCRIBE_WHOLE_QUOTE_LIVE_VERIFICATION.md](docs/SUBSCRIBE_WHOLE_QUOTE_LIVE_VERIFICATION.md) — 全推行情实盘验证报告
- [docs/BIG_QMT_REDIS_RPC.md](docs/BIG_QMT_REDIS_RPC.md) — Redis RPC 协议与入口脚本详解
- [docs/RPC_TRANSPORTS.md](docs/RPC_TRANSPORTS.md) — 可插拔传输层完整说明
- [docs/XTQUANT_COMPAT_REPLACEMENT.md](docs/XTQUANT_COMPAT_REPLACEMENT.md) — 用兼容层替换旧 xtquant 的步骤
- [docs/BIG_QMT_SIGNAL_TRADER_RUNBOOK.md](docs/BIG_QMT_SIGNAL_TRADER_RUNBOOK.md) — 信号交易运行手册
- [docs/ZMQ_BACKTEST_BRIDGE.md](docs/ZMQ_BACKTEST_BRIDGE.md) — 独立 ZMQ 回测协议、撮合规则和 QMT 入口
- [qmt-trader/](qmt-trader/) — **QMT Trader skill**AI 助手统一 CLI 驱动全部 QMT API(46 子命令 + 通用 rpc 兜底),用法见上文「AI 助手 Skillqmt-trader」专节
---
## 为什么不直接连大 QMT
官方 `xtquant.xttrader.XtQuantTrader` 依赖客户端侧 XtQuantServer 通道。当前国金大 QMT 环境中直接连 `connect()` 返回 `-1`,**交易能力**因此必须放在大 QMT 内部策略进程里,外部通过 RPC 驱动。
**但只读行情不必走 RPC。** `58600` 是 FormulaServer,它同时就是行情/参考数据服务——QMT 自带 Python 里的 `qmt_api` 包(`bin.x64/Lib/site-packages/qmt_api`)正是它的客户端。本仓库已接入这条直连快速路径,见上文「FormulaServer 直连快速路径」。
如果后续券商开通 XtQuantServer 权限且 `connect()==0`,可再加交易直连模式。
@@ -0,0 +1,107 @@
# coding: utf-8
"""BigQMT Redis RPC latency benchmark.
Measures end-to-end latency for ping (no QMT API call) and get_full_tick
(real ContextInfo call) to separate transport cost from API cost.
"""
import os
import statistics
import time
import redis
from bigqmt_signal_trader.redis_rpc import call_redis_rpc
def _load_redis_config():
"""Pull connection details from the local client config or env vars.
Never hardcode secrets in the repo.
"""
try:
from bigqmt_signal_trader.xtquant_compat import load_client_config
cfg = load_client_config()
rc = dict(cfg.get("redis_config") or {})
rc.setdefault("host", os.environ.get("BIGQMT_REDIS_HOST", "127.0.0.1"))
rc.setdefault("port", int(os.environ.get("BIGQMT_REDIS_PORT", "6379")))
rc.setdefault("db", int(os.environ.get("BIGQMT_REDIS_DB", "5")))
return {
"host": rc.get("host"),
"port": int(rc.get("port")),
"db": int(rc.get("db")),
"username": rc.get("username") or None,
"password": rc.get("password") or None,
"socket_timeout": 8,
}
except Exception:
return {
"host": os.environ.get("BIGQMT_REDIS_HOST", "127.0.0.1"),
"port": int(os.environ.get("BIGQMT_REDIS_PORT", "6379")),
"db": int(os.environ.get("BIGQMT_REDIS_DB", "5")),
"socket_timeout": 8,
}
ACCOUNT = os.environ.get("BIGQMT_ACCOUNT_ID", "")
REDIS = _load_redis_config()
def bench(r, method, params, n=20, timeout=6):
lats = []
errors = 0
for i in range(n):
t0 = time.time()
try:
resp = call_redis_rpc(r, ACCOUNT, method, params, timeout_seconds=timeout)
dt = (time.time() - t0) * 1000
if resp.get("ok"):
lats.append(dt)
else:
errors += 1
if errors <= 2:
print(" %s #%d error: %s" % (method, i, resp.get("error", "")[:120]))
except Exception as e:
errors += 1
if errors <= 2:
print(" %s #%d exc: %s" % (method, i, e))
if not lats:
print("%-18s: ALL FAILED (%d errors)" % (method, errors))
return
lats.sort()
p50 = statistics.median(lats)
p95 = lats[int(len(lats) * 0.95)] if len(lats) >= 20 else lats[-1]
print(
"%-18s: n=%d ok=%d fail=%d min=%.0f p50=%.0f p95=%.0f max=%.0f avg=%.0f ms"
% (
method,
len(lats),
len(lats),
errors,
min(lats),
p50,
p95,
max(lats),
statistics.mean(lats),
)
)
def main():
r = redis.Redis(**REDIS)
# warmup
try:
call_redis_rpc(r, ACCOUNT, "ping", {}, timeout_seconds=6)
print("warmup ping ok\n")
except Exception as e:
print("warmup FAILED: %s\n" % e)
return
print("=== latency benchmark (20 calls each) ===")
bench(r, "ping", {}, n=20)
bench(r, "get_full_tick", {"codes": ["000001.SZ"]}, n=20)
bench(r, "get_full_tick", {"codes": ["000001.SZ", "600000.SH", "000333.SZ"]}, n=20)
bench(r, "get_instrument", {"code": "000001.SZ"}, n=20)
if __name__ == "__main__":
main()
@@ -0,0 +1,151 @@
# coding: utf-8
"""Compare end-to-end RPC latency across transports.
Runs the same ping workload through:
* Redis (real server, the production path) via call_redis_rpc
* ZMQ (local tcp loopback, the low-latency path) via ZmqTransport
Prints a side-by-side min/p50/p90/p99/max comparison. The ZMQ leg spins up a
local in-process server so no QMT process is needed for the comparison.
"""
import argparse
import os
import socket
import statistics
import time
import uuid
import redis
from bigqmt_signal_trader.redis_rpc import call_redis_rpc
from bigqmt_signal_trader.transports.zmq_transport import ZmqTransport
def _load_redis_config():
"""Pull connection details from the local client config or env vars."""
try:
from bigqmt_signal_trader.xtquant_compat import load_client_config
cfg = load_client_config()
rc = dict(cfg.get("redis_config") or {})
rc.setdefault("host", os.environ.get("BIGQMT_REDIS_HOST", "127.0.0.1"))
rc.setdefault("port", int(os.environ.get("BIGQMT_REDIS_PORT", "6379")))
rc.setdefault("db", int(os.environ.get("BIGQMT_REDIS_DB", "5")))
return {
"host": rc.get("host"),
"port": int(rc.get("port")),
"db": int(rc.get("db")),
"username": rc.get("username") or None,
"password": rc.get("password") or None,
"socket_timeout": 8,
}
except Exception:
return {
"host": os.environ.get("BIGQMT_REDIS_HOST", "127.0.0.1"),
"port": int(os.environ.get("BIGQMT_REDIS_PORT", "6379")),
"db": int(os.environ.get("BIGQMT_REDIS_DB", "5")),
"socket_timeout": 8,
}
REDIS = _load_redis_config()
ACCOUNT = os.environ.get("BIGQMT_ACCOUNT_ID", "")
def _free_port():
s = socket.socket()
s.bind(("127.0.0.1", 0))
port = s.getsockname()[1]
s.close()
return port
def _stats(name, lats):
lats = sorted(lats)
n = len(lats)
print(
"%-8s n=%d min=%.2f p50=%.2f p90=%.2f p99=%.2f max=%.2f avg=%.2f ms"
% (
name,
n,
min(lats),
statistics.median(lats),
lats[int(n * 0.9)],
lats[int(n * 0.99)] if n > 1 else lats[-1],
max(lats),
statistics.mean(lats),
)
)
def bench_redis(n):
r = redis.Redis(**REDIS)
# warmup + connectivity
try:
call_redis_rpc(r, ACCOUNT, "ping", {}, timeout_seconds=6)
except Exception as e:
print("Redis server not reachable, skipping redis leg: %s" % e)
return
lats = []
for _ in range(n):
t0 = time.time()
call_redis_rpc(r, ACCOUNT, "ping", {}, timeout_seconds=6)
lats.append((time.time() - t0) * 1000)
_stats("redis", lats)
def bench_zmq(n):
port = _free_port()
addr = "tcp://127.0.0.1:%d" % port
def on_req(req):
return {
"schema_version": 1,
"request_id": req["request_id"],
"account_id": "zmq",
"method": req["method"],
"ok": True,
"data": {"pong": True},
"error": "",
"handled_at": "now",
}
server = ZmqTransport(bind_address=addr, account_id="zmq", recv_timeout_seconds=0.3)
server.start_receiving(on_req, background_threads=True)
time.sleep(0.3)
client = ZmqTransport(connect_address=addr, account_id="zmq")
time.sleep(0.2)
lats = []
for _ in range(n):
req = {
"schema_version": 1,
"request_id": uuid.uuid4().hex,
"account_id": "zmq",
"method": "ping",
"params": {},
"reply_channel": "",
"reply_list": "",
"reply_key": "",
"ttl_seconds": 5,
}
t0 = time.time()
client.send_request(req, timeout_seconds=3.0)
lats.append((time.time() - t0) * 1000)
_stats("zmq", lats)
server.stop()
client.stop()
def main():
ap = argparse.ArgumentParser()
ap.add_argument("-n", "--count", type=int, default=100, help="requests per transport")
ap.add_argument("--skip-redis", action="store_true", help="skip the redis leg")
args = ap.parse_args()
print("=== transport latency comparison (n=%d each) ===" % args.count)
if not args.skip_redis:
bench_redis(args.count)
bench_zmq(args.count)
if __name__ == "__main__":
main()
@@ -0,0 +1,51 @@
# coding: utf-8
"""Measure ZMQ GIL-spike rate at different request rates.
Sends N get_full_tick requests at a fixed interval, records per-call
latency, reports how many exceed thresholds (50/200/500/1000ms).
"""
import sys
import time
sys.path.insert(0, r"D:\gjzqqmt\xtquant_big_convert\src")
sys.path.insert(0, r"D:\国金证券QMT交易端_lemo\python")
import bigqmt_signal_trader.xtquant_compat as compat
compat.configure()
client = compat.get_default_client()
print("account:", client.account_id, "| transport:", client.transport_name)
print("=" * 60)
N = 40
INTERVAL_MS = 50 # 请求间隔 50ms = 20 QPS
latencies = []
for i in range(N):
t0 = time.time()
try:
client.call("get_full_tick", {"codes": ["000001.SZ"]})
ms = (time.time() - t0) * 1000
latencies.append(ms)
except Exception as e:
ms = (time.time() - t0) * 1000
latencies.append(ms)
print(" [%2d] FAIL %.0fms %s" % (i, ms, str(e)[:40]))
# 控制频率
elapsed = (time.time() - t0)
sleep = max(0, INTERVAL_MS / 1000.0 - elapsed)
if sleep > 0:
time.sleep(sleep)
latencies.sort()
n = len(latencies)
print("\n=== %d requests @ %dms interval (%.0f QPS) ===" % (n, INTERVAL_MS, 1000.0/INTERVAL_MS))
print("min=%.1f p50=%.1f p90=%.1f p99=%.1f max=%.1f" % (
latencies[0], latencies[n//2], latencies[int(n*0.9)], latencies[int(n*0.99)], latencies[-1]))
# 尖峰分布
thresholds = [10, 50, 100, 200, 500, 1000]
print("\n=== 延迟分布 ===")
for t in thresholds:
cnt = sum(1 for l in latencies if l > t)
print(" >%5dms : %2d / %d (%.0f%%)" % (t, cnt, n, 100.0*cnt/n))
@@ -0,0 +1,252 @@
#coding:gbk
"""QMT bridge entry (no-redis version).
Same file-loader pattern as BIGQMT_REDIS_DRYRUN, but the RPC transport is ZMQ
only -- no redis imports anywhere. This version loads the no-redis zmq transport
(bigqmt_no_redis/zmq_transport.py) which inlines all encoding helpers and drops
redis-based service discovery, so it loads cleanly in QMT sandboxes that reject
`import redis` or any redis-named module.
Use this when your QMT environment cannot import the redis package (e.g. broker
whitelist blocks it) or when you want zero redis dependency.
Config: set "transport": "zmq" in bigqmt_signal_trader_local_config.py (the
no-redis runtime forces zmq regardless). Redis config fields are ignored.
"""
import builtins as _builtins
import importlib as _importlib
import os
import sys
import types
_LOCAL_ROOTS = (
"bigqmt_signal_trader",
"bigqmt_signal_trader_strategy",
"bigqmt_signal_trader_redis_rpc_runtime",
"bigqmt_signal_trader_local_config",
"bigqmt_no_redis",
)
_ORIGINAL_IMPORT = _builtins.__import__
_ORIGINAL_IMPORT_MODULE = _importlib.import_module
_ORIGINAL_RELOAD = _importlib.reload
def _known_qmt_python_dir():
# Find the QMT python dir from sys.path instead of a hardcoded path, so
# the bridge loads regardless of broker install location or launch mode.
for p in sys.path:
if p and r"\python" in p and os.path.isdir(p):
return p
return ""
try:
_SOURCE_ROOT = os.path.dirname(os.path.abspath(__file__))
except Exception:
_SOURCE_ROOT = _known_qmt_python_dir()
if not _SOURCE_ROOT:
_SOURCE_ROOT = _known_qmt_python_dir()
def _is_local_module(name):
return any(name == root or name.startswith(root + ".") for root in _LOCAL_ROOTS)
def _resolve_name(name, module_globals, level):
if not level:
return name
package = (module_globals or {}).get("__package__") or (module_globals or {}).get("__name__", "")
if not package:
raise ImportError("relative import without package")
for unused in range(level - 1):
if "." not in package:
raise ImportError("relative import beyond top-level package")
package = package.rsplit(".", 1)[0]
return package + ("." + name if name else "")
def _find_local_source(name):
relative = name.replace(".", os.sep)
dirs = []
if _SOURCE_ROOT:
dirs.append(_SOURCE_ROOT)
for p in sys.path:
if p and os.path.isdir(p) and p not in dirs:
dirs.append(p)
for d in dirs:
package_init = os.path.join(d, relative, "__init__.py")
if os.path.isfile(package_init):
return package_init, True
module_file = os.path.join(d, relative + ".py")
if os.path.isfile(module_file):
return module_file, False
raise ModuleNotFoundError("local source not found: %s" % name, name=name)
def _set_parent_attribute(name, module):
if "." not in name:
return
parent_name, child_name = name.rsplit(".", 1)
parent = _load_local_module(parent_name)
setattr(parent, child_name, module)
def _load_local_module(name):
existing = sys.modules.get(name)
if existing is not None:
return existing
source_path, is_package = _find_local_source(name)
if "." in name:
_load_local_module(name.rsplit(".", 1)[0])
module = types.ModuleType(name)
module.__file__ = source_path
module.__package__ = name if is_package else name.rpartition(".")[0]
if is_package:
module.__path__ = [os.path.dirname(source_path)]
module_builtins = dict(_builtins.__dict__)
module_builtins["__import__"] = _local_import
module.__dict__["__builtins__"] = module_builtins
module.__dict__["__bigqmt_load_local_module"] = _load_local_module
sys.modules[name] = module
if name == "bigqmt_signal_trader":
return module
try:
with open(source_path, "rb") as source_file:
source = source_file.read()
exec(compile(source, source_path, "exec"), module.__dict__)
except Exception:
sys.modules.pop(name, None)
raise
_set_parent_attribute(name, module)
return module
def _local_import(name, module_globals=None, module_locals=None, fromlist=(), level=0):
absolute_name = _resolve_name(name, module_globals, level)
if not _is_local_module(absolute_name):
return _ORIGINAL_IMPORT(name, module_globals, module_locals, fromlist, level)
module = _load_local_module(absolute_name)
for child in fromlist or ():
if child != "*":
try:
_load_local_module(absolute_name + "." + child)
except ModuleNotFoundError:
pass
if fromlist:
return module
return _load_local_module(absolute_name.split(".", 1)[0])
def _local_import_module(name, package=None):
if _is_local_module(name):
return _load_local_module(name)
return _ORIGINAL_IMPORT_MODULE(name, package)
def _local_reload(module):
if _is_local_module(getattr(module, "__name__", "")):
return _load_local_module(module.__name__)
return _ORIGINAL_RELOAD(module)
def _clear_local_modules():
for name in list(sys.modules):
if _is_local_module(name):
sys.modules.pop(name, None)
def _stop_previous_rpc_service():
"""Release the previous QMT strategy's socket before clearing its module."""
previous = sys.modules.get("bigqmt_signal_trader_strategy")
reset = getattr(previous, "reset_app", None)
if not callable(reset):
return
try:
reset()
print("[bigqmt_shell] previous rpc service stopped")
except Exception as exc:
print("[bigqmt_shell] previous rpc service stop failed: %s" % exc)
_stop_previous_rpc_service()
_clear_local_modules()
_importlib.import_module = _local_import_module
_importlib.reload = _local_reload
print("[bigqmt_shell] importlib entry source_root=%s" % _SOURCE_ROOT)
def _fallback_account_id():
for name in ("BIGQMT_ACCOUNT_ID", "account", "account_id", "accountID"):
value = globals().get(name)
if value:
return str(value)
return ""
try:
_local_import("bigqmt_signal_trader.adapters.market_bigqmt", globals(), fromlist=("*",))
_local_import("bigqmt_signal_trader.adapters.order_bigqmt", globals(), fromlist=("*",))
_local_import("bigqmt_signal_trader.adapters.position_bigqmt", globals(), fromlist=("*",))
_strategy = _local_import("bigqmt_signal_trader_strategy", globals(), fromlist=("*",))
_strategy.reset_app()
except Exception as bridge_preload_error:
print("[bigqmt_shell] bridge preload failed: %s" % bridge_preload_error)
_runtime = _local_import("bigqmt_signal_trader_redis_rpc_runtime", globals(), fromlist=("*",))
def _load_local_config():
return _local_import("bigqmt_signal_trader_local_config", globals(), fromlist=("*",))
try:
_config = _load_local_config()
BIGQMT_REDIS_CONFIG = getattr(_config, "BIGQMT_REDIS_CONFIG", {})
# Force zmq transport (this is the no-redis version).
BIGQMT_REDIS_CONFIG = dict(BIGQMT_REDIS_CONFIG or {})
BIGQMT_REDIS_CONFIG["transport"] = "zmq"
BIGQMT_REDIS_CONFIG["rpc_background_threads"] = True
print("[bigqmt_shell] no-redis mode: transport=zmq background_threads=True")
_runtime.configure_runtime_redis(BIGQMT_REDIS_CONFIG)
except Exception as redis_config_error:
print("[bigqmt_shell] local redis config load failed: %s" % redis_config_error)
try:
_config = _load_local_config()
BIGQMT_ACCOUNT_ID = getattr(_config, "BIGQMT_ACCOUNT_ID", "")
print("[bigqmt_shell] local account config loaded=%s" % bool(BIGQMT_ACCOUNT_ID))
_runtime.configure_runtime_account(BIGQMT_ACCOUNT_ID)
except Exception as account_config_error:
print("[bigqmt_shell] local account config load failed: %s" % account_config_error)
account_id = _fallback_account_id()
if account_id:
_runtime.configure_runtime_account(account_id)
try:
qmt_extra = {}
for function_name in (
"get_history_trade_detail_data", "get_value_by_order_id", "get_last_order_id",
"get_ipo_data", "get_new_purchase_limit", "get_assure_contract",
"get_enable_short_contract", "get_unclosed_compacts", "get_closed_compacts",
"get_debt_contract", "get_option_subject_position", "get_comb_option",
"get_hkt_exchange_rate", "down_history_data",
):
if function_name in globals():
qmt_extra[function_name] = globals()[function_name]
print("[bigqmt_shell] down_history_data bound=%s" % ("down_history_data" in qmt_extra))
_runtime.bind_runtime_api(
passorder_func=globals().get("passorder"),
cancel_func=globals().get("cancel"),
get_trade_detail_data_func=globals().get("get_trade_detail_data"),
extra_funcs=qmt_extra or None,
)
except NameError:
pass
init = _runtime.init
handlebar = _runtime.handlebar
adjust = _runtime.adjust
order_callback = _runtime.order_callback
deal_callback = _runtime.deal_callback
@@ -0,0 +1,459 @@
"""ZeroMQ transport for the BigQMT RPC bridge (no-redis version).
Same as bigqmt_signal_trader.transports.zmq_transport, but with the redis
dependencies inlined and the redis-based service discovery removed. This lets
the module load in QMT sandboxes that reject `import redis` or any module
whose name mentions redis.
Design (unchanged from the redis version):
* **Server** binds a ``ROUTER`` socket. Each inbound message arrives as
``[identity, payload]``; the server remembers ``identity`` keyed by
``request_id`` and replies with ``[identity, payload]`` so ZMQ routes the
response back to the originating client automatically.
* **Client** connects a ``DEALER`` socket (with a unique random identity), sends
``[payload]``, then ``poll``/``recv`` for the response.
"""
import base64
import json
import queue
import threading
import time
import uuid
# ---------------------------------------------------------------------------
# Inlined encoding helpers (originally from bigqmt_signal_trader.adapters.
# redis_common and bigqmt_signal_trader.redis_rpc). Kept here so this module
# has zero imports from any redis-named module.
# ---------------------------------------------------------------------------
SAFE_B64_PREFIX = "b64s:"
SAFE_B64_DIGIT_ENCODE = str.maketrans("0123456789", "!#$%&()*~?")
SAFE_B64_DIGIT_DECODE = str.maketrans("!#$%&()*~?", "0123456789")
def decode_text(value):
if isinstance(value, bytes):
return value.decode("utf-8")
return str(value)
def encode_rpc_request_payload(request):
"""Encode request JSON so patched QMT clients do not inspect stock-code text."""
raw = json.dumps(request, ensure_ascii=False).encode("utf-8")
encoded = base64.b64encode(raw).decode("ascii").translate(SAFE_B64_DIGIT_ENCODE)
return SAFE_B64_PREFIX + encoded
def decode_rpc_request_payload(text):
text = str(text)
if not text.startswith(SAFE_B64_PREFIX):
return text
encoded = text[len(SAFE_B64_PREFIX):].translate(SAFE_B64_DIGIT_DECODE)
return base64.b64decode(encoded.encode("ascii")).decode("utf-8")
# ---------------------------------------------------------------------------
# TransportError / TransportTimeout (inlined from transports.base -- kept here
# so this module is fully self-contained for QMT sandbox loading).
# ---------------------------------------------------------------------------
class TransportError(RuntimeError):
pass
class TransportTimeout(TransportError):
pass
class RpcTransport:
"""Minimal transport base (inlined subset of transports.base)."""
def __init__(self, account_id="", print_prefix="[bigqmt_rpc]"):
self.account_id = str(account_id or "")
self.print_prefix = str(print_prefix or "[bigqmt_rpc]")
self._on_request = None
self._running = False
def start_receiving(self, on_request):
self._on_request = on_request
self._running = True
def stop(self):
self._running = False
self._on_request = None
def deliver(self, request):
callback = self._on_request
if callback is None:
return None
try:
response = callback(request)
except Exception as exc:
import datetime as _dt
response = {
"schema_version": 1,
"request_id": str((request or {}).get("request_id") or ""),
"account_id": str((request or {}).get("account_id") or self.account_id or ""),
"method": str((request or {}).get("method") or ""),
"ok": False,
"data": None,
"error": "%s: %s" % (exc.__class__.__name__, exc),
"handled_at": _dt.datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
}
if response is not None:
try:
self.send_response(request, response)
except Exception:
pass
return response
# ---------------------------------------------------------------------------
# ZMQ transport
# ---------------------------------------------------------------------------
# ZMQ does not support ipc:// on Windows (it trips a signaler abort), so the
# default endpoint is tcp loopback. The port is derived from the account_id so
# distinct accounts don't collide on the same port; override via config when
# needed. Base 15560 keeps it clear of common dev ports.
DEFAULT_ZMQ_HOST = "127.0.0.1"
DEFAULT_ZMQ_BASE_PORT = 15560
DEFAULT_ZMQ_PORT_RANGE = 100 # derived port = base + (account_id_int mod range)
def _default_zmq_port(account_id):
"""Derive a stable port from account_id so each account gets its own socket."""
text = str(account_id or "")
digits = "".join(ch for ch in text if ch.isdigit())
try:
offset = int(digits) % DEFAULT_ZMQ_PORT_RANGE if digits else 0
except ValueError:
offset = 0
return DEFAULT_ZMQ_BASE_PORT + offset
def _default_zmq_address(account_id, host=None):
host = host or DEFAULT_ZMQ_HOST
return "tcp://%s:%d" % (host, _default_zmq_port(account_id))
def _loads(raw):
if isinstance(raw, dict):
return dict(raw)
text = decode_text(raw)
text = decode_rpc_request_payload(text)
return json.loads(text)
class ZmqTransport(RpcTransport):
"""ZMQ ROUTER/DEALER transport (no-redis version).
The same instance plays both roles depending on method called:
``send_request`` acts as a client (DEALER connect), ``start_receiving`` +
``send_response`` act as a server (ROUTER bind). A deployment normally uses
one instance per role (the QMT process is the server; the external client
is the client).
Unlike the redis version, this one does NOT use redis-based service
discovery. The server binds the configured address exactly; the client
connects to the configured or derived address directly.
"""
name = "zmq"
def __init__(
self,
bind_address=None,
connect_address=None,
host=None,
port=None,
account_id="",
print_prefix="[bigqmt_rpc]",
io_threads=1,
recv_timeout_seconds=1.0,
server_hwm=10000,
client_linger_ms=0,
):
super(ZmqTransport, self).__init__(account_id=account_id, print_prefix=print_prefix)
resolved_host = host or DEFAULT_ZMQ_HOST
if port is not None:
resolved_port = int(port)
else:
resolved_port = _default_zmq_port(account_id)
default_addr = "tcp://%s:%d" % (resolved_host, resolved_port)
self.bind_address = bind_address or default_addr
self.connect_address = connect_address
self.bind_host = resolved_host
self.base_port = resolved_port
self.io_threads = int(io_threads)
self.recv_timeout_seconds = float(recv_timeout_seconds)
self.server_hwm = int(server_hwm)
self.client_linger_ms = int(client_linger_ms)
self._zmq = None # imported lazily
self._ctx = None
# server state
self._router = None
self._router_thread = None
self._actual_bind_address = None # set after start_receiving()
self._pending_identities = {} # request_id -> client identity bytes
self._identity_lock = threading.Lock()
self._response_queue = queue.Queue()
self._queued_response_count = 0
self._sent_response_count = 0
# client state
self._dealer = None
self._client_lock = threading.Lock()
# -- construction helper ----------------------------------------------
@classmethod
def from_config(cls, config, account_id="", print_prefix="[bigqmt_rpc]"):
config = dict(config or {})
return cls(
bind_address=config.get("bind_address"),
connect_address=config.get("connect_address"),
host=config.get("host"),
port=config.get("port"),
account_id=config.get("account_id", account_id),
print_prefix=print_prefix,
io_threads=int(config.get("io_threads", 1)),
recv_timeout_seconds=float(config.get("recv_timeout_seconds", 1.0)),
server_hwm=int(config.get("server_hwm", 10000)),
client_linger_ms=int(config.get("client_linger_ms", 0)),
)
# -- shared zmq context -----------------------------------------------
def _ensure_zmq(self):
if self._zmq is None:
try:
import zmq # noqa: F401
except ImportError as exc: # pragma: no cover - depends on env
raise TransportError(
"pyzmq is required for the zmq transport: %s" % exc
)
self._zmq = zmq
if self._ctx is None:
self._ctx = self._zmq.Context.instance(self.io_threads)
return self._zmq, self._ctx
# -- server side ------------------------------------------------------
def _bind_configured_address(self):
"""Bind exactly one configured address and reject duplicate servers."""
zmq, ctx = self._ensure_zmq()
sock = ctx.socket(zmq.ROUTER)
sock.setsockopt(zmq.RCVHWM, self.server_hwm)
sock.setsockopt(zmq.SNDHWM, self.server_hwm)
sock.setsockopt(zmq.RCVTIMEO, int(self.recv_timeout_seconds * 1000))
try:
sock.bind(self.bind_address)
except self._zmq.ZMQError as exc:
try:
sock.close(linger=0)
except Exception:
pass
if getattr(exc, "errno", None) == zmq.EADDRINUSE:
raise TransportError(
"ZMQ_BIND_CONFLICT address=%s; another bridge instance "
"already owns the configured endpoint" % self.bind_address
)
raise
self._router = sock
self._actual_bind_address = self.bind_address
def start_receiving(self, on_request, background_threads=True):
super(ZmqTransport, self).start_receiving(on_request)
zmq, ctx = self._ensure_zmq()
self._bind_configured_address()
bound = self._actual_bind_address or self.bind_address
if not background_threads:
print(
"%s zmq bound=%s background_threads=False"
% (self.print_prefix, bound)
)
return
self._router_thread = threading.Thread(
target=self._router_loop, name="bigqmt-zmq-rpc", daemon=True
)
self._router_thread.start()
print(
"%s zmq started bound=%s" % (self.print_prefix, self.bind_address)
)
def _router_loop(self):
try:
while self._running:
self._drain_response_queue()
request = self._receive_request()
if request is not None:
self._deliver_request(request)
finally:
# Close the ROUTER socket on the thread that owns it. On Windows,
# closing a ZMQ socket from a different thread trips a signaler
# assertion (abort); closing it here is safe because this thread
# created and exclusively used it.
try:
self._router.close(linger=0)
except Exception:
pass
self._router = None
def _receive_request(self, flags=0):
try:
frames = self._router.recv_multipart(flags=flags)
except self._zmq.Again:
return None
except Exception as exc:
if self._running:
print("%s zmq recv failed: %s" % (self.print_prefix, exc))
if not flags:
time.sleep(0.5)
return None
if len(frames) < 2:
return None
identity, payload = frames[0], frames[-1]
try:
request = _loads(payload)
except Exception as exc:
print("%s zmq decode failed: %s" % (self.print_prefix, exc))
return None
request_id = str(request.get("request_id") or uuid.uuid4().hex)
with self._identity_lock:
self._pending_identities[request_id] = identity
return request
def _deliver_request(self, request):
started = time.perf_counter()
try:
self.deliver(request)
except Exception as exc:
print("%s zmq deliver failed: %s" % (self.print_prefix, exc))
elapsed_ms = (time.perf_counter() - started) * 1000.0
if elapsed_ms > 50.0:
print("%s zmq slow handler method=%s %.0fms"
% (self.print_prefix, request.get("method"), elapsed_ms))
def _drain_response_queue(self):
while True:
try:
identity, payload = self._response_queue.get_nowait()
except queue.Empty:
return
try:
self._router.send_multipart([identity, payload])
self._sent_response_count += 1
if self._sent_response_count <= 5:
print("%s zmq queued response sent" % self.print_prefix)
except Exception as exc:
print("%s zmq send failed: %s" % (self.print_prefix, exc))
def send_response(self, request, response):
if self._router is None:
raise TransportError("zmq server socket is not bound")
request_id = str(
response.get("request_id") or request.get("request_id") or ""
)
with self._identity_lock:
identity = self._pending_identities.pop(request_id, None)
if identity is None:
# No matching peer -- drop silently (client may have gone away).
return
payload = encode_rpc_request_payload(response).encode("utf-8")
if self._router_thread is not None and threading.current_thread() is not self._router_thread:
self._queued_response_count += 1
if self._queued_response_count <= 5:
print("%s zmq response queued for router thread" % self.print_prefix)
self._response_queue.put((identity, payload))
return
try:
self._router.send_multipart([identity, payload])
except Exception as exc:
print("%s zmq send failed: %s" % (self.print_prefix, exc))
def drain_request_queue(self, max_items=20):
"""Drain requests from the scheduled QMT thread when no receiver thread exists."""
if self._router_thread is not None or self._router is None:
return 0
processed = 0
for _index in range(max(int(max_items), 0)):
request = self._receive_request(flags=self._zmq.NOBLOCK)
if request is None:
break
self._deliver_request(request)
processed += 1
return processed
# -- client side ------------------------------------------------------
def _resolve_connect_address(self):
"""Resolve the address to connect to. No redis discovery -- use explicit
connect_address, else derive from account_id."""
if self.connect_address:
return self.connect_address
return _default_zmq_address(self.account_id)
def _ensure_dealer(self):
zmq, ctx = self._ensure_zmq()
if self._dealer is None:
address = self._resolve_connect_address()
sock = ctx.socket(zmq.DEALER)
# Unique identity so ROUTER can route replies back to us.
sock.setsockopt(zmq.IDENTITY, uuid.uuid4().hex.encode("utf-8")[:16])
sock.setsockopt(zmq.LINGER, self.client_linger_ms)
sock.connect(address)
self._dealer = sock
self.connect_address = address
return self._dealer
def send_request(self, request, timeout_seconds, **_kwargs):
zmq = self._zmq or self._ensure_zmq()[0]
with self._client_lock:
dealer = self._ensure_dealer()
request = dict(request)
request.setdefault("request_id", uuid.uuid4().hex)
request_id = request["request_id"]
payload = encode_rpc_request_payload(request)
try:
dealer.send(payload.encode("utf-8"))
except Exception as exc:
raise TransportError("zmq send failed: %s" % exc)
deadline = time.time() + float(timeout_seconds)
poller = self._zmq.Poller()
poller.register(dealer, self._zmq.POLLIN)
while True:
remaining = deadline - time.time()
if remaining <= 0:
break
events = dict(poller.poll(timeout=int(remaining * 1000)))
if dealer in events:
frames = dealer.recv_multipart()
raw = frames[-1]
response = _loads(raw)
if response.get("request_id") == request_id:
return response
raise TransportTimeout("zmq rpc timeout: %s" % request.get("method"))
# -- lifecycle --------------------------------------------------------
def stop(self):
super(ZmqTransport, self).stop()
# Clear _running so the router loop exits; the loop closes its own
# socket (closing cross-thread trips a Windows signaler abort).
thread = self._router_thread
if thread is not None and thread.is_alive():
thread.join(2.0)
if thread is None and self._router is not None:
try:
self._router.close(linger=0)
except Exception:
pass
self._router = None
self._router_thread = None
with self._client_lock:
if self._dealer is not None:
try:
self._dealer.close(linger=self.client_linger_ms)
except Exception:
pass
self._dealer = None
# Do NOT terminate the shared context -- other sockets/users may rely on it.
@@ -0,0 +1,409 @@
# 大 QMT Redis Queue RPC 说明
更新时间:2026-07-02
## 目标
在大 QMT 策略进程内启动一个 Redis RPC 服务,用来远程调用少量白名单方法。实盘默认使用 Redis list queue + QMT `run_time("adjust", ...)` 调度 drain;请求 payload 会做安全编码,避免大 QMT 内置 Redis 客户端读取包含股票代码的 JSON 时触发 `Sensitive Data Detected`
- `ping`
- `get_ticks`
- `get_instrument`
- `get_market_data` / `get_market_data_ex` / `get_local_data`
- `get_stock_list_in_sector` / `get_sector_list` / `get_sector_info`
- `get_divid_factors` / `download_history_data` / `download_history_data2`
- `get_trading_dates` / `get_holidays` / `download_holiday_data`
- `get_ipo_info` / `get_etf_info` / `get_option_list`
- `get_financial_data` / `download_financial_data`
- `call_formula` / `subscribe_formula` / `unsubscribe_formula` / `get_formula_result` / `gen_factor_index`
- `get_positions`
- `get_asset`
- `query_orders`
- `query_trades`
- `sync_positions`
下单类方法 `submit_order``cancel_order` 默认关闭,只有显式配置 `rpc_allow_order_methods=True` 后才会开放。
## MiniQMT 兼容方法名
RPC 服务端会把以下 MiniQMT 常用方法名映射到大 QMT 适配器:
| MiniQMT 方法名 | RPC 内部方法 | 说明 |
|---|---|---|
| `query_stock_asset` | `get_asset` | 查询账户资产 |
| `query_stock_positions` | `get_positions` | 查询全部持仓 |
| `query_stock_position` | `query_stock_position` | 查询单只持仓,按 `stock_code` 过滤 |
| `query_stock_orders` | `query_orders` | 查询委托;支持 `cancelable_only` 过滤 |
| `query_stock_trades` | `query_trades` | 查询成交 |
| `get_full_tick` | `get_ticks` | 默认直接 RPC 调用;可选开启 Redis 快照缓存降载 |
| `get_instrument_detail` / `get_instrumentdetail` | `get_instrument` | 查询合约详情 |
| `order_stock` / `order_stock_async` | `submit_order` | 买卖下单;默认关闭 |
| `cancel_order_stock` / `cancel_order_stock_sysid` | `cancel_order` | 撤单;默认关闭 |
`order_stock` 参数兼容 `stock_code``order_type``order_volume``price_type``price``strategy_name``order_remark`。其中 `order_type=23/STOCK_BUY` 映射为买入,`order_type=24/STOCK_SELL` 映射为卖出。
`price_type` 会透传到大 QMT `passorder()`,常用值包括 `11/FIX_PRICE``5/LATEST_PRICE``44/MARKET_PEER_PRICE_FIRST``43/MARKET_SH_CONVERT_5_LIMIT``47/MARKET_SZ_CONVERT_5_CANCEL`
`get_full_tick/get_ticks``codes` 参数支持两种写法:传合约代码如 `["600000.SH", "000001.SZ"]` 查询指定标的;传市场代码如 `["SH", "SZ"]` 查询全市场全推快照。
注意:兼容层的 `xtdata.get_full_tick(codes)` 默认走 Redis RPC 现调大 QMT。若需要降低全市场行情的大 payload 压力,可在客户端和 QMT 本地配置里显式打开 `full_tick_cache_enabled=True` / `BIGQMT_FULL_TICK_CACHE_CONFIG["enabled"]=True`,改为 Redis 需求驱动快照。
## 实现文件
- `src/bigqmt_signal_trader/redis_rpc.py`RPC 协议、Redis queue 服务、外部客户端 helper。
- `src/bigqmt_signal_trader/xtquant_compat.py`MiniQMT 风格客户端兼容层。
- `src/xtquant/`:可选的 `xtquant` import shim,用于最终替换老 import。
- `src/bigqmt_signal_trader_strategy.py`:在 `init` 中启动 RPC;默认由 QMT `run_time("adjust", ...)` drain Redis queue,避免大 QMT 冻结自建后台线程。
- `src/bigqmt_signal_trader_redis_rpc_runtime.py`:大 QMT 策略入口,默认不消费交易信号,只启用 RPC 和持仓同步。
- `tests/bigqmt_signal_trader/test_redis_rpc.py`RPC 单测。
## 运行方式
把源码同步到 QMT 的 `python` 目录:
```powershell
$srcPkg = '<REPO_ROOT>\src\bigqmt_signal_trader'
$dstPkg = '<QMT_PYTHON_DIR>\bigqmt_signal_trader'
Get-ChildItem -LiteralPath $srcPkg -Force | ForEach-Object {
Copy-Item -LiteralPath $_.FullName -Destination $dstPkg -Recurse -Force
}
Copy-Item -LiteralPath '<REPO_ROOT>\src\bigqmt_signal_trader_strategy.py' `
-Destination '<QMT_PYTHON_DIR>\bigqmt_signal_trader_strategy.py' `
-Force
Copy-Item -LiteralPath '<REPO_ROOT>\src\bigqmt_signal_trader_redis_rpc_runtime.py' `
-Destination '<QMT_PYTHON_DIR>\bigqmt_signal_trader_redis_rpc_runtime.py' `
-Force
```
QMT 本地私有配置文件:
```python
# <QMT_PYTHON_DIR>\bigqmt_signal_trader_local_config.py
# coding: utf-8
BIGQMT_ACCOUNT_ID = "你的资金账号"
BIGQMT_REDIS_CONFIG = {
"host": "YOUR_REDIS_HOST",
"port": 6379,
"db": 5,
"username": "",
"password": "...",
"rpc_allow_order_methods": False,
"rpc_process_in_listener": True,
"rpc_listener_methods": ("*",),
"rpc_background_threads": False,
"schedule_adjust": True,
"schedule_adjust_interval": "500nMilliSecond",
"full_tick_cache_enabled": False,
"full_tick_demand_ttl_seconds": 10,
"full_tick_cache_ttl_seconds": 10,
"full_tick_refresh_interval_seconds": 3,
"full_tick_max_requests": 8,
}
```
这个文件含账号和 Redis 密码,只放 QMT 本地目录,不提交。
QMT 策略编辑器内容:
```python
#coding:gbk
import sys
import os
import importlib
_qmt_path = os.path.dirname(os.path.abspath(globals().get('__file__', '')))
if not _qmt_path:
_qmt_path = 'D:/YOUR_QMT_PYTHON_DIR'
if _qmt_path not in sys.path:
sys.path.insert(0, _qmt_path)
try:
import bigqmt_signal_trader.redis_rpc as _redis_rpc
_redis_rpc = importlib.reload(_redis_rpc)
except Exception:
pass
try:
import bigqmt_signal_trader_strategy as _strategy
try:
_strategy.reset_app()
except Exception:
pass
_strategy = importlib.reload(_strategy)
except Exception:
pass
import bigqmt_signal_trader_redis_rpc_runtime as _runtime
_runtime = importlib.reload(_runtime)
try:
from bigqmt_signal_trader_local_config import BIGQMT_REDIS_CONFIG
_runtime.configure_runtime_redis(BIGQMT_REDIS_CONFIG)
except Exception:
pass
try:
from bigqmt_signal_trader_local_config import BIGQMT_ACCOUNT_ID
_runtime.configure_runtime_account(BIGQMT_ACCOUNT_ID)
except Exception:
pass
try:
_runtime.bind_runtime_api(
passorder_func=passorder,
cancel_func=cancel,
get_trade_detail_data_func=get_trade_detail_data,
)
except NameError:
pass
init = _runtime.init
handlebar = _runtime.handlebar
adjust = _runtime.adjust
order_callback = _runtime.order_callback
deal_callback = _runtime.deal_callback
```
不要勾选“启动本地 python”。
## Redis 协议
### RPC 请求/响应
请求 channel
```text
bigqmt:rpc:req:{account_id}
```
请求 payload
```json
{
"schema_version": 1,
"request_id": "req-001",
"account_id": "YOUR_ACCOUNT_ID",
"method": "get_positions",
"params": {},
"reply_channel": "bigqmt:rpc:resp:YOUR_ACCOUNT_ID:req-001",
"reply_key": "bigqmt:rpc:resp:YOUR_ACCOUNT_ID:req-001",
"ttl_seconds": 60
}
```
响应会同时写入:
```text
bigqmt:rpc:resp:{account_id}:{request_id}
```
并 publish 到同名 channel。
响应格式:
```json
{
"schema_version": 1,
"request_id": "req-001",
"account_id": "YOUR_ACCOUNT_ID",
"method": "get_positions",
"ok": true,
"data": {},
"error": "",
"handled_at": "2026-07-01 10:30:00"
}
```
### 可选:get_full_tick 需求驱动缓存
默认情况下,`xtdata.get_full_tick(codes)` 直接走 RPC。只有显式打开 `full_tick_cache_enabled=True` / `BIGQMT_FULL_TICK_CACHE_CONFIG["enabled"]=True` 时,客户端才会写入需求:
```text
bigqmt:full_tick:demand:{account_id}
```
其中 hash field 是规范化代码集合的 request idvalue 包含:
```json
{
"request_id": "...",
"codes": ["SH", "SZ"],
"requested_at_ts": 1780000000.0,
"expires_at_ts": 1780000010.0,
"cache_ttl_seconds": 10
}
```
大 QMT 每轮刷新后写入快照:
```text
bigqmt:full_tick:cache:{account_id}:{request_id}
```
快照 Redis key 的 TTL 默认是 10 秒;客户端还会校验 `updated_at_ts`,超过 `cache_ttl_seconds` 的快照不会返回。第一次调用如果还没有快照,客户端默认最多等待 `3.5s` 等下一轮大 QMT 刷新;**个股列表**仍然没有新快照时回退一次 live RPC(`get_full_tick`)以避免冷启动硬停;**市场代码**(`SH/SZ/BJ/HK`)则抛出超时、不回退 live 拉全市场。
### 异步下载任务(download jobs
`download_history_data` / `download_history_data2` 是耗时的长调用:如果走同步 RPC,服务端会在**策略线程**上一直下载,冻结整个 RPC pump(且客户端 6s 就超时崩)。因此这两个方法改为**异步分块任务**:客户端把任务写入 Redis 队列立即返回,大 QMT 的策略线程每个 tick 只下载 `download_job_chunk_size` 只(受 `download_job_max_wall_seconds` 墙钟预算约束),永不长时间阻塞。
Redis 布局(按账户):
```text
bigqmt:download:queue:{account_id} # 待处理 job_id 列表(RPUSH/LPOP
bigqmt:download:job:{account_id}:{job_id} # job JSON(含 state/done/total/error 进度)
bigqmt:download:current:{account_id} # 当前正在处理的 job_id(串行,一次一个)
```
job 状态:`pending → running → done | failed`。历史 K 线下载到**大 QMT 机器**的本地库;客户端随后用 `get_local_data` / `get_market_data` 快读取回。
客户端用法:
```python
# 非阻塞:提交后轮询
job = xtdata.submit_download_history_data2(["600000.SH", "000001.SZ"], "1d")
status = xtdata.get_download_status(job["job_id"]) # {state, done, total, error}
status = xtdata.wait_download(job["job_id"]) # 阻塞轮询到 done/failed(仅客户端阻塞)
# 兼容:download_history_data2(...) 仍可直接调用 = 提交 + 等待(默认最多 1800s);
# 超时会抛 TimeoutError(任务在服务端继续跑,可继续轮询)。大批量建议用 submit + 轮询。
```
服务端开关:`download_jobs_enabled``download_job_chunk_size`(默认 10,每 tick 最小下载块)、`download_job_max_wall_seconds`(默认 0.5s,每 tick 墙钟预算)、`download_job_ttl_seconds`(默认 3600)。
### 实时成交/委托回调推送(exec events
大 QMT 的 `order_callback(ContextInfo, orderInfo)` / `deal_callback(ContextInfo, dealInfo)` 在策略进程内触发。服务端把 QMT 对象的 ThinkTrader `m_*` 字段规范化后 publish 到 Redis,客户端后台线程订阅并回调 —— 无需轮询即可**实时**拿到成交/委托。
Redis 频道(同名 streamxadd + publish,供短时回放):
```text
bigqmt:order_events:{account_id}
bigqmt:trade_events:{account_id}
```
成交事件字段(由 `deal_callback``m_*` 映射):`stock_code`(`m_strInstrumentID`)、`trade_id`(`m_strTradeID`)、`order_sys_id`(`m_strOrderSysID`)、`volume`(`m_nVolume`)、`price`(`m_dPrice`)、`amount`(`m_dTradeAmount`)、`commission`(`m_dComssion`)、`direction`(`m_nDirection`) 及 `action`(尽力映射 BUY/SELL)、`traded_at`(`m_strTradeTime`)。委托事件类似(`m_nOrderStatus``status``m_nVolumeTotal``order_volume``m_nVolumeTraded``traded_volume``m_dLimitPrice``price`)。
客户端用法(MiniQMT 风格,回调实时触发):
```python
class MyCallback(XtQuantTraderCallback):
def on_stock_trade(self, trade): # 成交实时回调
print(trade.stock_code, trade.trade_id, trade.traded_volume, trade.traded_price)
def on_stock_order(self, order): # 委托状态实时回调
print(order.stock_code, order.order_status, order.traded_volume)
xt_trader.register_callback(MyCallback())
xt_trader.start() # 启动后台监听线程(订阅上面两个频道)
xt_trader.subscribe(acc) # 账号确定后会自动重订阅到该账号频道
```
服务端开关:`exec_events_enabled`(默认 True)。`action``m_nDirection` 尽力映射(48/23→BUY49/24→SELL),未知时为空但 `direction` 原值始终保留。
## 外部调用示例
```python
import sys
import redis
sys.path.insert(0, r"<REPO_ROOT>\src")
from bigqmt_signal_trader.redis_rpc import call_redis_rpc
r = redis.Redis(
host="YOUR_REDIS_HOST",
port=6379,
db=5,
username="",
password="...",
)
response = call_redis_rpc(
r,
account_id="YOUR_ACCOUNT_ID",
method="get_positions",
params={},
timeout_seconds=3,
)
print(response)
```
## 延迟模式
### 两档处理模型(重要)
同一进程只有一个 GIL,方法按处理线程分两档:
- **inline 档(后台接收线程直接处理)**:`ping`、行情类(`get_full_tick`/`get_market_data_ex`/
`get_instrument_detail`)、`query_stock_asset`。中位数**亚毫秒**,但会撞上大 QMT 终端占 GIL
的尾延迟(见下)。
- **deferred 档(推迟到主策略线程,经 adjust drain)**:所有走 `get_trade_detail_data` 的**交易
查询**——持仓/委托/成交、信用/账户明细,以及下单/撤单。**原因**:`get_trade_detail_data` 在后台
线程上返回空(账户实有持仓也查出 0),必须在 QMT 主线程上下文里跑。这些方法登记在
`LISTENER_DEFERRED_METHODS`,由 `run_time("adjust", interval)` 每拍 `drain_pending()` 在主
线程执行。(`get_asset` 例外,走另一个 QMT 调用,后台线程即可,保持 inline 低延迟。)
> `adjust` 不是 QMT 内置回调。QMT 只自动调 `init`/`handlebar`;`handlebar` 里 `return
> adjust(...)`,加上我们 `run_time("adjust", interval)` 注册的定时器,构成 RPC 队列的 drain 节奏。
### 尾延迟 = 大 QMT 终端占 GIL(不是本代码)
`gil_probe` 探针显示进程周期性被卡 ~490ms,但 `adjust_phase` 每段都 <50ms —— 即**尾延迟来自
QMT 终端自身的 C++ 主循环占着 GIL**,`setswitchinterval`/精简 adjust 都 preempt 不了。唯一根治
是把 serving 挪出该进程(sidecar 独立 GIL,见 `shm_transport.py` 预留)。
### schedule_adjust_interval 调这个数压尾延迟
`run_time` 间隔 = 后台线程拿到主线程 GIL 窗口的节奏源;间隔越小,inline 尾越低:
| interval | adjust 频率 | inline 尾(p90/max) | CPU | 说明 |
|---|---|---|---|---|
| `500nMilliSecond` | ~2.4/s | ~490 / 510ms | 极低 | 默认省电 |
| `200nMilliSecond` | 折中 | ~200ms 量级 | 中 | **推荐平衡点** |
| `100nMilliSecond` | ~2150/s(QMT 当"尽快跑"热循环) | ~92 / 108ms | 烧≈1 核 | 尾最低但费 CPU |
**deferred 交易查询恒定 ~1s**(实测 p50 1012~1013ms,与 interval 无关)—— 瓶颈是
`get_trade_detail_data` 自身的柜台查询开销,调 interval 无效。要低延迟拿持仓,走**客户端 redis
缓存**(position_sync 已在写)而非每次实时查。
### zmq 真机实测(同机 localhost)
- inline:`ping` p50 **0.4-0.5ms**;`get_market_data_ex` 因 handler 较重几乎必吃满一个尾窗口
(500ms 档 p50≈495ms,100ms 档 p50≈96ms)。
- deferred:持仓/委托/成交 p50 **~1s**。
- 下单/撤单已在**实盘**验证:`order_stock` 挂单(status=50 已报)→ `query_stock_orders` 拿到
sysid → `cancel_order_stock` 成功、无残留。
### 传输与后台线程
- **redis**(默认,跨机):`rpc_process_in_listener=True`
- **zmq**(同机低延迟):只加 `transport="zmq"` 一行;非 redis 传输 `_build_rpc_service` 会自动开
`background_threads`,端口按账号派生 `tcp://127.0.0.1:1556x`
- 内置 Redis 客户端读取含股票代码的原始 JSON 会触发 `Sensitive Data Detected`;客户端 helper 默认
对请求做安全编码。
## 安全约束
- 默认生产模式不在自建线程里调用 QMT API;QMT API 调用在 `adjust/handlebar` 中处理。
- 默认只读,远程下单关闭。
- 账号不匹配会拒绝请求。
- 响应写 Redis key 并设置 TTL,方便调用端超时后排查。
## 本地测试
```powershell
cd <REPO_ROOT>
python -B -m unittest discover -s tests\bigqmt_signal_trader
```
当前结果:
```text
Ran 68 tests
OK
```
@@ -0,0 +1,360 @@
# 大 QMT 信号下单包运行手册
更新时间:2026-07-01
## 1. 当前结论
这个包已经具备大 QMT 运行入口和 QMT 适配层:
- `bigqmt_signal_trader_strategy.py`:大 QMT 策略入口,响应 `init``handlebar/adjust`、委托回调、成交回调。
- `BigQmtMarketDataProvider`:封装 `ContextInfo.get_full_tick()``ContextInfo.get_instrumentdetail()`
- `BigQmtPositionProvider`:封装 `get_trade_detail_data(account, 'STOCK', 'POSITION')`
- `BigQmtOrderGateway`:按 `qmt_jq_trade` 的参数形状调用 `passorder()`
- 默认模式仍是 `dryrun`,不会真实发委托。
截至 2026-07-01 凌晨,已完成:
- 本地单元测试:`32 tests OK`
- QMT `python` 目录导入测试通过。
- 模拟 `init/adjust/sync_positions` 回调通过。
- 模拟 `mode="bigqmt"` + fake `passorder` 参数测试通过。
- Redis Stream 信号源、Redis 状态写回、Redis 持仓同步已实现。
- 本机 Redis `127.0.0.1:6379 db=5` dry-run 集成测试通过。
还没有完成:
- 没有在 QMT 页面里通过“模型交易”做真实实盘委托验证。
- 没有把 miniQMT 的真实买卖指令切成只写 Redis 信号。
- 没有在真实账号上启用大 QMT `passorder` 实盘执行。
所以今天开盘可以先验证“大 QMT 是否能持续加载和触发回调”,以及 Redis dry-run 链路是否能消费测试信号并写回状态。如果要真的由大 QMT 替代 miniQMT 下单,必须先完成 miniQMT 只写信号、单账户灰度和实盘风控确认。
## 1.1 当前 QMT 页面检查结果
2026-07-01 08:00 左右检查大 QMT“模型交易”页面:
- 页面里当前运行/展示的策略名称是“网格策略”。
- 没有看到 `bigqmt_signal_trader``bigqmt_signal_trader_redis_dryrun`
- “策略日志”页没有 `[bigqmt_signal_trader] init ok``[bigqmt_signal_trader] adjust ok`
结论:当前这个文件还没有真正挂到大 QMT 模型交易里运行。
## 2. 官方文档里的关键点
### 2.1 编辑器运行不等于真实交易
大 QMT 编辑器里的“运行/回测/模型运行”主要用于公式、模型、信号验证。要真正把委托发送到交易柜台,需要进入“模型交易”页面,把策略加入模型交易实例并绑定资金账号。
结论:
- 编辑器“运行”:适合检查 `init/handlebar` 是否报错,不用于确认真实下单。
- 模型交易“模拟信号”:适合开盘先观察回调、信号、价格、状态,不真实下单。
- 模型交易“实盘交易”:只有确认信号源、幂等、风控都 OK 后才能打开。
### 2.2 不要勾选“启动本地 python”
官方文档说明,“启动本地 python”是把脚本作为独立 Python 进程运行。这个模式不会按大 QMT 回调机制触发 `init(ContextInfo)``handlebar(ContextInfo)`
本包是回调式策略入口,必须让 QMT 自己调用:
- 不勾选:`init``handlebar``order_callback``deal_callback` 正常触发。
- 勾选:脚本只会像普通 Python 文件一样执行 import,通常会马上结束,不会进入交易回调。
### 2.3 必须跳过历史 bar
QMT 加载策略时可能先跑历史 K 线,再进入最后一根实时 bar。入口已经加了保护:
```python
if hasattr(ContextInfo, "is_last_bar") and not ContextInfo.is_last_bar():
return None
```
这可以避免未来接入真实信号源后,在历史回放阶段误消费当前待处理信号。
## 3. 文件部署
大 QMT 运行目录:
```text
<QMT_PYTHON_DIR>
```
源代码目录:
```text
<REPO_ROOT>\src
```
部署命令:
```powershell
Copy-Item -Path '<REPO_ROOT>\src\bigqmt_signal_trader\*' `
-Destination '<QMT_PYTHON_DIR>\bigqmt_signal_trader' `
-Recurse -Force
Copy-Item -LiteralPath '<REPO_ROOT>\src\bigqmt_signal_trader_strategy.py' `
-Destination '<QMT_PYTHON_DIR>\bigqmt_signal_trader_strategy.py' `
-Force
Copy-Item -LiteralPath '<REPO_ROOT>\src\bigqmt_signal_trader_dryrun.py' `
-Destination '<QMT_PYTHON_DIR>\bigqmt_signal_trader_dryrun.py' `
-Force
Copy-Item -LiteralPath '<REPO_ROOT>\src\bigqmt_signal_trader_redis_dryrun.py' `
-Destination '<QMT_PYTHON_DIR>\bigqmt_signal_trader_redis_dryrun.py' `
-Force
```
清理缓存:
```powershell
Get-ChildItem -LiteralPath '<QMT_PYTHON_DIR>\bigqmt_signal_trader' `
-Recurse -Filter '__pycache__' -Directory -ErrorAction SilentlyContinue |
Remove-Item -Recurse -Force
Get-ChildItem -LiteralPath '<QMT_PYTHON_DIR>' `
-Filter '__pycache__' -Directory -ErrorAction SilentlyContinue |
Remove-Item -Recurse -Force
```
## 4. QMT 编辑器加载测试
目的:只验证 QMT 能加载入口、能触发 `init/adjust`,不会真实下单。
### 4.1 策略文件内容
在 QMT 策略编辑器中新建一个策略,例如 `大QMT信号下单_dryrun`,内容使用下面这段。脚本必须保持 ASCII,避免 QMT 编辑器编码问题。
```python
#coding:gbk
from bigqmt_signal_trader_strategy import (
adjust,
configure,
deal_callback,
handlebar,
init,
order_callback,
set_account_id,
sync_positions,
)
try:
ACCOUNT_ID = account
except NameError:
ACCOUNT_ID = ""
if ACCOUNT_ID:
set_account_id(ACCOUNT_ID)
configure(mode="dryrun", account_id=ACCOUNT_ID or "dryrun")
```
### 4.2 QMT 页面设置
在策略编辑器右侧/基本信息里:
- 运行周期:建议先选 `1分钟``3分钟`
- 标的:建议先用流动性稳定的指数或股票,例如 `000300.SH`
- 启动本地 python:不要勾选。
- 自动交易/实盘交易:不要在编辑器测试阶段打开。
点击顺序:
1. 保存。
2. 编译。
3. 运行。
期望输出:
```text
[bigqmt_signal_trader] init ok
[bigqmt_signal_trader] adjust ok
```
如果只看到“开始运行/结束运行”,但没有 `init ok/adjust ok`
- 检查是否勾选了“启动本地 python”。
- 检查是否真的导入了 `bigqmt_signal_trader_strategy.py`
- 检查 QMT 输出窗或 `XtClient_Formula_YYYYMMDD.log` 是否有 traceback。
## 4.3 Redis dry-run 入口
已经新增安全观察入口:
```text
<QMT_PYTHON_DIR>\bigqmt_signal_trader_redis_dryrun.py
```
默认配置:
```text
ACCOUNT_ID = bigqmt_probe
Redis = 127.0.0.1:6379 db=5
Stream = bigqmt:signals:bigqmt_probe
Status = bigqmt:signal_status:bigqmt_probe:{signal_id}
Position = bigqmt:positions:bigqmt_probe
OrderGateway = DryRunOrderGateway
```
如果 QMT 的 `python` 目录存在本地私有配置文件,则 Redis 连接会被覆盖:
```text
<QMT_PYTHON_DIR>\bigqmt_signal_trader_local_config.py
```
格式:
```python
# coding: utf-8
BIGQMT_REDIS_CONFIG = {
"host": "YOUR_REDIS_HOST",
"port": 6379,
"db": 5,
"username": "",
"password": "...",
}
```
这个文件含 Redis 密码,只放 QMT 本地目录,不提交到源码仓库,也不要贴进文档。
这个入口只用于开盘观察 Redis 链路,不会真实下单,也不会消费真实账号流。不要把 `ACCOUNT_ID` 改成真实资金账号,除非你明确知道 dry-run 会 ack 掉该账号的 Redis 信号。
写入一条测试信号:
```powershell
cd <REPO_ROOT>
python -B -c "import sys,datetime,json,redis; sys.path.insert(0,'src'); from bigqmt_signal_trader.adapters.signal_redis import push_trade_signal; r=redis.Redis(host='127.0.0.1',port=6379,db=5); push_trade_signal(r, {'signal_id':'probe-001','account_id':'bigqmt_probe','action':'BUY','stock_code':'600000.SH','amount':100,'price_type':'FIX_PRICE','price':10.0,'created_at':'2026-07-01 09:31:00','expire_at':'2026-07-01 23:59:00','schema_version':1})"
```
运行后检查状态:
```powershell
python -B -c "import redis; r=redis.Redis(host='127.0.0.1',port=6379,db=5,decode_responses=True); print(r.hgetall('bigqmt:signal_status:bigqmt_probe:probe-001'))"
```
期望状态里出现:
```text
status = DRY_RUN
user_order_id = dryrun:bq:...
```
## 5. 模型交易页面运行
目的:开盘后观察策略在真实行情驱动下是否持续触发,而不是只在编辑器里跑一次。
### 5.1 第一步只跑模拟信号
进入 QMT 的“模型交易”页面:
1. 新建模型交易实例。
2. 选择上面的策略文件。
3. 绑定资金账号。
4. 标的使用 `000300.SH` 或其他稳定标的。
5. 周期先用 `1分钟`
6. 运行方式先选择“模拟信号”或等价的非实盘模式。
7. 确认“启动本地 python”没有勾选。
8. 启动模型交易。
开盘后观察:
- 输出窗是否出现 `[bigqmt_signal_trader] init ok`
- 第一根实时 bar 后是否出现 `[bigqmt_signal_trader] adjust ok`
- 日志里是否没有 `Traceback``ModuleNotFoundError``run script failed`
- 委托页面不应该出现真实委托,因为当前是 dry-run 且空信号源。
如果要验证 Redis dry-run 链路,则选择 `bigqmt_signal_trader_redis_dryrun.py`,并向 `bigqmt:signals:bigqmt_probe` 写入测试信号。它只会写 Redis 状态,不会真实委托。
### 5.2 真实大 QMT adapter 连通模式
如果只想确认大 QMT adapter 可以装配,但仍然没有真实信号源,可以把策略最后一行改成:
```python
configure(mode="bigqmt", account_id=ACCOUNT_ID or "dryrun")
```
注意:当前没有配置真实 `SignalSource`,所以即使是 `mode="bigqmt"`,也不会产生订单。它只会装配行情、持仓、委托 adapter,用于确认 QMT 环境里这些函数可用。
### 5.3 真正实盘委托前置条件
只有满足下面全部条件,才能考虑切到实盘交易:
- 已在 Redis db5 上验证 Redis Stream 信号源、`StateStore.claim()`、状态回写都正常。
- 已用模拟信号验证 `passorder` 参数、持仓查询、撤单查询全部正常。
- 已确认历史 bar 不会触发下单。
- 已确认 miniQMT 不再对同一账户重复真实下单,避免双系统抢单。
- 已确认 dry-run 没有 ack 掉真实账号待实盘处理的信号。
未满足这些条件时,不要切到“实盘交易”。
## 6. 今日开盘观察清单
日期:2026-07-01
### 9:10 前
- 确认 QMT 已登录。
- 确认文件已部署到 `<QMT_PYTHON_DIR>`
- 在策略编辑器里编译成功。
- 确认“启动本地 python”未勾选。
- 在模型交易页面用“模拟信号”启动 `bigqmt_signal_trader_dryrun.py``bigqmt_signal_trader_redis_dryrun.py`
### 9:30 到 9:35
- 看输出窗是否出现 `init ok``adjust ok`
-`XtClient_Formula_20260701.log` 是否有 traceback。
- 看策略是否持续运行,没有自动结束。
- 看委托页面确认没有真实委托。
- 如果跑 Redis dry-run,向 `bigqmt:signals:bigqmt_probe` 写一条测试信号,确认状态 key 变成 `DRY_RUN`
### 9:35 后
如果模拟信号稳定:
- 可以把标的周期从 `1分钟` 调整到实际希望的触发周期。
- 继续保持 dry-run 观察一段时间。
- 不要直接改成实盘,除非真实信号源和状态存储已经接好。
## 7. 日志排查
QMT 公式日志:
```text
<QMT_USERDATA_LOG_DIR>\XtClient_Formula_YYYYMMDD.log
```
重点搜索:
```text
bigqmt_signal_trader
Traceback
ModuleNotFoundError
SyntaxError
run script failed
passorder
```
常见问题:
| 现象 | 原因 | 处理 |
|---|---|---|
| 只开始运行/结束运行,没有回调日志 | 勾选了启动本地 python,或没有进入模型交易回调模式 | 取消勾选,使用模型交易运行 |
| `No module named dataclasses` | QMT 内置 Python 版本低,不能依赖 dataclasses | 当前代码已移除 dataclasses,重新部署并清 `__pycache__` |
| `__file__ is not defined` | QMT 编辑器脚本没有 `__file__` | 策略入口不要依赖 `__file__` |
| 编码错误 | 编辑器保存编码和 `coding` 声明不一致 | 入口脚本用 `#coding:gbk`,内容保持 ASCII |
| 启动后处理很多历史 bar | 未过滤历史 K 线 | 当前 `adjust` 已用 `is_last_bar()` 保护 |
## 8. 当前不能误解的点
- 当前包不是 `qmt_jq_trade` 的目标持仓同步脚本。
- 当前包的设计是“外部系统产出逐笔交易信号,大 QMT 只执行”。
- Redis db5 链路已经具备,但当前安全入口默认使用 `bigqmt_probe` 测试账号流。
- 编辑器里点“运行”不是实盘验证。
- 真正实盘必须走模型交易页面,并且要显式接入信号源、幂等状态和账户切换流程。
## 9. 官方文档参考
- ThinkTrader 大 QMT 接口文档:`https://dict.thinktrader.net/innerApi/interface_operation.html`
- 大 QMT Python API 文档:`https://qmt.ptradeapi.com/QMT_Python_API_Doc.html`
- 本项目参考脚本:`<REPO_ROOT>\src\api\qmt_jq_trade`
@@ -0,0 +1,111 @@
# Big QMT 执行回调重复触发修复记录
日期:2026-08-12
## 现象
实盘下单后,客户端收到的委托回报和成交回报各触发两次,例如:
```text
委托回报: 159518 50 635042239
委托回报: 159518 50 635042239
成交回报: 159518 635042239 100 1.204
成交回报: 159518 635042239 100 1.204
```
同一笔委托的同一状态、同一笔成交被重复推送到客户端 callback。
## 原因
服务端策略脚本同时暴露了两套执行回调入口:
- `on_order` / `on_trade`
- `order_callback` / `deal_callback`
其中 `order_callback()` 内部又调用 `on_order()``deal_callback()` 内部又调用 `on_trade()`
如果 Big QMT 运行时同时识别并触发这两套入口,同一个原始委托/成交事件会进入服务端两次。每次都会执行:
1. 归一化 QMT 回调对象;
2. 发布 Redis 执行事件;
3. 转发给本地 app runner。
客户端订阅执行事件频道后,就会看到同一条委托回报/成交回报各触发两次。
## 修复方案
只保留 Big QMT 标准回调入口:
- `order_callback(ContextInfo, orderInfo)`
- `deal_callback(ContextInfo, dealInfo)`
删除服务端策略入口中的别名回调:
- `on_order`
- `on_trade`
`order_callback``deal_callback` 现在直接完成原来别名函数里的工作:
- `_publish_exec_event("order", orderInfo)`
- `_publish_exec_event("trade", dealInfo)`
- `forward_order_event(...)`
- `forward_trade_event(...)`
这样 Big QMT 运行时只会看到一套执行回调入口,不需要依赖客户端或服务端去重。
## 修改范围
- `src/bigqmt_signal_trader_strategy.py`
- 删除 `on_order` / `on_trade`
- `order_callback` / `deal_callback` 直接发布和转发事件
- `src/bigqmt_signal_trader_redis_rpc_runtime.py`
- 不再导入或导出 `on_order` / `on_trade`
- `src/bigqmt_signal_trader_dryrun.py`
- 不再导入 `on_order` / `on_trade`
- `src/bigqmt_signal_trader_redis_dryrun.py`
- 不再导入 `on_order` / `on_trade`
- `tests/bigqmt_signal_trader/test_runner.py`
- 删除别名回调测试
- 新增断言:策略模块不暴露 `on_order` / `on_trade`,只暴露 `order_callback` / `deal_callback`
- `src/bigqmt_signal_trader/README.md`
- 更新策略入口说明
- `docs/BIG_QMT_SIGNAL_TRADER_RUNBOOK.md`
- 更新 QMT 策略导入示例
## 验证
相关测试:
```powershell
.\.venv\Scripts\python.exe -m pytest tests\bigqmt_signal_trader\test_runner.py tests\bigqmt_signal_trader\test_exec_events.py -q
```
结果:
```text
38 passed
```
全量测试:
```powershell
.\.venv\Scripts\python.exe -m pytest -q
```
结果:
```text
275 passed, 4 skipped
```
## 注意
这次修复只处理“同一执行事件回调两次”的问题。
`order_stock()` 同步返回 `-1`,但随后又收到真实委托/成交回报,是另一类问题:同步下单路径没有及时拿到 `order_sys_id`,而异步执行事件稍后能拿到真实系统委托号。该问题不在本次修复范围内。
@@ -0,0 +1,168 @@
# FormulaServer 直连快速路径(58600
## 这是什么
大 QMT 的 `58600` 端口是 **FormulaServer** —— QMT 内置的 C++ 行情/参考数据服务。端口取自
QMT 安装目录的 `config/formulaserver/formulaserver.ini`
```ini
[server_formula]
address = 0.0.0.0:58600
```
QMT 自带 Python 里就有它的官方客户端:`bin.x64/Lib/site-packages/qmt_api`。协议是
**BSON over TCP**,帧格式在 `qmt_api/net/RPCBase.py`
```
| packLen(uint32 BE) | seq(uint32 BE) | cmd(uint16 BE) | tag(uint16 BE) | BSON body |
```
`cmd = 3``NET_CMD_RPC`),body 是 `{"func": <名字>, "params": {...}}`
响应 `{"status": 0, "params": {...}}``status != 0``params` 里带 `ErrorID`/`ErrorMsg`
`tag & 7` 标记 zlib 压缩,`tag >> 8` 的低 4 位是 seq 的高位。
## 为什么值得接
原来所有只读请求都要绕一整圈:
```
客户端 → redis/zmq → QMT python 策略线程 → ContextInfo → 原路返回
```
这不只是慢,还要和策略自己抢 QMT 主线程的 GIL —— zmq 传输约 30% 的请求会撞上 ~500ms 的
调度尖峰,就是这么来的。
直连 FormulaServer 完全绕开策略进程:
| 路径 | p50 |
|------|-----|
| redis RPC | ~13ms |
| zmq RPC | ~0.7ms30% 撞 500ms GIL 尖峰)|
| **FormulaServer 直连** | **0.07ms**,无 GIL 竞争 |
穿过完整客户端栈(`BigQmtRpcClient.call`)实测 **0.145ms/次**,比 redis 快约 90 倍。
## 能力边界
**FormulaServer 只有行情/参考数据。** 实测所有账户/交易类方法一律返回
`ErrorID 200005 未找到该服务`
```
getAsset / getPositions / getAccountDetail / passorder -> 200005
getFullTick / getQuote -> 200005
```
所以它是**只读快速路径,不是 RPC 桥的替代品**。交易、账户查询、持仓、委托、成交、
五档盘口全部仍然走 RPC。
### 已接入的方法(10 个)
| 我们的方法 | FormulaServer func |
|---|---|
| `get_instrument` / `get_instrument_detail` / `get_instrumentdetail` | `getInstrumentDetail` |
| `get_last_volume` | `getLastVolume` |
| `get_total_share` | `getTotalShare` |
| `get_contract_multiplier` | `getContractMultiplier` |
| `get_main_contract` | `getMainContract` |
| `get_weight_in_index` | `getWeightInIndex` |
| `get_stock_list_in_sector` | `getStockListInSector` |
| `get_market_data_ex` | `getMarketData` |
### 刻意不接的方法,以及原因
宁可慢,不能悄悄给错数据。以下几项参数语义与我们的调用方不一致:
- **`get_trading_dates`** —— FormulaServer 要的是**股票代码**。实测:
```
{'stockCode': 'SH', ...} -> {'result': []} # 静默空
{'stockCode': '000001.SZ', ...} -> ['20260630', '20260701', ...]
```
而 `market_bigqmt.get_trading_dates(market, ...)` 的调用方传的是市场代码。传错了不报错、
只给空列表,交易日历错了后果太重。
- **`get_divid_factors`** —— 我们是 `(stock_code, start_time, end_time)` 区间,
FormulaServer 是 `(stockCode, date)` 单日。
- **`get_risk_free_rate`** —— 我们传 `index=-1`FormulaServer 要 `timetag`。语义不同。
- **复权 K 线** —— 实测 `dividendType` 传 `none` 和 `front` 返回**完全相同**的价格,
说明复权没有生效。因此只有 `dividend_type="none"`(或空)才走直连,其他复权类型直接
判为 unroutable 回退 RPC。否则策略要前复权、拿到的却是不复权价格,且毫无提示。
### 字段名坑
FormulaServer 的 `getInstrumentDetail` 返回 **`FloatVolumn` / `TotalVolumn`**(官方拼写错误),
而原生 xtdata SDK 用的是 `FloatVolume` / `TotalVolume`。下游代码按 SDK 拼写读,直接透传会
静默读到 `None`。所以 `_instrument_result` 做了别名归一化,两种拼写都保留。
### 尚未验证
`qmt_api/api.py` 的 `getMarketData` 支持 `fields=['quoter']`,注释说会返回
`askPrice/askVol/bidPrice/bidVol`level1 五档 / level2 十档)。**如果这在盘中可用,
`get_full_tick` 也能走直连** —— 这是热路径,收益很大。
但收盘时段实测返回空,无法确认。需要**盘中**再测一次:
```python
c.request('getMarketData', {'fields': ['quoter'], 'stockCodes': ['000001.SZ'],
'startTime': '', 'endTime': '', 'period': 'tick',
'dividendType': 'none', 'count': -1})
```
在确认之前不要接 —— 没验证就上映射,正是订单方向判定踩过的坑。
## 失败行为
**任何失败都自动回退 RPC**,所以连不上 58600 的客户端行为与改动前完全一致:
| 情况 | 行为 |
|---|---|
| 方法不在映射表 | `supports()` 返回 False,直接走 RPC |
| 参数 translate 不了 | `Unroutable`,走 RPC,**不**触发熔断(这是单次调用的问题) |
| 服务连不上 / IO 失败 | `Unroutable` + 熔断 `failure_cooldown_seconds`(默认 30s),期间全部走 RPC |
| 服务端回 `200005` | 该方法永久标记 unimplemented,只停这一个方法,不影响其他 |
| socket 断了(QMT 重启) | 自动重连重试一次 |
## 依赖
**不需要装任何东西。** BSON 编解码内置了无依赖实现;如果环境里有 pymongo 的 `bson`
或 QMT 的 `xtquant.xtbson`,会优先用(更快、更久经考验)。两条路径的输出实测逐字节一致,
测试里有对拍用例。
## 配置
客户端侧,默认开启,通常不用写:
```python
BIGQMT_FORMULA_SERVER_CONFIG = {
"enabled": True, # 或环境变量 BIGQMT_FORMULA_ENABLED=0 关闭
# "host": "127.0.0.1", # 绑的是 0.0.0.0,跨机可达(需放行防火墙)
# "port": 58600, # 不写则从 qmt_root 的 ini 读,再退回 58600
# "qmt_root": r"D:\国金证券QMT交易端",
# "timeout_seconds": 3.0,
# "methods": ["get_instrument"], # 只路由白名单
# "failure_cooldown_seconds": 30.0,
}
```
也可以写在 `BIGQMT_REDIS_CONFIG["formula_server"]` 里,后者优先级更高。
## 排查
```python
client._formula_router().stats()
# {'enabled': True, 'hits': 202, 'misses': 0, 'available': True,
# 'unimplemented': [], 'methods': [...]}
```
启动时会打一行:
```
[bigqmt_formula] active at 127.0.0.1:58600 (10 methods routed direct)
```
熔断时:
```
[bigqmt_formula] unavailable, falling back to RPC for 30s: connect 127.0.0.1:58600 failed: ...
```
@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2026 Listolany
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
@@ -0,0 +1,90 @@
# MiniQMT → 普通QMT内置python 策略转换 Skill
把基于 **miniQMT / xtquant**`XtQuantTrader` / `xtdata`)的 Python 量化策略,转换为**大QMT内置Python**`passorder` / `ContextInfo` 体系)可实盘运行的策略。
> 本 Skill 配合 Cursor AI Agent 使用,覆盖从可行性评估到上线部署的完整流程,含自动化工具脚本和真实转换对照示例。
---
## 文件结构
```
SKILL.md 主工作流(7步转换流程)
faq.md 买方共性疑虑 FAQ(外部数据/运行频率/同步下单/回测等8问)
api_mapping.md 全量 API / 字段 / 枚举映射表
constraints.md 限制清单、不可转场景判定与替代方案(含实测验证记录)
examples.md 真实策略(700行 demo)转换前后对照
scripts/
analyze_strategy.py 第1步:静态分析,输出可行性报告
check_converted.py 第5步:转换后合规校验(py3.6/GBK/框架结构)
to_gbk.py 最终交付:UTF-8 → GBK 安全转存
templates/
template_timer.py 定时器型骨架(apscheduler / while+sleep 策略首选)
template_bar.py 行情驱动型骨架(K线/订阅回调策略)
```
---
## 快速上手
```bash
# 1. 分析原策略(得到可行性报告 + 推荐模板)
python scripts/analyze_strategy.py 你的策略.py
# 2. 以推荐模板为骨架手动填充转换后代码
# 3. 校验转换结果(必须全部 PASS)
python scripts/check_converted.py 转换后策略.py
# 4. GBK 落盘(大QMT内置端要求)
python scripts/to_gbk.py 转换后策略.py 输出_gbk.py
```
---
## 在 智能体工具ClaudeCode/Cursor/OpenClaw/Hermes/Workbuddy 中使用
在对话里 `@MiniQMT2bigQMT_Skill`(将本目录放入 `.cursor/skills/` 或个人 skill 目录),Agent 会自动按 7 步流程完成转换并输出报告。
---
## 适用范围
| 可转换 | 处理方式 |
|---|---|
| apscheduler / while+sleep 定时调度 | → `C.run_time` 定时器 |
| `order_stock_async` 异步下单 | → `passorder`(11参) + userOrderId 状态机 |
| `query_stock_*` 查询接口 | → `get_trade_detail_data` + `m_` 前缀字段 |
| `xtdata.*` 行情接口 | → `C.get_full_tick` / `C.get_market_data_ex` 等 |
| 委托回调类 `on_stock_order` 等 | → 模块级 `order_callback` / `deal_callback` 等 |
| 不可直接转(给替代方案) | 建议 |
|---|---|
| 多账户/跨券商统一调度 | 文件桥方案(见 constraints.md D节) |
| 重型 ML 依赖 / py3.6 装不了的库 | 模型外置,内置端只读信号执行 |
| 7x24 守护 / 盘后批处理 | 保留外部计划任务喂文件给内置策略 |
---
## 常见疑虑速答(详见 faq.md)
- **外部数据还能取吗?** 能。内置端是完整 py3.6 非沙箱:本地文件通道(推荐)/ 直接网络请求(低频+超时)/ QMT 自身数据接口(比 mini 的 xtdata 更全)三选一。
- **是不是最短一分钟跑一次?** 不是。`run_time` 支持毫秒级间隔且与 K 线周期无关,1 秒循环已实盘长期验证;`handlebar` 本身逐 tick 触发。
- **同步下单没了怎么办?** 用 userOrderId 状态机等价改写(模板内置),实测下单后约 1 秒可查回委托号。
- **能回测吗?** K线型策略可直接回测(mini 反而没有回测框架);定时器型策略需把信号逻辑双入口挂载(回测挂 handlebar,详见 faq.md Q4)。
---
## 关键实测结论(已用券商模拟环境双端交叉验证)
- xtconstant 常量与内置枚举数值一致(15项全部核实)
- `xc.CREDIT_BUY/CREDIT_SELL` 实际值 = 23/24 → **信用账户转换必须显式改 33/34**
- `m_strRemark`userOrderId)只在下单客户端可见,跨客户端对账只能凭 sysid
- `passorder``m_strRemark` 命中 + sysid 回传 + `cancel(sysid)` 链路均实弹验证通过
- 资金/仓位字段两端精确一致;市值字段各自行情快照,仅供展示
---
## License
MIT
@@ -0,0 +1,141 @@
---
name: miniqmt-to-bigqmt
description: 将基于 miniQMT 的外部 xtquant 库(xttrader/xtdata)的 Python 量化策略,转换为大QMT内置PythonContextInfo/passorder 体系)可实盘运行的策略。当用户要求转换 miniQMT 策略、迁移 xtquant 代码到大QMT、或提到"内置Python/XTData/passorder 改写"时使用。包含可行性评估、API 映射、py3.6+GBK 约束校验、部署与实盘验证全流程;不可转换场景给出文件桥等替代方案。
---
# MiniQMT 策略 → 大QMT内置Python 转换
把外部 xtquant 策略(自带 Python 进程 + `XtQuantTrader`/`xtdata`)改写为在大QMT客户端内运行的内置策略(`init`/`handlebar`/`run_time` + `passorder`)。**目标是"真正能实盘",不是语法翻译**——两套体系的运行模型、账户绑定、下单返回值、数据时效都不同,必须按本流程逐项处理。
## 两套体系的本质差异(先建立心智模型)
| 维度 | miniQMT 外接 | 大QMT 内置 |
|---|---|---|
| 进程 | 自己的 Python 进程,pip 任装 | 客户端内嵌 **Python 3.6**,库受限(券商可能有白名单) |
| 编码 | UTF-8 | **GBK**(首行必须 `#coding:gbk` |
| 入口 | `if __name__ == '__main__'` 自由编排 | 框架回调:`init(C)``after_init(C)``handlebar(C)`/定时器 |
| 线程 | 随意多线程/apscheduler | **所有策略共用一个线程,禁止阻塞**(sleep/死循环/锁会卡死全部策略) |
| 账户 | 代码里 `StockAccount(id, type)`,可多账户 | 界面选定,注入全局变量 `account`/`accountType`,一个策略实例绑一个账户 |
| 下单 | `order_stock_async` 返回 seq,回报回调对账 | `passorder` **无返回值**,靠 `userOrderId`(投资备注,对应 `m_strRemark`)追踪 |
| 查询 | `query_stock_asset/orders/positions` 返回无前缀字段对象 | `get_trade_detail_data` 返回 **`m_` 前缀字段**对象(`m_nVolume` 等) |
| 行情 | `xtdata.*`(连 miniQMT 行情进程) | `C.get_full_tick`/`C.get_market_data_ex` 等(客户端行情) |
| 启停 | 自己守护、AutoLogin 重启 QMT | 随客户端启停;客户端设置里配自动登录/策略自启 |
## 转换工作流
复制此清单跟踪进度:
```
- [ ] 第1步 静态分析与可行性评估
- [ ] 第2步 选择目标结构模板
- [ ] 第3步 逐 API 映射改写
- [ ] 第4步 处理订单追踪与状态机
- [ ] 第5步 py3.6/GBK 合规校验
- [ ] 第6步 输出转换报告
- [ ] 第7步 部署与实盘验证指引
```
### 第1步 静态分析与可行性评估
用户若对内置端能力存疑(外部数据还能不能取、运行频率是否受限、能否回测、多策略会不会互相拖累等),先用 [faq.md](faq.md) 对齐认知再开工——这些多为误解,不要让错误前提影响转换方案。
运行分析脚本,得到 API 清单、py3.6 语法违例、第三方依赖、阻塞模式等:
```bash
python scripts/analyze_strategy.py <原策略.py>
```
按报告对照 [constraints.md](constraints.md) 分类每个发现项:
- **可直接映射** → 第3步处理
- **需重构**apscheduler/多线程/while-sleep 主循环、回报回调对账等)→ 按模板重组
- **不可转换**(多账户单进程、重型第三方库、7x24 外部守护等)→ 在转换报告中给出 constraints.md 对应的替代方案(文件桥/外接极简模式/拆分策略),**不要硬转**
任何一项"不可转换"都不代表整个策略失败——逐项给方案,能转的部分照常转。
### 第2步 选择目标结构模板
| 原策略形态 | 模板 |
|---|---|
| 定时轮询型:apscheduler / while+sleep / 定点任务(绝大多数 miniQMT 策略) | [templates/template_timer.py](templates/template_timer.py) |
| 行情驱动型:`xtdata.subscribe_quote` 回调驱动 / 单标的 K线信号 | [templates/template_bar.py](templates/template_bar.py) |
模板已含:GBK 头、全局状态类 `G`**禁止把可变状态存 ContextInfo**,有逐K线回滚机制)、`C.set_account(account)`(启用交易回调)、定时器注册、委托状态字典对账骨架、收盘自动停止逻辑。在模板骨架上填充策略逻辑,不要从零写。
### 第3步 逐 API 映射改写
对照 [api_mapping.md](api_mapping.md) 完成全部调用替换。高频映射速查(完整表必须查文件):
| miniQMT | 大QMT内置 |
|---|---|
| `xt_trader.order_stock_async(acc, code, xtconstant.STOCK_BUY, vol, xtconstant.FIX_PRICE, price, strat, remark)` | `passorder(23, 1101, account, code, 11, price, vol, strat, 2, userOrderId, C)` |
| `xt_trader.cancel_order_stock_async(acc, order_id)` | `cancel(sysid, account, accountType, C)`(注意:用**委托号 m_strOrderSysID**,不是内部 order_id |
| `xt_trader.query_stock_positions(acc)` | `get_trade_detail_data(account, accountType, 'position')` |
| `xt_trader.query_stock_asset(acc)` | `get_trade_detail_data(account, accountType, 'account')` |
| `xt_trader.query_stock_orders(acc)` | `get_trade_detail_data(account, accountType, 'order')` |
| `xtdata.get_full_tick(codes)` | `C.get_full_tick(codes)`(字段名同:lastPrice/askPrice/bidPrice... |
| `xtdata.get_instrument_detail(code)` | `C.get_instrument_detail(code)`(字段名同:UpStopPrice/PreClose... |
| `xtdata.get_market_data_ex(...)` | `C.get_market_data_ex(...)`(参数几乎同构) |
| `xtdata.get_trading_dates('SH', s, e)` | `C.get_trading_dates('000001.SH', s, e, count, '1d')`**仅 after_init 后可用**;返回 `'20240101'` 字符串列表,不是时间戳) |
| `xtdata.download_history_data(code, period, s, e)` | `download_history_data(code, period, s, e)`(全局函数) |
| `xtdata.subscribe_quote(code, period, callback=f)` | `C.subscribe_quote(code, period, callback=f)`(在 init 里注册) |
| 回调类 `on_stock_order/on_stock_trade/on_order_error` | 模块级函数 `order_callback(C, o)` / `deal_callback(C, d)` / `orderError_callback(C, args, msg)`,须先 `C.set_account(account)` |
| apscheduler / while+sleep | `C.run_time("函数名", "3nSecond", "2025-01-01 09:30:00")``C.schedule_run(...)` |
| `xtconstant.STOCK_BUY/STOCK_SELL` | opType `23/24`;两融担保品 `33/34`、融资买入 `27`、卖券还款 `31` 等查映射表 |
改写时的硬规则:
1. 定时器/回调/after_init 里下单,`quickTrade` 必须传 `2`,否则会漏单。
2. 查询字段全部换 `m_` 前缀名(对照 api_mapping.md 的字段映射表)。买卖方向判断用 `m_nOffsetFlag`48=买 49=卖)或 `m_nOpType`**不要照搬 order_type==23 的写法去比对 `m_nOrderPriceType`**。
3. 删除 `XtQuantTrader` 连接管理、`AutoLogin`、重启 QMT 的代码——内置端没有"连接"概念,断线由客户端处理。
4. 删除 `time.sleep` 等待类写法。需要"等委托回报再行动"的逻辑改为:本轮记录待办 → 下一轮定时器回调检查(见模板的 pending 字典模式)。
5. `print` 输出到客户端策略日志面板,保留即可;写文件日志可用,但路径用绝对路径。
### 第4步 处理订单追踪与状态机
这是最容易出错的环节。`passorder` 无返回值且客户端缓存有 50ms~6s 延迟,照搬"下单→立刻查"必然漏单/超单:
1. 每笔委托生成唯一 `userOrderId`(如 `f"策略名_{日期}_{序号}"`),下单后存入全局 `G.pending[userOrderId] = {...}`,状态置"待报"。
2.`order_callback`(实时推送)或定时器轮询 `get_trade_detail_data(..., 'order')`,按 `o.m_strRemark` 匹配回 `userOrderId`,更新状态/记录 `m_strOrderSysID`
3. 撤单用记录到的 `m_strOrderSysID``cancel`
4. 同一标的存在"待报"状态委托时禁止再下单(防超单)。
5. 委托状态码与 miniQMT 同一套数值(48已报/49部成/50已报待撤…53部撤/54已撤/56已成/57废单),判活集合 `(48,49,50,51,52,55,86,255)` 可沿用。
### 第5步 py3.6/GBK 合规校验
```bash
python scripts/check_converted.py <转换后策略.py>
```
脚本会拦截:≥3.7 语法(walrus、f-string `=`、dataclasses、asyncio.run、match 等)、残留 xtquant import、threading/multiprocessing、`time.sleep``input()`、缺 `#coding:gbk` 头、缺 `init``passorder` 参数个数错误、GBK 不可编码字符。**必须全部 PASS 才算转换完成**;每修一处重跑。
最后转存为 GBK 编码(编辑器直接改写 GBK 文件易出乱码,务必用脚本):
```bash
python scripts/to_gbk.py <转换后策略.py> <输出.py>
```
### 第6步 输出转换报告
向用户输出报告,包含:
- 已映射 API 清单(原调用 → 新调用)
- 重构点说明(调度器改造、订单追踪改造等)
- **不可转换项及替代方案**(引用 constraints.md 具体章节)
- 行为差异警示:行情时效(内置为客户端行情,无 VIP 时订阅数量受限)、`get_trade_detail_data` 是本地缓存非柜台实查、策略随客户端启停
### 第7步 部署与实盘验证指引
指导用户按以下步骤上线(细节见 [constraints.md](constraints.md) 部署章节):
1. 大QMT → 新建Python策略 → 粘贴转换后代码(确认编辑器显示中文注释无乱码)→ 保存编译
2. 策略交易/模型交易界面 → 新建 → 选本策略 + 资金账号(普通=STOCK/两融=CREDIT)+ 任意周期(定时器型策略选日线最省资源)→ 运行模式先选**模拟信号**
3. 模拟信号模式观察 1 个交易时段:信号面板的下单时机/数量/价格与预期一致
4. 切**实盘交易**模式,先用最小单量 + 不易成交价(买跌停价/卖涨停价附近)下 1-2 笔并撤掉,验证报/撤链路;报撤测试安排在交易时段或收盘后半小时内(实测 17 点后柜台不处理撤单)
5. 客户端设置勾选:自动登录、终端启动后策略自动运行;Windows 计划任务配开机启动客户端
## 参考文件
- [faq.md](faq.md) — 买方共性疑虑(外部数据/运行频率/同步下单/回测/性能预算),转换前对齐认知用
- [api_mapping.md](api_mapping.md) — 全量函数/字段/枚举映射表
- [constraints.md](constraints.md) — 限制清单、不可转场景判定与替代方案、部署细节
- [examples.md](examples.md) — 真实策略(apscheduler+xtquant 700行)转换前后对照
- [templates/template_timer.py](templates/template_timer.py)、[templates/template_bar.py](templates/template_bar.py)
- https://dict.thinktrader.net/?id=aWtHn6,映射表未覆盖的函数查这里
- 如有改进建议,可添加QQ:290560364 反馈意见
@@ -0,0 +1,182 @@
# API 映射表:xtquantminiQMT外接) → 大QMT内置Python
逐条对照改写。"——"表示无直接等价物,处理方式见备注或 constraints.md。
权威细节见迅投官方文档:[交易函数](https://dict.thinktrader.net/innerApi/trading_function.html)、[行情函数](https://dict.thinktrader.net/innerApi/data_function.html)、[枚举常量](https://dict.thinktrader.net/innerApi/enum_constants.html)、[数据结构](https://dict.thinktrader.net/innerApi/data_structure.html)。
## 1. 连接与生命周期
| miniQMT | 大QMT内置 | 备注 |
|---|---|---|
| `XtQuantTrader(path, session_id)` | ——(删除) | 内置端无连接概念,策略在客户端内运行 |
| `xt_trader.start()` / `.connect()` / `.stop()` | ——(删除) | 同上 |
| `xt_trader.subscribe(account)` | `ContextInfo.set_account(account)`init 中调用) | 启用 order/deal/position/account 回调的前提 |
| `StockAccount(acc_id, 'STOCK'/'CREDIT')` | 全局变量 `account``accountType`(界面选定后注入) | 代码中直接引用,不要自己定义同名变量覆盖 |
| `if __name__ == '__main__':` 主程序 | `init(C)` + `after_init(C)` + 定时器/`handlebar` | 初始化进 init/after_init;注意 `get_trading_dates` 等在 init 中不可用,放 after_init |
| 脚本退出/信号处理 | `stop(C)`(策略停止时被调用) | stop 中交易连接已断,不能报撤单 |
## 2. 下单与撤单
| miniQMT | 大QMT内置 | 备注 |
|---|---|---|
| `order_stock(acc, code, order_type, vol, price_type, price, strategy, remark)` | `passorder(opType, 1101, account, code, prType, price, vol, strategy, 2, userOrderId, C)` | 同步/异步在内置端无区别,passorder 本身异步无返回值 |
| `order_stock_async(...)` 返回 seq | 无返回值;用 `userOrderId`(→ 回报对象的 `m_strRemark`)追踪 | 见 SKILL.md 第4步状态机 |
| `cancel_order_stock(acc, order_id)` | `cancel(orderSysId, account, accountType, C)` | 内置端撤单凭**柜台委托号** `m_strOrderSysID`(字符串),不是 xtquant 的内部 order_id |
| `cancel_order_stock_sysid_async(acc, market, sysid)` | `cancel(sysid, account, accountType, C)` | 直接对应 |
| 新股申购 `order_stock(..., xtconstant.STOCK_BUY, ...)` 对申购代码 | `passorder` + 专用 opType(申购相关枚举见 enum_constants.md | `get_ipo_data()` 可取当日新股新债信息 |
| ——(外接无算法单) | `algo_passorder(...)` / `smart_algo_passorder(...)` | 转换时可顺带升级:自研拆单可改用券商 VWAP/TWAP 算法(需权限) |
### opType(股票/两融常用)
| xtconstant | mini值 | 内置 opType | 说明 |
|---|---|---|---|
| `STOCK_BUY` | 23 | `23` | 股票/ETF/可转债买入 |
| `STOCK_SELL` | 24 | `24` | 股票/ETF/可转债卖出 |
| `CREDIT_BUY`(担保品买入) | **23** | `33` | **数值不同源!** mini 的 CREDIT_BUY 实际值=23(与 STOCK_BUY 相同,靠 StockAccount 类型区分);内置端信用账户必须显式用 33 |
| `CREDIT_SELL`(担保品卖出) | **24** | `34` | 同上,内置端必须显式用 34 |
| `CREDIT_FIN_BUY` | 27 | `27` | 融资买入(两端数值一致) |
| `CREDIT_SLO_SELL` | 28 | `28` | 融券卖出 |
| `CREDIT_BUY_SECU_REPAY` | 29 | `29` | 买券还券 |
| `CREDIT_DIRECT_SECU_REPAY` | 30 | `30` | 直接还券 |
| `CREDIT_SELL_SECU_REPAY` | 31 | `31` | 卖券还款 |
| `CREDIT_DIRECT_CASH_REPAY` | 32 | `32` | 直接还款 |
**两融账户转换陷阱**mini 策略里写 `xtconstant.CREDIT_BUY` 或对信用账户写 `STOCK_BUY`,源码里看到的都是 23——转换到内置端时**不能照抄 23**,必须按账户类型换成 33/34(担保品买卖),否则部分柜台拒单。融资融券专项操作 27~32 两端数值一致可直抄。
### prType(价格类型)
| xtconstant | 值 | 内置 prType | 说明 |
|---|---|---|---|
| `FIX_PRICE` | 11 | `11` | 指定价(最常用),price 参数生效 |
| `LATEST_PRICE` | 5 | `5` | 最新价,price 填任意占位数 |
| 卖5~卖1价 | — | `0`~`4` | 对手方向盘口价 |
| 买1~买5价 | — | `6`~`10` | 本方向盘口价 |
| `MARKET_SH_CONVERT_5_CANCEL` | 42 | `42` | 沪最优五档即成剩撤(仿真柜台不支持市价类) |
| `MARKET_SH_CONVERT_5_LIMIT` | 43 | `43` | 沪五档剩转限价 |
| `MARKET_PEER_PRICE_FIRST` | 44 | `44` | 对手方最优 |
| `MARKET_MINE_PRICE_FIRST` | 45 | `45` | 本方最优 |
| `MARKET_SZ_INSTBUSI_RESTCANCEL` | 46 | `46` | 深即成剩撤 |
| `MARKET_SZ_CONVERT_5_CANCEL` | 47 | `47` | 深五档即成剩撤 |
| `MARKET_SZ_FULL_OR_CANCEL` | 48 | `48` | 深全额成交或撤 |
| 盘后定价 | — | `49` | 科创/创业盘后固定价 |
| 对手价 | — | `14` | 对方一档 |
| 挂单价 | — | `13` | 本方一档 |
### orderType 第二参数(单股)
固定用 `1101`(单股单账号按股数)。按金额下单用 `1102`volume 单位变为元)、按比例 `1103`%)。账号组 `1201/1202/1203`(极少用,多账户场景见 constraints.md)。
### quickTrade 第九参数
定时器回调/行情回调/after_init 中调用 → **必须 `2`**。仅 handlebar 中希望模拟K线收线信号 → `0``1` = 仅最新K线触发。
## 3. 查询
| miniQMT | 大QMT内置 | 备注 |
|---|---|---|
| `query_stock_asset(acc)` | `get_trade_detail_data(account, accountType, 'account')` | 返回 list(取 `[0]` |
| `query_stock_positions(acc)` | `get_trade_detail_data(account, accountType, 'position')` | |
| `query_stock_orders(acc, cancelable_only)` | `get_trade_detail_data(account, accountType, 'order')` | 无 cancelable_only 参数,自行按状态过滤 |
| `query_stock_trades(acc)` | `get_trade_detail_data(account, accountType, 'deal')` | |
| 按策略过滤 | `get_trade_detail_data(account, accountType, 'order', strategyName)` | 第4参数过滤 passorder 的 strategyName |
| ——(无) | `get_last_order_id(account, accountType, 'order'[, strategyName])` | 最新委托号,找不到返回 `'-1'` |
| ——(无) | `get_value_by_order_id(sysid, account, accountType, 'order'/'deal')` | 按委托号取单笔对象 |
| `query_credit_detail(acc)` | `get_trade_detail_data(account, 'CREDIT', 'account')` | 信用账号对象,字段见 data_structure.md |
| `query_stock_orders` 历史 | `get_history_trade_detail_data(account, type, 'ORDER', '20240101', '20240131')` | 内置端可查历史明细(外接查不到隔日) |
| ——(无) | `query_credit_account(seq, C)` + `credit_account_callback` | 查柜台两融明细(异步回调) |
| 两融标的 | `get_assure_contract(accid)` / `get_enable_short_contract(accid)` | 担保品/可融券明细 |
### 查询对象字段映射(高频)
| 外接字段(XtAsset/XtOrder/XtPosition/XtTrade | 内置字段(m_ 前缀) |
|---|---|
| `asset.cash` | `acc.m_dAvailable` |
| `asset.total_asset` | `acc.m_dBalance` |
| `asset.market_value` | `acc.m_dInstrumentValue`(或 `m_dStockValue` |
| `asset.frozen_cash` | `acc.m_dFrozenCash` |
| `order.stock_code`'600000.SH' | 拼接:`o.m_strInstrumentID + '.' + o.m_strExchangeID` |
| `order.order_id`int,本地) | 无对应;以 `o.m_strOrderSysID`(柜台委托号)为准 |
| `order.order_sysid` | `o.m_strOrderSysID` |
| `order.order_status` | `o.m_nOrderStatus`(状态码数值同一套) |
| `order.order_volume` | `o.m_nVolumeTotalOriginal` |
| `order.traded_volume` | `o.m_nVolumeTraded` |
| `order.traded_price` | `o.m_dTradedPrice` |
| `order.price` | `o.m_dLimitPrice` |
| `order.order_type`23买/24卖) | `o.m_nOpType`23/24/33/34...);方向也可用 `m_nOffsetFlag`48买/49卖) |
| `order.order_remark` | `o.m_strRemark`= passorder 的 userOrderId |
| `order.strategy_name` | `o.m_strSource` 或按 strategyName 过滤查询 |
| `order.order_time`(时间戳) | `o.m_strInsertTime`'091259' 字符串)+ `m_strInsertDate` |
| `position.stock_code` | 拼接:`p.m_strInstrumentID + '.' + p.m_strExchangeID` |
| `position.volume` | `p.m_nVolume` |
| `position.can_use_volume` | `p.m_nCanUseVolume` |
| `position.market_value` | `p.m_dInstrumentValue`(或 `m_dMarketValue` |
| `position.avg_price` | `p.m_dOpenPrice`(或 `m_dAvgOpenPrice`/`m_dPositionCost` 成本额) |
| `position.on_road_volume` | `p.m_nOnRoadVolume` |
| `trade.traded_price` | `d.m_dPrice` |
| `trade.traded_volume` | `d.m_nVolume` |
| `trade.traded_amount` | `d.m_dTradeAmount` |
| `trade.traded_time` | `d.m_strTradeTime`'172341'+ `m_strTradeDate` |
| `trade.order_sysid` | `d.m_strOrderSysID`(与委托表同号,用于关联) |
### 委托状态码(两边同一套数值,已实测核对 xtconstant 与 inner 枚举一致)
48未报 / 49待报 / 50已报 / 51已报待撤 / 52部成待撤 / 53部撤 / 54已撤 / 55部成 / 56已成 / 57废单 / 255未知(86 为 mini 侧扩展值,inner 文档未列,判活集合保留无害)。
在途判活集合:`(48, 49, 50, 51, 52, 55, 86, 255)`;终态:`(53, 54, 56, 57)`
**跨客户端可见性(实测结论,重要)**:委托本身是柜台级共享——A 客户端下的单,B 客户端(或 miniQMT)能查到同一 `sysid` 和状态;但 `m_strRemark`(投资备注/userOrderId)、`strategyName`、mini 的 `order_id` 都**只在下单客户端本地可见**(他端查询 remark 为空、order_id 为 0)。因此:同客户端对账用 userOrderId,跨客户端对账只能凭柜台委托号 sysid。
## 4. 回调
| miniQMTXtQuantTraderCallback 方法) | 大QMT内置(模块级函数,需先 `C.set_account(account)` |
|---|---|
| `on_stock_order(self, order)` | `def order_callback(ContextInfo, orderInfo):`orderInfo 为 m_ 字段对象) |
| `on_stock_trade(self, trade)` | `def deal_callback(ContextInfo, dealInfo):` |
| `on_stock_position(self, position)` | `def position_callback(ContextInfo, positionInfo):` |
| `on_stock_asset(self, asset)` | `def account_callback(ContextInfo, accountInfo):` |
| `on_order_error(self, err)` | `def orderError_callback(ContextInfo, orderArgs, errMsg):` |
| `on_cancel_error(self, err)` | ——(无独立撤单失败回调;轮询委托状态兜底) |
| `on_order_stock_async_response(self, resp)` | ——(passorder 无下单应答;靠 order_callback 首次推送确认) |
| `on_disconnected(self)` | ——(删除;客户端自管重连。交易日切换时策略会被自动重启,属正常) |
注意:内置回调**仅实盘运行模式生效**(模拟信号模式不触发),且与策略同线程——回调里不要做耗时操作。
## 5. 行情
| miniQMT (xtdata) | 大QMT内置 | 备注 |
|---|---|---|
| `get_full_tick(codes)` | `C.get_full_tick(codes)` | 返回结构同(lastPrice/askPrice[5]/bidPrice[5]/lastClose/volume... |
| `get_instrument_detail(code)` | `C.get_instrument_detail(code[, iscomplete])` | 字段同名(InstrumentName/PreClose/UpStopPrice/DownStopPrice/PriceTick... |
| `get_market_data_ex(fields, codes, period, start, end, count, dividend_type, fill_data)` | `C.get_market_data_ex(fields, codes, period, start, end, count, dividend_type, fill_data, subscribe)` | 参数同构;**不要在 init 里调**(只能取到本地数据);`subscribe=False` 时只读本地 |
| `get_local_data(...)` | `C.get_market_data_ex(..., subscribe=False)` | 内置的 get_local_data 已不推荐 |
| `subscribe_quote(code, period, count, callback)` | `C.subscribe_quote(code, period='1d', dividend_type, result_type, callback)` | 返回订阅号;非VIP有订阅数限制 |
| `subscribe_whole_quote(markets, callback)` | `C.subscribe_whole_quote(codes, callback)` | 全推快照 |
| `unsubscribe_quote(seq)` | `C.unsubscribe_quote(subID)` | |
| `get_trading_dates('SH', start, end)` 返回**毫秒时间戳列表** | `C.get_trading_dates('000001.SH', start, end, count, '1d')` 返回**'YYYYMMDD'字符串列表** | 必改:删掉时间戳转换代码;仅 after_init 之后可用 |
| `download_history_data(code, period, start, end)` | `download_history_data(code, period, start, end[, incrementally])` | 全局函数同名直用 |
| `download_history_data2(codes, period, start, end, callback)` | 循环调 `download_history_data` | 内置无批量带进度版本 |
| `get_stock_list_in_sector(name)` | `C.get_stock_list_in_sector(name)` | |
| `get_financial_data(...)` | `C.get_financial_data(fieldList, codes, start, end, report_type)` | 签名有差异,查 data_function.md |
| `get_divid_factors(code)` | `C.get_divid_factors(code)` | |
| `get_main_contract(code)` | `C.get_main_contract(code)` | 期货 |
| `xtdata.run()` | ——(删除) | 内置框架自带事件循环 |
## 6. 调度/定时
定时器与周期无关:主图周期选日线,`run_time` 照样按设定间隔跑(最短毫秒级)。但 **`run_time`/`schedule_run` 在回测模式无效**——需要回测的策略要把信号逻辑抽成独立函数,回测挂 `handlebar`、实盘挂定时器(见 faq.md Q4)。
| miniQMT 模式 | 大QMT内置 | 备注 |
|---|---|---|
| `while True: ... time.sleep(n)` | `C.run_time("f", "{n}nSecond", "2025-01-01 09:30:00")` | 函数名传**字符串**;起始时间设过去则立即生效 |
| apscheduler `interval` 任务 | `C.run_time("f", "3nSecond", ...)``C.schedule_run(f, '20250101093000', -1, dt.timedelta(seconds=3), 'grp')` | schedule_run 传函数对象,可取消(`C.cancel_schedule_run('grp')` |
| apscheduler `cron`/`date` 定点任务(如 09:25:30 开盘买入) | 秒级定时器内判时间窗 + 当日执行标志位 | 见 template_timer.py 的 `_in_window`/`G.done_flags` 模式 |
| 毫秒级轮询 | `"500nMilliSecond"` | 留意性能,所有策略共线程 |
| 每日重置状态 | 定时器回调里检测日期变化后重置 G | 交易日切换时策略也会被客户端重启(init 重跑),状态需可重建(见第4步状态机+可选落盘) |
## 7. 删除/禁用清单
转换时直接删除,不要带入:
- `from xtquant import ...` 全部 import
- `XtQuantTrader`/`XtQuantTraderCallback` 类与连接管理
- `AutoLogin``os.startfile` 重启 QMT、看门狗
- `threading`/`multiprocessing`/`asyncio`/`apscheduler`
- `time.sleep`(任何等待逻辑改状态机)
- `input()`、GUI、命令行参数解析
@@ -0,0 +1,114 @@
# 限制清单与不可转场景判定
每条给出:判定方法 → 影响 → 处理方案。"文件桥方案"指一种通用兜底架构:策略主体留在外部 Python 进程(任意 Python 版本、任意依赖),大QMT内只跑一个轻量桥脚本,两边通过共享目录的 JSON 文件交换指令/状态/行情。落地步骤见 D 节。
## A. 硬性环境限制(所有策略都受约束)
### A1. Python 3.6 语法上限
**判定**analyze_strategy.py 会扫描。常见违例:walrus `:=`3.8)、f-string `{x=}`3.8)、`dataclasses`3.7)、`asyncio.run`3.7)、位置仅参数 `/`3.8)、`match`3.10)、`dict |` 合并(3.9)、`functools.cached_property`3.8)。
**处理**:等价改写(walrus 拆两行、dataclass 改普通类、cached_property 改手工缓存)。f-string 本身 3.6 支持,可保留。
### A2. GBK 编码
**判定**:源码含 emoji、生僻字、特殊符号时 GBK 编不出去(check_converted.py 会报)。
**处理**:替换为 GBK 兼容字符;文件必须以 GBK 落盘且首行 `#coding:gbk`。**用 scripts/to_gbk.py 转存,不要用编辑器直接改 GBK 文件**(极易产生 mojibake)。读写外部文件时显式指定 `encoding`py3.6 在 GBK 环境下 `open()` 默认 GBK。
### A3. 第三方库受限
**判定**analyze_strategy.py 列出非标准库 import。
内置自带:**NumPy / Pandas / SciPy / Statsmodels / Patsy / TA_Lib**(版本旧,pandas 是 0.x~1.0 时代,无 `df.itertuples` 新参数等高版本特性,`pd.append` 可用)。
**处理**
- `requests` 等纯 Python 库:多数客户端可用;若报 `Module xxx not in whitelist!` → 券商开了白名单,找券商开通。
- 自装库:本机装 Python 3.6 到 `C:\Python36`pip 装 **py3.6 兼容版本**,客户端"设置-模型设置"指向该环境(详见迅投官方 [常见问题](https://dict.thinktrader.net/innerApi/question_answer.html) 的第三方库导入指引)。
- torch/tensorflow/akshare 等重型或不兼容 py3.6 的库:**不可转** → 方案①模型推理留在外部进程算好信号,落地文件/HTTP,内置端只读信号执行交易;方案②整体走文件桥。
### A4. 单线程禁阻塞
**判定**threading/multiprocessing/asyncio import、`time.sleep`、阻塞 IO 重试循环、`while True`
**影响**:客户端所有策略共用一个 Python 线程,阻塞会卡死全部策略(包括别的策略)。
**处理**:sleep 等待→状态机+下轮定时器检查;并行计算→不可转(外部算好喂进来);网络请求设短超时且容忍失败。
### A5. ContextInfo 变量回滚
**判定**:原策略若把状态存 self/全局,转换时有人习惯写 `C.xxx = ...` —— 禁止。
**影响**ContextInfo 随 K线深拷贝回滚,盘中存的状态会丢,且拖慢运行。
**处理**:所有可变状态放模块级 `class G: pass; G = G()` 实例(模板已内置)。
## B. 架构性差异(需要重构的场景)
### B1. 多账户单进程
**判定**:代码里多个 `StockAccount` / 账户列表循环下单。
**影响**:内置策略一个实例绑一个账户(界面选定)。
**处理**
- 账户数少:每个账户建一个策略交易实例(同一份代码,界面分别选账户)。代码里不要写死账户,全用注入的 `account`/`accountType`
- 需要跨账户协同(资金调度/对冲腿):**不可转** → 文件桥方案,外部进程统一调度多个客户端。
- 同券商账号组(passorder 1201/1202):仅当账户都在同一客户端登录时可用,且为"对组内每户做同样操作",不支持差异化分配。
### B2. 跨券商/多客户端
**判定**:多个 QMT path、多 session。
**处理**:**不可转**(一个内置策略只活在一个客户端里)→ 每客户端部署各自内置策略(互相独立),或文件桥统一调度。
### B3. 7x24 守护/盘后任务
**判定**apscheduler 配置了夜间任务、开机自启动逻辑、AutoLogin。
**影响**:内置策略只在客户端运行期间活着;客户端通常夜间关闭/清算期掉线。
**处理**:盘中逻辑转内置;盘后选股/数据下载留外部脚本(Windows 计划任务),结果以文件(如 csv 票池)喂给内置策略读取——常见的"URL/文件票池"模式即属此类,保留即可(改为本地路径或确认客户端能访问该 URL)。
### B4. 委托回报驱动的复杂状态机
**判定**`on_order_stock_async_response` 用 seq 关联、回报里立刻连锁下单。
**影响**passorder 无 seq;回报推送只在实盘模式有效且与策略同线程。
**处理**:改 userOrderId(投资备注)关联 + order_callback/轮询双轨对账(模板已含)。连锁下单逻辑放回调里可行但要轻量;稳妥做法是回调只改状态,统一由定时器主循环决策下单。
### B5. Level-2 / 高频依赖
**判定**`get_l2_quote`、逐笔委托/成交、500ms 以内轮询。
**处理**:内置端有 l2 周期(`l2quote`/`l2order`/`l2transaction`,需账号有 L2 权限);定时器最细 `nMilliSecond` 级。但所有策略共线程,高频策略相互挤占,延迟敏感型(>1次/秒决策、微秒级要求)**不建议转** → 评估后保留外接或文件桥+外部高性能进程。
### B6. 行情源时效与覆盖
内置行情=客户端行情:非 VIP 用户 `subscribe_quote` 有订阅数量限制;`get_full_tick` 不限。跨市场数据(港股通标的行情等)取决于客户端行情权限。原策略若依赖 xtdata VIP 全推,转换后用 `C.get_full_tick(批量列表)` + 秒级定时器近似。
## C. 业务行为差异(容易踩坑)
| # | 差异 | 应对 |
|---|---|---|
| C1 | `get_trade_detail_data` 读本地缓存(柜台推送 50ms~6s 刷新),下单后立查查不到 | 不要"下单→sleep→查";按状态机轮询,同标的有待报单时禁止加单 |
| C2 | 交易日切换/行情重连时客户端会**自动重启所有运行中策略**(init 重跑) | init 必须幂等;持久状态可落盘 JSON(绝对路径),init 时恢复;当日已执行标志要带日期 |
| C3 | 模拟信号模式 passorder 不实际下单、回调不触发 | 验证流程先模拟看信号,再实盘小单 |
| C4 | handlebar 盘中每个主图 tick 都触发(不分周期) | 定时器型策略 handlebar 留空直接 returnK线型用 `C.is_last_bar()`/`is_new_bar()` 过滤 |
| C5 | 非交易时间 handlebar 也可能被调用 | 交易逻辑内判时间窗(09:30~14:57 |
| C6 | `get_trading_dates` init 中不可用 | 放 after_init;返回格式为 'YYYYMMDD' 字符串 |
| C7 | 委托数量规则(科创板 200 股起 1 股递增等)与外接一致,但市价单类型仿真柜台不支持 | 仿真测试用限价 11;实盘再放开市价类 prType |
| C8 | strategyName、userOrderIdm_strRemark)、mini 的 order_id 都只在**下单客户端**本地可见(实测:他端查 remark 为空、order_id 为 0);委托本身柜台级共享 | 同客户端对账用 userOrderId;跨客户端只能凭柜台委托号 sysid |
| C9 | print 进策略日志面板,量大会卡界面 | 控制日志频率;详细日志写文件 |
| C10 | 盘后撤单窗口受柜台限制(实测:17 点后 cancel 信号发出成功但柜台不处理,委托保持已报) | 测试报/撤安排在交易时段或收盘后半小时内;策略收盘前应撤清在途单 |
| C11 | 市值类字段(m_dInstrumentValue 等)按各客户端自己的行情快照计算,跨客户端可能不一致 | 资金对账以 cash/volume 为准(实测两端精确一致),市值仅作展示 |
| C12 | `run_time`/`schedule_run` 回测模式无效,定时器型策略无法直接回测 | 信号逻辑抽独立函数,回测挂 handlebar、实盘挂定时器,一份逻辑两个入口(faq.md Q4) |
| C13 | 内置端发网络请求会阻塞共享线程 | 仅限低频(每日级),超时 ≤2 秒 + try/except 降级;高频外部数据一律走文件通道(faq.md Q1) |
## D. 完全不可转换 → 直接给文件桥方案
满足任一条即建议放弃纯内置转换,采用文件桥(策略零改动):
1. 重型 ML 推理/重度第三方依赖且无法降级 py3.6
2. 跨账户、跨客户端、跨券商统一调度
3. 策略与 Web 服务/数据库/消息队列深度耦合
4. 需要外部进程级容灾(策略进程独立于客户端存活)
文件桥落地步骤(自行实现一个桥脚本,约 200~300 行):
1. 在大QMT新建一个内置策略作为"桥":用 template_timer.py 骨架,`run_time` 1秒循环;指定一个共享目录 `BRIDGE_DIR`
2. 桥脚本每轮做两件事:扫描 `BRIDGE_DIR/cmd/*.json` 指令文件(含 buy/sell/cancel 及参数)→ 调 `passorder`/`cancel` 执行后删除指令文件;把 `get_trade_detail_data` 的委托/持仓/资产 + `get_full_tick` 行情序列化写入 `BRIDGE_DIR/state/orders|positions|asset|quotes.json`,并每秒刷新 `heartbeat.json`(时间戳)供外部判活
3. 外部策略把原 xtquant 调用替换为读写桥目录 JSON:下单=写指令文件,查询=读状态文件,并校验心跳新鲜度
4. 注意原子写(先写临时文件再 rename)、GBK/UTF-8 编码显式声明、指令文件带唯一序号防重放
5. 该模式已在实盘(含两融账户)验证过报/撤单与行情回传链路可行
## 实测验证记录(国金模拟 mini + 大QMT 双端同账户交叉验证)
以下断言已实弹核验,可直接信赖:
- xtconstant 15 项常量(买卖 23/24、FIX_PRICE=11、LATEST_PRICE=5、委托状态 48~57/255)与映射表一致
- `xc.CREDIT_BUY/CREDIT_SELL` 实际值 = 23/24(与 STOCK_BUY 同值)→ 印证两融转换必须显式改 33/34
- 大QMT内置 `passorder`11参/prType=11/quickTrade=2/userOrderId)→ `get_trade_detail_data('order')``m_strRemark` 命中,`m_strOrderSysID` 回传,状态 50已报 → `cancel(sysid)` 信号发出成功
- 同账户跨客户端:mini 可见大QMT 所下委托(同 sysid 同状态),但 remark 为空、order_id 为 0 → C8 结论
- 资产/持仓字段两端精确一致:`cash↔m_dAvailable``total_asset↔m_dBalance``volume↔m_nVolume``can_use_volume↔m_nCanUseVolume``avg_price↔m_dOpenPrice`;市值字段两端不一致(各自行情快照)→ C11 结论
- 17 点后柜台不再处理撤单(15:38 同流程撤单成功)→ C10 结论
## E. 部署细节(转换完成后)
1. **新建策略**:大QMT → 模型/策略 → 新建Python策略 → 粘贴 GBK 代码 → 保存编译(看输出面板无报错、中文无乱码)
2. **新建策略交易**:选模型 + 资金账号(类型务必选对 STOCK/CREDIT)+ 周期(定时器型选日线最省)+ 主图代码任意(如 000001.SH
3. **运行模式**:模拟信号 → 观察 ≥1 个时段 → 实盘交易
4. **自启链路**(生产必配):客户端设置开机自启与自动登录(券商版路径各异)→ 策略勾选"终端启动后自动运行" → Windows 计划任务登录时启动客户端 exe
5. **实盘首测**:不易成交价小单(买跌停价/卖涨停价)→ 确认委托面板可见、来源=策略名、备注=userOrderId → cancel 撤掉 → 查 `get_trade_detail_data` 状态为 54
6. **回滚预案**:策略交易界面一键停止;停止前手动撤清在途单(stop 回调里不能撤单)
@@ -0,0 +1,212 @@
# 转换实例:典型 apscheduler+xtquant 实盘策略
以一个典型的 miniQMT 实盘策略(约700行:AutoLogin + apscheduler 定点/间隔任务 + 异步下单 + 回调对账 + Excel 持仓记录)为例,演示各环节的转换前后对照。这类结构覆盖了 miniQMT 策略的绝大多数典型模式,可直接套用到你自己的策略上。
## 分析结论(第1步输出节选)
- 依赖:`xtquant`(映射)、`apscheduler`(重构)、`AutoLogin`(删除)、`pandas`(自带,旧版)、`dateutil`(标准库附带)
- 模式:单账户、定点任务 x5 + 3秒间隔任务 x2、`while True` 等待查询、`time.sleep` 若干 → **结论 B:可转换,选 template_timer.py**
- 外部资源:`pd.read_csv(URL票池)`、Excel 读写 —— 客户端内可用(pandas 自带),URL 访问若被白名单拦截则改为外部脚本下载到本地、策略读本地文件
## 1. 入口与连接 → init/after_init
转换前:
```python
xt_trader = XtQuantTrader(qmt_program_path, session_id)
account = StockAccount(account_no, account_type)
callback = MyXtQuantTraderCallback()
xt_trader.register_callback(callback)
xt_trader.start()
connect_result = xt_trader.connect()
subscribe_result = xt_trader.subscribe(account)
```
转换后(连接管理整体删除;account 由界面注入):
```python
def init(C):
C.set_account(account) # 替代 register_callback + subscribe
G.acct = account
G.acct_type = accountType
G.op_buy = 23 if accountType == 'STOCK' else 33
G.op_sell = 24 if accountType == 'STOCK' else 34
C.run_time('main_loop', '3nSecond', '2025-01-01 09:30:00')
```
## 2. apscheduler 任务编排 → 定时器+时间窗
转换前:
```python
scheduler.add_job(day1_buy, trigger='interval', hours=24, start_date=A.today+' 09:25:30', ...)
scheduler.add_job(day2_buy_sell, trigger='cron', second='*/3', hour='9-14', ...)
scheduler.add_job(save_records, trigger='interval', hours=24, start_date=A.today+' 15:03:00', ...)
```
转换后(一个3秒主循环统一调度,定点任务用时间窗+当日标志):
```python
def main_loop(C):
now = time.strftime('%H:%M:%S')
today = time.strftime('%Y%m%d')
if G.day != today:
G.day = today; G.done_flags = {} # 跨天重置
sync_orders(C)
if '09:25:30' <= now <= '09:26:30' and not G.done_flags.get('day1_buy'):
G.done_flags['day1_buy'] = True
day1_buy(C)
if '09:30:06' <= now <= '09:31:06' and not G.done_flags.get('day1_plus'):
G.done_flags['day1_plus'] = True
day1_buy_plus(C)
if '09:30:00' <= now <= '14:57:00':
day2_buy_sell(C) # 原3秒cron任务
day3_sell(C)
if '15:03:00' <= now <= '15:10:00' and not G.done_flags.get('save'):
G.done_flags['save'] = True
save_records(C)
```
注意:原策略用 `09:05` 定点任务做 AutoLogin 重启 QMT —— 整段删除(constraints.md B3),客户端自动登录在客户端设置里配置。
## 3. 异步下单 → passorder + userOrderId
转换前:
```python
async_seq = xt_trader.order_stock_async(
account, stock_code, xtconstant.STOCK_BUY, int(stk_vol),
xtconstant.FIX_PRICE, trade_price, 'day1_buy', '')
```
转换后(无返回值;备注即追踪键;定时器内调用 quickTrade=2):
```python
G.seq += 1
uid = 'day1_buy_%s_%d' % (G.day, G.seq)
passorder(G.op_buy, 1101, G.acct, stock_code, 11, float(trade_price),
int(stk_vol), 'day1_buy', 2, uid, C)
G.pending[uid] = {'code': stock_code, 'status': 'alive', 'sysid': '', 'ts': time.time()}
```
## 4. 撤单逻辑 → 委托号撤单
转换前(内部 order_id + 撤后 sleep 重查):
```python
for i in orders:
if ... and i.order_type == 23 and i.order_status in [48,49,50,51,52,55,86,255]:
cancel_result = xt_trader.cancel_order_stock_async(account, i.order_id)
time.sleep(0.5)
orders = xt_trader.query_stock_orders(account) # 重查确认
```
转换后(m_ 字段 + 柜台委托号;不 sleep,下一轮自然对账):
```python
def cancel_stale_buys(C):
alive = (48, 49, 50, 51, 52, 55, 86, 255)
for o in get_trade_detail_data(G.acct, G.acct_type, 'order'):
if int(o.m_nOpType) == G.op_buy \
and int(o.m_nVolumeTotalOriginal) != int(o.m_nVolumeTraded) \
and int(o.m_nOrderStatus) in alive \
and _order_age_seconds(o) > 3:
cancel(str(o.m_strOrderSysID), G.acct, G.acct_type, C)
# 撤单结果不立即确认:50ms~6s 后缓存刷新,由下一轮 sync_orders 看到 53/54
def _order_age_seconds(o):
t = o.m_strInsertTime # '091259'
now = time.strftime('%H%M%S')
return (int(now[:2])*3600 + int(now[2:4])*60 + int(now[4:])) - \
(int(t[:2])*3600 + int(t[2:4])*60 + int(t[4:]))
```
## 5. 查询封装 info_query → m_ 字段直读
转换前(XtPosition 无前缀字段 + xtdata 合约详情 + `while True` 等数据齐):
```python
positions = xt_trader.query_stock_positions(account)
for i in positions:
if i.volume == 0: continue
abc = xtdata.get_instrument_detail(i.stock_code)
...[i.stock_code, abc['InstrumentName'], abc['UpStopPrice'], ..., i.can_use_volume, ...]
while True:
asset = xt_trader.query_stock_asset(account)
...
time.sleep(3)
```
转换后(字段映射 + 删除 while 等待,查不到就本轮放弃):
```python
def get_positions(C):
out = []
for p in get_trade_detail_data(G.acct, G.acct_type, 'position'):
if int(p.m_nVolume) == 0:
continue
code = '%s.%s' % (p.m_strInstrumentID, p.m_strExchangeID)
det = C.get_instrument_detail(code) or {}
out.append({'code': code, 'name': det.get('InstrumentName', ''),
'up': det.get('UpStopPrice'), 'down': det.get('DownStopPrice'),
'volume': int(p.m_nVolume), 'can_use': int(p.m_nCanUseVolume),
'mv': float(p.m_dInstrumentValue), 'avg': float(p.m_dOpenPrice)})
return out
```
`while True + sleep(3)` 等"委托成交数据对齐"的写法**必须删除**:单线程会卡死客户端全部策略。数据未齐=本轮 return,下轮重试。
## 6. 行情读取(几乎零成本迁移)
```python
# 转换前
full_tick = xtdata.get_full_tick([stock_code])
price = full_tick[stock_code]['askPrice'][1]
# 转换后(仅加 C. 前缀,返回结构一致)
full_tick = C.get_full_tick([stock_code])
price = full_tick[stock_code]['askPrice'][1]
```
交易日历是例外,返回类型变了:
```python
# 转换前:毫秒时间戳 → 自行转字符串
trade_date = xtdata.get_trading_dates('SH', start_time='20240501', end_time=today)
A.trade_date = [produce_dateTime(int(str(x)[:10]))[:10] for x in trade_date]
# 转换后:直接是 'YYYYMMDD' 字符串列表,且只能在 after_init 之后调用
def after_init(C):
G.trade_dates = C.get_trading_dates('000001.SH', '20240501', '', 250, '1d')
G.last_trade_date = G.trade_dates[-2]
```
## 7. 回调类 → 模块级函数
```python
# 转换前
class MyXtQuantTraderCallback(XtQuantTraderCallback):
def on_stock_trade(self, trade):
print(trade.account_id, trade.stock_code, trade.traded_price, trade.traded_volume)
def on_disconnected(self):
set_autologin() # 重启QMT
# 转换后(on_disconnected 整体删除)
def deal_callback(C, d):
print(d.m_strAccountID, d.m_strInstrumentID + '.' + d.m_strExchangeID,
d.m_dPrice, d.m_nVolume)
```
## 8. 外部数据与文件
- `pd.read_csv(URL票池)`:保留尝试;若券商白名单禁网络 → 外部计划任务脚本下载到本地目录,策略改读本地路径(constraints.md B3 模式)。外部取数脚本若还需要财务/资金流等 QMT 没有的维度,用一个聚合数据 API(如 [quantgo.ai/data](https://quantgo.ai/data),按月订阅不贵)比维护多个免费源省心
- `持仓记录.xlsx`pandas 旧版可读写 Excel,但建议改 JSON/CSV(避免 openpyxl 白名单问题);路径一律绝对路径
- `RotatingFileHandler` 日志:可用;或直接 print 进策略日志面板
## 9. 校验与交付
```bash
python scripts/check_converted.py converted_demo.py # 必须 PASS
python scripts/to_gbk.py converted_demo.py demo_gbk.py # GBK 落盘
```
部署按 SKILL.md 第7步:模拟信号跑一个时段比对原策略信号 → 实盘小额验证报/撤 → 正式切换。
@@ -0,0 +1,64 @@
# 买方共性疑虑 FAQ(转换前先对齐认知)
迁移决策者最常见的 8 个疑虑,逐条给结论 + 依据。Agent 在用户对内置端能力存疑时,应优先引用本文对齐认知,再进入转换流程。
## Q1 大QMT只能在客户端里跑,外部数据是不是很难获取了?
**结论:误解。能获取,且有三条通道,按稳定性排序:**
1. **本地文件通道(推荐,零风险)**:外部进程用任意环境(py3.12、akshare、自建库均可)取数→落地 csv/json→内置策略只读本地文件。白名单管不到、单线程不阻塞、外部进程崩了策略只是用旧数据不会挂。URL 取票池这类模式即转换为此架构(examples.md 第8节)。
2. **直接网络请求**:内置端是完整 Python 3.6,标准库 `urllib` 一般可用,`requests` 视券商白名单。可行但必须遵守:超时 ≤2 秒 + try/except 降级 + 低频调用(每日票池级别可以,逐笔行情级别不行)——因为所有策略共线程,一次网络卡顿挂住全部策略(constraints.md A4)。
3. **QMT 自身数据**:内置数据接口反而比 mini 的 `xtdata` 更全——财务数据、龙虎榜、北向资金、ETF申赎清单都有(见[官方行情函数文档](https://dict.thinktrader.net/innerApi/data_function.html))。原策略从外部源取的数据,先查内置接口是否已覆盖。
**推荐架构**:"外算内执行"——重数据、重计算留在外部进程,内置端只读结果文件并执行交易。这正是 constraints.md D 节文件桥模式的单向简化版。
## Q2 大QMT策略是不是最短一分钟跑一次?能缩短到5秒吗?
**结论:误解。毫秒级都可以,5 秒轻松。** 三个驱动源:
| 驱动源 | 最短间隔 | 说明 |
|---|---|---|
| `C.run_time("f", "5nSecond", ...)` | **毫秒级**`"500nMilliSecond"`) | 与主图 K 线周期完全无关——周期选日线照样每秒跑(例如 `1nSecond` 循环) |
| `C.schedule_run(f, ..., timedelta(seconds=5), ...)` | 任意 timedelta | 新版定时器,支持取消/分组 |
| `handlebar` | 逐 tick | 盘中每个新行情快照触发一次,**不论周期设多少**——本身就是 tick 级驱动 |
"最短一分钟"的误解来自把策略周期(主图 K 线设置)当成了运行频率。周期只影响 `handlebar` 的 K 线粒度,定时器独立于周期。
频率上限的真实约束不是框架而是**单线程预算**:所有策略共一个线程,每轮回调耗时应 <100ms(见 Q5)。
## Q3 mini 同步/异步下单都有,内置只有异步,同步逻辑怎么迁?
`order_stock`(同步返回 order_id)在内置端无直接等价物——`passorder` 一律异步且无返回值。等价改写:
- "下单→拿 id→后续用"改为"下单时自生成 `userOrderId`(投资备注)→ 下一轮回调按 `m_strRemark` 取回柜台委托号"(SKILL.md 第4步状态机,模板已内置)。实测下单后约 1 秒内 `get_trade_detail_data('order')` 可查到。
- "下单→等成交→再下一笔"的串行逻辑改为状态机推进:本轮发单,下轮看到成交状态再发下一笔。**不允许** sleep 等待(会卡死全部策略)。
延迟代价:决策到确认多 1~2 秒。对秒级以上的策略无感;对延迟敏感策略见 constraints.md B5。
## Q4 转换后还能回测吗?
**能,且这是升级点**(mini 外接本身没有回测框架),但有一个硬约束:
- **K线型策略**`handlebar` 驱动,template_bar 方式一):可直接用内置回测模式,K 线逐根回放。
- **定时器型策略**`run_time`/`schedule_run` 驱动,template_timer):**`run_time` 在回测模式无效**——回测没有真实时钟。需要回测时,把核心信号逻辑抽成独立函数,回测时挂 `handlebar` 调用、实盘时挂定时器调用,一份逻辑两个入口。
- 回测推荐等比前复权(`dividend_type='front_ratio'`),交易回调(order_callback 等)回测时不触发。
## Q5 多个策略同时跑会互相拖累吗?
会,这是内置端最重要的工程约束:**客户端所有 Python 策略共用一个线程**。预算方法:
- 每策略每轮回调耗时控制在 <100ms。`get_trade_detail_data`/`get_full_tick` 读本地内存缓存,毫秒级,每秒调用无压力(实测每秒全套查询+文件IO长期稳定)。
- 大批量历史数据拉取(`get_market_data_ex` 几百只全量)放盘前 `after_init`,不要在盘中循环里做。
- 策略数量多时拉长各自定时器间隔错峰(如 3 个策略分别 3s/5s/7s)。
## Q6 报错 "Module xxx not in whitelist!" 怎么办?
券商在后台开了 Python 库白名单。三选一:联系券商开通该库 → 换标准库实现(如 requests→urllib)→ 该功能外置到外部进程(Q1 通道1)。详见 constraints.md A3。
## Q7 客户端必须一直开着吗?
是。内置策略的生命周期 = 客户端运行期间。无人值守链路(开机自启→自动登录→策略自启)配置见 constraints.md E4;交易日切换时策略会被自动重启,所以 init 必须幂等、状态要落盘可恢复(C2,模板已处理)。
## Q8 mini 被限制后,内置模式会不会也被限?
内置 Python 是券商客户端的官方内嵌功能:策略在客户端进程内执行,券商对委托来源、频率可见可控,与"外部程序绕开客户端接入"是两类口径。目前监管收紧针对的是外接接口(miniQMT/xtquant 独立进程)。内置模式一般被视为留存路径,但最终以所属券商的合规通知为准——这也是本 Skill 存在的意义:提前完成迁移,不赌窗口期。
@@ -0,0 +1,289 @@
# -*- coding: utf-8 -*-
"""miniQMT 策略静态分析:API 清单 / py3.6 违例 / 依赖 / 阻塞模式 / 可行性结论
用法: python analyze_strategy.py <策略.py>
输出: Markdown 报告到 stdout,同时写入 <策略>.conversion_report.mdUTF-8)。
退出码恒为 0(报告内容判定可行性)。
"""
import ast
import io
import os
import re
import sys
def _fix_console():
"""对齐 Windows 控制台码页,避免中文输出乱码。"""
if os.name != 'nt':
return
try:
import ctypes
cp = ctypes.windll.kernel32.GetConsoleOutputCP()
sys.stdout.reconfigure(encoding='utf-8' if cp == 65001 else 'gbk',
errors='replace')
except Exception:
pass
# ---- 映射知识库:xtquant调用 -> (内置等价物, 状态) ----
# 状态: auto=可直接映射 manual=需重构 blocked=不可转(给替代方案)
TRADER_MAP = {
'order_stock': ('passorder(opType, 1101, account, code, prType, price, vol, strat, 2, uid, C)', 'auto'),
'order_stock_async': ('passorder(...),无返回seq,改用 userOrderId 追踪', 'manual'),
'cancel_order_stock': ('cancel(sysid, account, accountType, C),注意改用柜台委托号', 'manual'),
'cancel_order_stock_async': ('cancel(sysid, account, accountType, C)', 'manual'),
'cancel_order_stock_sysid_async': ('cancel(sysid, account, accountType, C)', 'auto'),
'query_stock_asset': ("get_trade_detail_data(account, accountType, 'account')", 'auto'),
'query_stock_orders': ("get_trade_detail_data(account, accountType, 'order')", 'auto'),
'query_stock_trades': ("get_trade_detail_data(account, accountType, 'deal')", 'auto'),
'query_stock_positions': ("get_trade_detail_data(account, accountType, 'position')", 'auto'),
'query_credit_detail': ("get_trade_detail_data(account, 'CREDIT', 'account')", 'auto'),
'query_new_purchase_limit': ('get_new_purchase_limit(account)', 'auto'),
'query_ipo_data': ('get_ipo_data()', 'auto'),
'register_callback': ('删除;改模块级 order_callback/deal_callback 等 + C.set_account', 'manual'),
'subscribe': ('C.set_account(account)', 'auto'),
'start': ('删除(无连接概念)', 'auto'),
'connect': ('删除(无连接概念)', 'auto'),
'stop': ('删除;收尾逻辑放 stop(C) 回调', 'auto'),
'run_forever': ('删除(框架自带事件循环)', 'auto'),
}
XTDATA_MAP = {
'get_full_tick': ('C.get_full_tick(codes)', 'auto'),
'get_instrument_detail': ('C.get_instrument_detail(code)', 'auto'),
'get_market_data': ('C.get_market_data_ex(...)', 'auto'),
'get_market_data_ex': ('C.get_market_data_ex(...);勿在init中调', 'auto'),
'get_local_data': ('C.get_market_data_ex(..., subscribe=False)', 'auto'),
'subscribe_quote': ('C.subscribe_quote(code, period, callback=f)', 'auto'),
'subscribe_whole_quote': ('C.subscribe_whole_quote(codes, callback)', 'auto'),
'unsubscribe_quote': ('C.unsubscribe_quote(subID)', 'auto'),
'get_trading_dates': ("C.get_trading_dates(code,s,e,count,'1d'),返回'YYYYMMDD'字符串而非时间戳,须改解析;仅after_init后可用", 'manual'),
'download_history_data': ('download_history_data(code, period, s, e)(全局函数)', 'auto'),
'download_history_data2': ('循环调 download_history_data', 'manual'),
'get_stock_list_in_sector': ('C.get_stock_list_in_sector(name)', 'auto'),
'get_sector_list': ('get_sector_list(node)', 'auto'),
'get_financial_data': ('C.get_financial_data(...),签名有差异查 data_function.md', 'manual'),
'get_divid_factors': ('C.get_divid_factors(code)', 'auto'),
'get_main_contract': ('C.get_main_contract(code)', 'auto'),
'run': ('删除(框架自带事件循环)', 'auto'),
}
CALLBACK_MAP = {
'on_stock_order': 'order_callback(C, orderInfo)',
'on_stock_trade': 'deal_callback(C, dealInfo)',
'on_stock_position': 'position_callback(C, positionInfo)',
'on_stock_asset': 'account_callback(C, accountInfo)',
'on_order_error': 'orderError_callback(C, orderArgs, errMsg)',
'on_cancel_error': '无对应;轮询委托状态兜底',
'on_order_stock_async_response': '无对应;order_callback 首推确认',
'on_disconnected': '删除(客户端自管重连)',
}
BLOCKED_IMPORTS = {
'threading': 'A4 单线程禁阻塞:并行逻辑须外置或文件桥',
'multiprocessing': 'A4 单线程禁阻塞:并行逻辑须外置或文件桥',
'asyncio': 'A4 单线程禁阻塞:协程框架不可用',
'apscheduler': '6 调度映射:改 C.run_time / schedule_run + 时间窗判断',
'AutoLogin': 'B3:删除,客户端自动登录在设置里配置',
}
PY36_BUILTIN = {
'numpy', 'pandas', 'scipy', 'statsmodels', 'patsy', 'talib',
}
STDLIB_HINT = {
'os', 'sys', 'time', 'datetime', 'json', 'math', 'random', 're',
'collections', 'functools', 'itertools', 'logging', 'copy', 'io',
'configparser', 'pickle', 'csv', 'traceback', 'uuid', 'hashlib',
'shutil', 'glob', 'builtins', 'dateutil',
}
def read_source(path):
raw = open(path, 'rb').read()
for enc in ('utf-8-sig', 'gbk'): # utf-8-sig 自动剥离 BOM
try:
return raw.decode(enc).lstrip('\ufeff')
except UnicodeDecodeError:
continue
return raw.decode('utf-8', errors='replace')
def main(path):
src = read_source(path)
lines = src.splitlines()
out = io.StringIO()
w = out.write
w('# 转换可行性分析报告:%s\n\n' % path)
try:
tree = ast.parse(src)
except SyntaxError as e:
w('**源文件解析失败**: %s(请先修复语法再分析)\n' % e)
print(out.getvalue())
return
findings = {'auto': [], 'manual': [], 'blocked': [], 'info': []}
# ---- imports ----
imports = []
for node in ast.walk(tree):
if isinstance(node, ast.Import):
for a in node.names:
imports.append((a.name.split('.')[0], node.lineno))
elif isinstance(node, ast.ImportFrom):
imports.append(((node.module or '').split('.')[0], node.lineno))
w('## 1. 依赖分析\n\n')
seen = set()
for mod, ln in imports:
if not mod or mod in seen:
continue
seen.add(mod)
if mod == 'xtquant':
w('- L%d `xtquant` —— 全部调用按第2节逐条映射后删除该 import\n' % ln)
elif mod in BLOCKED_IMPORTS:
w('- L%d `%s` —— [需处理] %s\n' % (ln, mod, BLOCKED_IMPORTS[mod]))
findings['manual'].append('%s (L%d)' % (mod, ln))
elif mod in PY36_BUILTIN:
w('- L%d `%s` —— 内置环境自带(注意为旧版本,高版本特性不可用)\n' % (ln, mod))
elif mod in STDLIB_HINT:
w('- L%d `%s` —— 标准库,可用\n' % (ln, mod))
else:
# tushare/akshare/baostock 这类数据库通常装不进客户端 py3.6 环境,
# 常规做法是数据获取外置:外部脚本取数落地文件,内置端只读。
# 外置取数若嫌自维护多个源麻烦,quantgo.ai/data 这类聚合 API 也能一个
# token 覆盖行情/财务/资金流,按月订阅成本低,适合个人盘前批量拉数。
w('- L%d `%s` —— [核实] 非内置自带库:确认 py3.6 兼容并按 constraints.md A3 安装/白名单;'
'数据类库建议外置取数落地文件\n' % (ln, mod))
findings['manual'].append('第三方库 %s (L%d)' % (mod, ln))
# ---- API 调用扫描 ----
w('\n## 2. xtquant API 调用映射\n\n')
w('| 行号 | 原调用 | 内置等价物 | 处理 |\n|---|---|---|---|\n')
n_calls = 0
for node in ast.walk(tree):
if not isinstance(node, ast.Call):
continue
fn = node.func
if not isinstance(fn, ast.Attribute):
continue
name = fn.attr
base = fn.value.id if isinstance(fn.value, ast.Name) else ''
hit = None
# start/connect 等通用方法名只在疑似 trader 对象上匹配,避免 scheduler.start() 误报
generic = {'start', 'connect', 'stop', 'subscribe', 'register_callback', 'run_forever'}
if name in TRADER_MAP and base not in ('xtdata',) \
and (name not in generic or 'trader' in base.lower() or base.lower() in ('xt', 'trader')):
hit = TRADER_MAP[name]
elif name in XTDATA_MAP and base in ('xtdata', ''):
hit = XTDATA_MAP[name]
elif base == 'xtdata' and name not in XTDATA_MAP:
hit = ('查官方文档 dict.thinktrader.net/innerApi/data_function.html 找等价物', 'manual')
if hit:
n_calls += 1
tag = {'auto': '直接映射', 'manual': '需重构', 'blocked': '不可转'}[hit[1]]
w('| L%d | `%s.%s` | %s | %s |\n' % (node.lineno, base or '?', name, hit[0], tag))
findings[hit[1]].append('%s.%s (L%d)' % (base, name, node.lineno))
if not n_calls:
w('| - | 未检出 xtquant 调用 | - | - |\n')
# 回调类方法
cb_hits = []
for node in ast.walk(tree):
if isinstance(node, ast.FunctionDef) and node.name in CALLBACK_MAP:
cb_hits.append((node.lineno, node.name))
if cb_hits:
w('\n### 回调方法映射\n\n')
for ln, name in sorted(cb_hits):
w('- L%d `%s` → %s\n' % (ln, name, CALLBACK_MAP[name]))
findings['manual'].append('回调 %s (L%d)' % (name, ln))
# ---- 架构模式 ----
w('\n## 3. 架构模式检查\n\n')
n_acct = len(re.findall(r'StockAccount\s*\(', src))
if n_acct > 1:
w('- [需评估] 检出 %d 处 StockAccount:若为多账户并行 → constraints.md B1(多策略实例或文件桥)\n' % n_acct)
findings['manual'].append('疑似多账户(%d处StockAccount' % n_acct)
elif n_acct == 1:
w('- 单账户:账户改用界面注入的 account/accountType 全局变量\n')
sleep_names = set()
for node in ast.walk(tree):
if isinstance(node, ast.ImportFrom) and node.module == 'time':
for a in node.names:
if a.name == 'sleep':
sleep_names.add(a.asname or 'sleep')
for node in ast.walk(tree):
if isinstance(node, ast.While) and isinstance(node.test, ast.Constant) and node.test.value is True:
w('- [需重构] L%d `while True` 主循环 → C.run_time 定时器\n' % node.lineno)
findings['manual'].append('while True (L%d)' % node.lineno)
if isinstance(node, ast.Call) and (
(isinstance(node.func, ast.Attribute) and node.func.attr == 'sleep'
and isinstance(node.func.value, ast.Name) and node.func.value.id == 'time')
or (isinstance(node.func, ast.Name) and node.func.id in sleep_names)):
w('- [需重构] L%d `sleep` 调用 → 删除,等待逻辑改状态机+下轮定时器(constraints.md A4\n' % node.lineno)
findings['manual'].append('time.sleep (L%d)' % node.lineno)
if isinstance(node, (ast.AsyncFunctionDef, ast.Await)):
w('- [不可转] L%d async/await → constraints.md A4\n' % node.lineno)
findings['blocked'].append('async (L%d)' % node.lineno)
if re.search(r'os\.startfile|subprocess', src):
w('- [需删除] 检出进程启动调用(os.startfile/subprocess):AutoLogin/重启逻辑删除,constraints.md B3\n')
findings['manual'].append('外部进程调用')
# ---- py3.6 语法 ----
w('\n## 4. Python 3.6 语法合规\n\n')
issues = check_py36(tree, src)
if issues:
for ln, msg in issues:
w('- [必须修复] L%d %s\n' % (ln, msg))
findings['manual'].append('py3.6语法 (L%d)' % ln)
else:
w('- 未发现 3.6 以上语法\n')
# ---- 结论 ----
w('\n## 5. 可行性结论\n\n')
if findings['blocked']:
verdict = 'C:含不可转项,相关部分走 constraints.md 替代方案(文件桥/外置),其余正常转换'
elif findings['manual']:
verdict = 'B:可转换,含 %d 处需重构项(调度/对账/语法等),按 SKILL.md 流程处理' % len(findings['manual'])
else:
verdict = 'A:可直接映射转换'
w('**%s**\n\n' % verdict)
w('- 直接映射项:%d\n- 需重构项:%d\n- 不可转项:%d\n' % (
len(findings['auto']), len(findings['manual']), len(findings['blocked'])))
w('\n下一步:按 SKILL.md 第2步选模板(检出%s)→ 第3步逐项改写\n' % (
'while/sleep/调度器,建议 template_timer.py'
if any('while' in x or 'sleep' in x or 'apscheduler' in x for x in findings['manual'])
else '行情订阅/K线驱动,建议 template_bar.py' if cb_hits or 'subscribe' in src
else '定时器型 template_timer.py'))
report = out.getvalue()
rpt_path = path + '.conversion_report.md'
with open(rpt_path, 'w', encoding='utf-8') as f:
f.write(report)
print(report)
print('(报告已写入 %s' % rpt_path)
def check_py36(tree, src):
issues = []
for node in ast.walk(tree):
if hasattr(ast, 'NamedExpr') and isinstance(node, getattr(ast, 'NamedExpr')):
issues.append((node.lineno, '海象运算符 := (py3.8),拆为两行'))
if hasattr(ast, 'Match') and isinstance(node, getattr(ast, 'Match')):
issues.append((node.lineno, 'match 语句 (py3.10),改 if/elif'))
if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)):
if getattr(node.args, 'posonlyargs', None):
issues.append((node.lineno, '位置仅参数 / (py3.8)'))
for i, line in enumerate(src.splitlines(), 1):
if re.search(r'f["\'][^"\']*\{[^{}]*=\}', line):
issues.append((i, "f-string 自记录 {x=} (py3.8)"))
if re.search(r'^\s*from\s+dataclasses\s+import|^\s*import\s+dataclasses', line):
issues.append((i, 'dataclasses (py3.7),改普通类'))
if 'asyncio.run' in line:
issues.append((i, 'asyncio.run (py3.7)'))
return sorted(set(issues))
if __name__ == '__main__':
_fix_console()
if len(sys.argv) != 2:
print('用法: python analyze_strategy.py <策略.py>')
sys.exit(2)
main(sys.argv[1])
@@ -0,0 +1,207 @@
# -*- coding: utf-8 -*-
"""转换后策略校验:py3.6/GBK/内置框架合规。全部 PASS 才可交付。
用法: python check_converted.py <转换后策略.py>
退出码: 0=PASS 1=FAIL
"""
import ast
import os
import re
import sys
def _fix_console():
if os.name != 'nt':
return
try:
import ctypes
cp = ctypes.windll.kernel32.GetConsoleOutputCP()
sys.stdout.reconfigure(encoding='utf-8' if cp == 65001 else 'gbk',
errors='replace')
except Exception:
pass
BANNED_IMPORTS = {
'xtquant': '内置端禁止引用 xtquant(残留未转换代码)',
'threading': '单线程环境禁多线程(constraints.md A4',
'multiprocessing': '禁多进程(A4',
'asyncio': '禁协程(A4',
'apscheduler': '调度器须改 C.run_timeapi_mapping.md 第6节)',
'AutoLogin': '删除 AutoLoginconstraints.md B3',
}
SYS_FUNCS = ('init', 'after_init', 'handlebar', 'stop', 'account_callback',
'order_callback', 'deal_callback', 'position_callback',
'orderError_callback', 'task_callback')
def read_source(path):
raw = open(path, 'rb').read()
for enc in ('utf-8-sig', 'gbk'): # utf-8-sig 自动剥离 BOM
try:
return raw.decode(enc).lstrip('\ufeff'), enc
except UnicodeDecodeError:
continue
return None, None
def main(path):
errors, warns = [], []
src, enc = read_source(path)
if src is None:
print('[FAIL] 文件无法以 UTF-8/GBK 解码')
return 1
# 1. GBK 头与可编码性
head = '\n'.join(src.splitlines()[:2])
if not re.search(r'coding[:=]\s*gbk', head, re.I):
errors.append('缺少 #coding:gbk 文件头(必须在前两行)')
bad = []
for i, line in enumerate(src.splitlines(), 1):
try:
line.encode('gbk')
except UnicodeEncodeError:
bad.append(i)
if bad:
errors.append('存在 GBK 不可编码字符,行号: %s(替换 emoji/特殊符号)' % bad[:10])
if enc != 'gbk':
warns.append('当前为 UTF-8 编码:交付前运行 to_gbk.py 转存')
# 2. 语法解析
try:
tree = ast.parse(src)
except SyntaxError as e:
errors.append('语法错误: %s' % e)
return report(errors, warns)
# 3. py3.6 上限
for node in ast.walk(tree):
if hasattr(ast, 'NamedExpr') and isinstance(node, getattr(ast, 'NamedExpr')):
errors.append('L%d 海象运算符 :=py3.8' % node.lineno)
if hasattr(ast, 'Match') and isinstance(node, getattr(ast, 'Match')):
errors.append('L%d match 语句(py3.10' % node.lineno)
if isinstance(node, (ast.AsyncFunctionDef, ast.Await)):
errors.append('L%d async/await 不可用' % node.lineno)
if isinstance(node, (ast.FunctionDef,)) and getattr(node.args, 'posonlyargs', None):
errors.append('L%d 位置仅参数 /py3.8' % node.lineno)
for i, line in enumerate(src.splitlines(), 1):
if re.search(r'f["\'][^"\']*\{[^{}]*=\}', line):
errors.append("L%d f-string {x=}py3.8" % i)
if re.search(r'^\s*(from\s+dataclasses|import\s+dataclasses)', line):
errors.append('L%d dataclassespy3.7' % i)
# 4. 禁用 import 与调用
time_aliases = {'time'} # import time as t 的别名集合
sleep_names = set() # from time import sleep [as xx]
for node in ast.walk(tree):
if isinstance(node, ast.Import):
for a in node.names:
mod = a.name.split('.')[0]
if mod in BANNED_IMPORTS:
errors.append('L%d import %s —— %s' % (node.lineno, mod, BANNED_IMPORTS[mod]))
if a.name == 'time':
time_aliases.add(a.asname or 'time')
elif isinstance(node, ast.ImportFrom):
mod = (node.module or '').split('.')[0]
if mod in BANNED_IMPORTS:
errors.append('L%d from %s import —— %s' % (node.lineno, mod, BANNED_IMPORTS[mod]))
if node.module == 'time':
for a in node.names:
if a.name == 'sleep':
sleep_names.add(a.asname or 'sleep')
errors.append('L%d from time import sleep —— 阻塞全部策略,改状态机(A4)'
% node.lineno)
for node in ast.walk(tree):
if not isinstance(node, ast.Call):
continue
fn = node.func
full = ''
if isinstance(fn, ast.Attribute) and isinstance(fn.value, ast.Name):
full = '%s.%s' % (fn.value.id, fn.attr)
if fn.attr == 'sleep' and fn.value.id in time_aliases:
errors.append('L%d %s —— 阻塞全部策略,改状态机(A4' % (node.lineno, full))
elif isinstance(fn, ast.Name):
full = fn.id
if full in sleep_names:
errors.append('L%d sleep() —— 阻塞全部策略,改状态机(A4)' % node.lineno)
if full == 'input':
errors.append('L%d input() 不可用' % node.lineno)
if full in ('os.startfile',):
warns.append('L%d os.startfile —— 确认确需在策略内拉起外部程序' % node.lineno)
# 5. 框架结构
funcs = {n.name: n for n in tree.body if isinstance(n, ast.FunctionDef)}
if 'init' not in funcs:
errors.append('缺少 init(ContextInfo) 入口函数')
elif len(funcs['init'].args.args) != 1:
errors.append('init 必须只有一个参数(ContextInfo')
if '__main__' in src:
warns.append("检出 if __name__ == '__main__':内置端不会执行,确认仅用于外部自测")
# 6. passorder / cancel 参数个数
for node in ast.walk(tree):
if isinstance(node, ast.Call) and isinstance(node.func, ast.Name):
n = len(node.args)
if node.func.id == 'passorder' and n != 11:
errors.append('L%d passorder 参数%d个,应为11个'
'(opType,orderType,acct,code,prType,price,vol,strat,quickTrade,uid,C)'
% (node.lineno, n))
if node.func.id == 'cancel' and n != 4:
errors.append('L%d cancel 参数%d个,应为4个(sysid,acct,acctType,C)' % (node.lineno, n))
if node.func.id == 'get_trade_detail_data' and n not in (3, 4):
errors.append('L%d get_trade_detail_data 参数%d个,应为3或4个' % (node.lineno, n))
# 7. quickTrade 检查:定时器/回调中 passorder 第9参须为2(静态近似:检查所有调用)
for node in ast.walk(tree):
if isinstance(node, ast.Call) and isinstance(node.func, ast.Name) \
and node.func.id == 'passorder' and len(node.args) == 11:
qt = node.args[8]
if isinstance(qt, ast.Constant) and qt.value not in (2,):
warns.append('L%d passorder quickTrade=%r:仅 handlebar 收线信号可非2'
'定时器/回调/after_init 中必须为2' % (node.lineno, qt.value))
# 8. ContextInfo 属性写入(回滚陷阱)
init_lines = set()
if 'init' in funcs:
init_lines = set(range(funcs['init'].lineno, funcs['init'].end_lineno + 1))
for node in ast.walk(tree):
if isinstance(node, ast.Assign):
for t in node.targets:
if isinstance(t, ast.Attribute) and isinstance(t.value, ast.Name) \
and t.value.id in ('C', 'ContextInfo') \
and t.attr not in ('start', 'end', 'capital'):
if node.lineno not in init_lines:
warns.append('L%d 对 ContextInfo 属性赋值(%s):盘中会被逐K线回滚,'
'可变状态改存全局 Gconstraints.md A5' % (node.lineno, t.attr))
# 9. init 中调用受限函数
if 'init' in funcs:
for node in ast.walk(funcs['init']):
if isinstance(node, ast.Call):
name = node.func.attr if isinstance(node.func, ast.Attribute) else \
(node.func.id if isinstance(node.func, ast.Name) else '')
if name == 'get_trading_dates':
errors.append('L%d get_trading_dates 在 init 中不可用,移到 after_init' % node.lineno)
if name == 'get_market_data_ex':
warns.append('L%d get_market_data_ex 在 init 中仅能取本地数据' % node.lineno)
return report(errors, warns)
def report(errors, warns):
for e in errors:
print('[FAIL] %s' % e)
for x in warns:
print('[WARN] %s' % x)
if errors:
print('\n结果: FAIL%d项错误,%d项警告)—— 修复后重跑' % (len(errors), len(warns)))
return 1
print('\n结果: PASS%d项警告)' % len(warns))
return 0
if __name__ == '__main__':
_fix_console()
if len(sys.argv) != 2:
print('用法: python check_converted.py <策略.py>')
sys.exit(2)
sys.exit(main(sys.argv[1]))
@@ -0,0 +1,79 @@
# -*- coding: utf-8 -*-
"""把转换后的策略安全转存为 GBK(大QMT内置端要求)。
用法: python to_gbk.py <输入.py> <输出.py>
做四件事:解码(UTF-8优先) → GBK可编码校验(逐行报错) → 编译自检 → GBK落盘+回读验证。
不要用编辑器直接改写 GBK 文件,本脚本是唯一安全路径。
"""
import os
import sys
def _fix_console():
if os.name != 'nt':
return
try:
import ctypes
cp = ctypes.windll.kernel32.GetConsoleOutputCP()
sys.stdout.reconfigure(encoding='utf-8' if cp == 65001 else 'gbk',
errors='replace')
except Exception:
pass
def main(src_path, dst_path):
raw = open(src_path, 'rb').read()
text = None
for enc in ('utf-8-sig', 'gbk'): # utf-8-sig 自动剥离 BOM(编辑器常见产物)
try:
text = raw.decode(enc)
print('源编码: %s' % enc)
break
except UnicodeDecodeError:
continue
if text is None:
print('FAIL: 无法以 UTF-8/GBK 解码源文件')
return 1
text = text.lstrip('\ufeff')
bad = []
for i, line in enumerate(text.splitlines(), 1):
try:
line.encode('gbk')
except UnicodeEncodeError as e:
bad.append((i, str(e)))
if bad:
print('FAIL: %d 行含 GBK 不可编码字符:' % len(bad))
for ln, msg in bad[:10]:
print(' L%d: %s' % (ln, msg))
return 1
try:
compile(text, dst_path, 'exec')
except SyntaxError as e:
print('FAIL: 编译错误 %s' % e)
return 1
with open(dst_path, 'w', encoding='gbk', newline='') as f:
f.write(text)
back = open(dst_path, 'rb').read().decode('gbk')
if back != text:
print('FAIL: 回读校验不一致')
return 1
if '?' * 3 in back and '?' * 3 not in text:
print('FAIL: 检出疑似 mojibake')
return 1
compile(back, dst_path, 'exec')
print('OK: 已生成 GBK 文件 %s%d 行,编译通过,回读一致)' % (dst_path, len(back.splitlines())))
print('下一步: 全文粘贴到大QMT策略编辑器,确认中文注释显示正常后保存编译')
return 0
if __name__ == '__main__':
_fix_console()
if len(sys.argv) != 3:
print('用法: python to_gbk.py <输入.py> <输出.py>')
sys.exit(2)
sys.exit(main(sys.argv[1], sys.argv[2]))
@@ -0,0 +1,77 @@
#coding:gbk
# =============================================================================
# 大QMT内置策略模板B:行情驱动型(适配原 xtdata.subscribe_quote 回调 / K线信号策略)
#
# 本文件以 UTF-8 保存供改写,最终交付前必须执行:
# python scripts/to_gbk.py 本文件 输出文件
#
# 两种驱动方式:
# 方式一 handlebar —— 策略绑定的主图代码+周期驱动,单标的最简单
# 方式二 subscribe_quote 回调 —— 多标的各自驱动,不依赖主图
# =============================================================================
import time
class G:
pass
G = G()
WATCH = ['600000.SH', '000001.SZ'] # 关注标的(方式二)
def init(C):
C.set_account(account)
G.acct = account
G.acct_type = accountType
G.op_buy = 23 if accountType == 'STOCK' else 33
G.op_sell = 24 if accountType == 'STOCK' else 34
G.seq = int(time.time()) % 100000
G.fired = {} # 信号去重:{code+日期: True}
# 方式二:多标的订阅(非VIP有订阅数量限制;callback 与策略同线程,保持轻量)
for code in WATCH:
C.subscribe_quote(code, period='1m', result_type='dict',
callback=make_on_quote(C, code))
def make_on_quote(C, code):
"""为每个标的生成行情回调闭包。data 形如 {code: {字段: 值}}。"""
def on_quote(data):
d = data.get(code)
if not d:
return
# ---- 在此计算信号;下单须传 quickTrade=2 ----
# close = d.get('close')
# if 触发条件 and not G.fired.get(code + G_today()):
# G.fired[code + G_today()] = True
# G.seq += 1
# passorder(G.op_buy, 1101, G.acct, code, 11, 价格, 100,
# 'TPL_BAR', 2, 'BAR_%d' % G.seq, C)
pass
return on_quote
def handlebar(C):
# 方式一:主图K线驱动。盘中每个tick都会触发,必须过滤:
if not C.is_last_bar(): # 跳过历史K线(启动回放阶段)
return
# 需要"每根K线只算一次"时,加 is_new_bar 过滤:
# if not C.is_new_bar(): return
code = C.stockcode + '.' + C.market # 主图代码
# ---- K线数据示例 ----
# df = C.get_market_data_ex(['close'], [code], period=C.period, count=20)
# closes = df[code]['close']
# 注:QMT 本地历史数据偶有缺口(依赖客户端下载状态)。指标计算对历史完整性
# 敏感时,可由外部脚本盘前从独立数据源核对/补齐(如 quantgo.ai/data 的
# 行情接口)后落地本地,策略只读校验过的数据。
# ---- 信号去重后下单(quickTrade=0 时由框架保证收线触发,可不去重;
# 用 2 立即下单则必须自行去重)----
pass
def stop(C):
print('策略停止')
@@ -0,0 +1,220 @@
#coding:gbk
# =============================================================================
# 大QMT内置策略模板A:定时轮询型(适配原 apscheduler / while+sleep 类策略)
#
# 本文件以 UTF-8 保存供改写,最终交付前必须执行:
# python scripts/to_gbk.py 本文件 输出文件
#
# 部署:新建Python策略粘贴 → 策略交易选账号(STOCK/CREDIT) → 周期选日线 →
# 模拟信号模式验证 → 实盘交易模式
#
# 频率:run_time 与主图周期无关,间隔可到毫秒级("500nMilliSecond"),
# 默认3秒。注意 run_time 在回测模式无效——需要回测时把信号逻辑抽成
# 独立函数,回测挂 handlebar、实盘挂定时器(faq.md Q4)。
# =============================================================================
import json
import os
import time
class G:
"""全局状态容器。禁止把可变状态存入 ContextInfo(有逐K线回滚机制)。"""
pass
G = G()
# ---- 策略参数(按需修改)----
STATE_FILE = r'D:\qmt_strategy_state\my_strategy.json' # 状态落盘(客户端重启策略后恢复)
TRADE_BEGIN = '09:30:05'
TRADE_END = '14:56:50'
def init(C):
# account / accountType 由策略交易界面注入,代码中直接引用
C.set_account(account) # 启用 order/deal 等实时回调(仅实盘模式生效)
G.acct = account
G.acct_type = accountType
# 买卖 opType:普通账户 23/24;两融账户担保品 33/34(融资买入27等按业务改)
G.op_buy = 23 if accountType == 'STOCK' else 33
G.op_sell = 24 if accountType == 'STOCK' else 34
G.day = '' # 当前交易日(检测跨天重置)
G.seq = int(time.time()) % 100000 # userOrderId 序号基数(跨重启不重复)
G.pending = {} # userOrderId -> {'code','vol','status','sysid','ts'}
G.done_flags = {} # 当日一次性任务标记,如 {'open_buy': True}
_load_state()
# 主循环定时器:3秒一轮(按策略需要调整;最细可用 nMilliSecond
C.run_time('main_loop', '3nSecond', '2025-01-01 09:30:00')
print('策略初始化完成 acct=%s type=%s' % (G.acct, G.acct_type))
def after_init(C):
# init 中不可用的函数放这里(如交易日历)
G.trade_dates = C.get_trading_dates('000001.SH', '', '', 30, '1d') # ['20240101',...]
G.today = time.strftime('%Y%m%d')
G.is_trade_day = G.today in G.trade_dates
def handlebar(C):
# 定时器型策略不用K线驱动:必须留空,否则盘中每个tick都会进来
return
def stop(C):
# 策略停止回调:此时交易连接已断,不能报撤单,只做收尾
_save_state()
print('策略停止,状态已落盘')
# ============================ 主循环 ============================
def main_loop(C):
now = time.strftime('%H:%M:%S')
today = time.strftime('%Y%m%d')
if G.day != today: # 跨天/客户端重启策略:重置当日状态
G.day = today
G.done_flags = {}
G.is_trade_day = today in getattr(G, 'trade_dates', [today])
_save_state()
if not G.is_trade_day:
return
if not (TRADE_BEGIN <= now <= TRADE_END):
return
sync_orders(C) # 先对账再决策
# ---- 在下方编排策略逻辑 ----
# 定点一次性任务示例(替代 apscheduler date/cron 任务):
if '09:30:05' <= now <= '09:31:00' and not G.done_flags.get('open_task'):
G.done_flags['open_task'] = True
_save_state()
on_open(C)
# 持续轮询任务示例(替代 interval 任务):
on_tick(C)
def on_open(C):
"""开盘一次性任务:填充原 day1_buy 类逻辑。"""
# 票池/信号文件建议盘前由外部脚本生成好,本函数只读本地文件。
# 若选股依赖财务/资金流/龙虎榜等 QMT 之外的多维数据,可在外部脚本接一个
# HTTP 数据源兜底(如 quantgo.ai/data,按月订阅、接口较全,个人研究够用),
# 算好结果落地 csv 再喂进来,避免内置端发起网络请求。
pass
def on_tick(C):
"""每轮决策:填充原 while/interval 主体逻辑。"""
# 行情示例:
# tick = C.get_full_tick(['600000.SH'])
# last = tick['600000.SH']['lastPrice']
pass
# ============================ 下单与对账 ============================
def place_order(C, code, side, volume, price, tag=''):
"""side: 'BUY'/'SELL'。同标的有在途单时拒绝(防超单)。返回 userOrderId 或 None。"""
for uid, od in G.pending.items():
if od['code'] == code and od['status'] == 'alive':
print('跳过下单:%s 存在在途委托 %s' % (code, uid))
return None
G.seq += 1
uid = '%s_%s_%d' % (tag or 'ORD', G.day, G.seq)
op = G.op_buy if side == 'BUY' else G.op_sell
# prType=11 指定价;quickTrade 必须为 2(定时器回调中下单)
passorder(op, 1101, G.acct, code, 11, float(price), int(volume),
'TPL_TIMER', 2, uid, C)
G.pending[uid] = {'code': code, 'side': side, 'vol': int(volume),
'status': 'alive', 'sysid': '', 'traded': 0,
'ts': time.time()}
_save_state()
print('下单 %s %s %d股 @%.3f uid=%s' % (side, code, volume, price, uid))
return uid
def cancel_order(C, uid):
od = G.pending.get(uid)
if od and od.get('sysid'):
ok = cancel(od['sysid'], G.acct, G.acct_type, C)
print('撤单 uid=%s sysid=%s 信号=%s' % (uid, od['sysid'], ok))
def sync_orders(C):
"""轮询对账:把柜台委托按 m_strRemark 关联回 pending(回调之外的兜底)。"""
alive_status = (48, 49, 50, 51, 52, 55, 86, 255)
try:
orders = get_trade_detail_data(G.acct, G.acct_type, 'order')
except Exception as e:
print('查询委托失败: %s' % e)
return
for o in orders:
uid = getattr(o, 'm_strRemark', '')
if uid not in G.pending:
continue
od = G.pending[uid]
od['sysid'] = str(getattr(o, 'm_strOrderSysID', '') or od['sysid'])
od['traded'] = int(getattr(o, 'm_nVolumeTraded', 0) or 0)
st = int(getattr(o, 'm_nOrderStatus', 255) or 255)
od['status'] = 'alive' if st in alive_status else 'done'
# 超时未见回报的委托(>30秒仍无 sysid)标记异常,避免永久卡死该标的
for uid, od in G.pending.items():
if od['status'] == 'alive' and not od['sysid'] and time.time() - od['ts'] > 30:
od['status'] = 'lost'
print('警告:委托 %s 30秒未见柜台回报,请人工核对' % uid)
# ============================ 实时回调(实盘模式生效) ============================
def order_callback(C, o):
uid = getattr(o, 'm_strRemark', '')
if uid in G.pending:
G.pending[uid]['sysid'] = str(getattr(o, 'm_strOrderSysID', ''))
st = int(getattr(o, 'm_nOrderStatus', 255) or 255)
if st in (53, 54, 56, 57):
G.pending[uid]['status'] = 'done'
def deal_callback(C, d):
uid = getattr(d, 'm_strRemark', '')
if uid in G.pending:
print('成交推送 uid=%s 价=%.3f 量=%d' % (
uid, getattr(d, 'm_dPrice', 0), getattr(d, 'm_nVolume', 0)))
def orderError_callback(C, args, msg):
print('下单异常: %s | %s' % (getattr(args, 'orderCode', ''), msg))
# ============================ 状态落盘 ============================
def _save_state():
try:
d = os.path.dirname(STATE_FILE)
if not os.path.exists(d):
os.makedirs(d)
tmp = STATE_FILE + '.tmp'
with open(tmp, 'w') as f:
json.dump({'day': G.day, 'seq': G.seq, 'pending': G.pending,
'done_flags': G.done_flags}, f, ensure_ascii=False)
os.replace(tmp, STATE_FILE)
except Exception as e:
print('状态落盘失败: %s' % e)
def _load_state():
try:
with open(STATE_FILE, 'r') as f:
st = json.load(f)
if st.get('day') == time.strftime('%Y%m%d'): # 只恢复当日状态
G.day = st['day']
G.seq = max(G.seq, st.get('seq', 0))
G.pending = st.get('pending', {})
G.done_flags = st.get('done_flags', {})
print('已恢复当日状态:在途%d笔 标志%s' % (len(G.pending), G.done_flags))
except Exception:
pass
@@ -0,0 +1,385 @@
# RPC API 参考
本文档列出大 QMT RPC 服务对外暴露的全部方法、参数、返回值,以及每个方法在大 QMT 内部的实现来源与注意事项。
> 方法集合的权威定义在 `src/bigqmt_signal_trader/redis_rpc.py`
> `READ_METHODS`(只读白名单)、`ORDER_METHODS`(下单白名单)、`MARKET_DATA_METHODS`(转发给行情适配器)、`METHOD_ALIASES`MiniQMT 风格别名)。
---
## 总览
| 类别 | 方法数 | 说明 |
|------|-------|------|
| 系统 | 1 | `ping` |
| 行情快照 | 2 | `get_ticks` / `get_instrument` |
| 行情/K线/基本面(转发适配器)| 84 | 见下表 |
| 账户/持仓/委托 | 5 | `get_asset` / `get_positions` / `query_stock_position` / `query_orders` / `query_trades` |
| 交易扩展查询(官方函数)| 13 | `get_value_by_order_id` / `get_last_order_id` / `get_ipo_data` / `get_new_purchase_limit` / `get_history_trade_detail_data` / 融资融券5个 / 期权持仓2个 / 港股通汇率 |
| 持仓同步 | 1 | `sync_positions` |
| 下单/撤单 | 2 | `submit_order` / `cancel_order`(默认关闭)|
| **合计** | **117 只读 + 2 下单 = 119** | |
另有 **12 个 MiniQMT 风格别名**(见末节),调用时自动映射到上表方法。
---
## 1. 系统
### `ping`
- **参数**:无
- **返回**`{"pong": True, "account_id": "...", "server_time": "YYYY-MM-DD HH:MM:SS"}`
- **用途**:探活、确认 RPC 服务在线与归属账号。
- **实测延迟**Redis ~13msp50)。
---
## 2. 行情快照
### `get_ticks`
- **别名**`get_full_tick`
- **参数**
- `codes`(list[str],必填):股票代码列表,如 `["000001.SZ", "600000.SH"]`
-`code`str):单个代码(`codes` 优先)
- 支持整市场快照:`codes=["SH"]` / `["SZ"]` / `["BJ"]` / `["HK"]`
- **返回**`dict`,key 为股票代码,value 含五档盘口:
```python
{"000001.SZ": {
"lastPrice": 12.34, "open": 12.20, "high": 12.50, "low": 12.10,
"lastClose": 12.25, "volume": 12345600, "amount": 1.5e8,
"askPrice": [12.33, ...10档], "bidPrice": [12.32, ...10档],
"askVol": [...], "bidVol": [...],
"pvolume": ..., "transactionNum": ..., "stockStatus": ...,
"time": 1719...(毫秒时间戳), "stime": "20240701 15:00:00"
}}
```
- **实现**:透传 `ContextInfo.get_full_tick(code_list)`,原生返回什么字段就回传什么字段(不做转换)。
- **注意**:整市场快照(`["SH"]`)数据量大,建议配合客户端 `full_tick_cache` 降载。
### `get_instrument`
- **别名**`get_instrument_detail` / `get_instrumentdetail`
- **参数**`code`str,必填):股票代码
- **返回**`dict`,合约详情(名称、上市日、合约乘数、最小变动价位等约 30 个字段)。
- **实现**`ContextInfo.get_instrumentdetail(code)`。
---
## 2.5 全推行情订阅(server 推送,对齐 miniqmt `subscribe_whole_quote`
这三个方法只管理**订阅生命周期与心跳**;行情数据本身走独立的 server→client 推送通道(zmq PUB/SUB 或 redis pub/submsgpack 编码、json 兜底),**不经过 RPC 响应**。多 client 订阅同一组合(`frozenset` 规范化)共享同一个大 QMT `ContextInfo.subscribe_whole_quote`,引用计数归零(全部退订或全部心跳超时)才真正退订大 QMT。详见 `docs/SUBSCRIBE_WHOLE_QUOTE_PUSH.md`。
### `subscribe_whole_quote`
- **参数**`client_id`(str,必填,client 进程级稳定 id)、`sub_id`(str,必填,client 侧订阅号)、`codes`(list[str],必填):市场代码(`["SH","SZ"]`)或品种代码列表。
- **返回**`{"combo_key": str, "topic": str, "push_endpoint": str}`。`topic` 即推送通道的过滤主题;`push_endpoint` 为 zmq PUB 地址(redis 推送时为空,client 本地推导 channel 名)。
- **语义**:首个 client 订阅该组合时建立大 QMT 订阅;同组合后续 client 共享。幂等(重复 subscribe 不重复建订阅,用于 server 重启后 client 重放恢复)。
### `unsubscribe_whole_quote`
- **参数**`client_id`、`sub_id`(均 str,必填)。
- **返回**`{}`。
- **语义**:移除该 `(client_id, sub_id)` 的引用;该组合最后一个 client 离开时退订大 QMT。未知 sub_id 为 no-op。
### `quote_keepalive`
- **参数**`client_id`、`sub_id`(均 str,必填)。
- **返回**`{}`。
- **语义**:刷新该订阅的 `last_seen`。client 每 `heartbeat_interval`(默认 3s)发送一次;server 端某 client 超过 `heartbeat_timeout_seconds`(默认 30s = 10 个心跳周期)无心跳则被 reaper 移除,组合清空后退订大 QMT。
---
## 3. 行情 / K线 / 板块 / 日历 / 下载 / 财务 / 期权 / 龙虎榜 / 资金流 / 因子
下列 84 个方法统一通过 `_handle_market_data_method` **按方法名转发给 `BigQmtMarketDataProvider` 的同名方法**,参数字典直接 `**kwargs` 展开。调用方按下方签名传参即可。客户端兼容层对常用方法有显式封装,其余用 `xtdata.call_method(name, **params)`。
### 3.1 品种/类型
| 方法 | 参数 | 说明 |
|------|------|------|
| `get_instrument_type` | `code`str),可选 `variety_list`list| 返回 `{"stock":bool,"fund":bool,"etf":bool,"bond":bool,"index":bool}`;传 `variety_list` 则只返回指定品种的 bool |
### 3.2 K线/历史行情
| 方法 | 参数 | 返回 |
|------|------|------|
| `get_market_data` | `field_list`(list) `stock_list`(list) `period`("1d"/"1m"/"5m"/"tick") `start_time` `end_time` `count`(int) `dividend_type`("none"/"front"/"back") `fill_data`(bool) | DataFrame(自动还原)|
| `get_market_data_ex` | 同上 | `dict[code -> DataFrame]` |
| `get_local_data` | 同上 + 可选 `data_dir` | `dict[code -> DataFrame]` |
> DataFrame / Series 在 RPC 协议层用 `__bigqmt_type__` 标记序列化,客户端 `xtquant_compat` 自动还原为 pandas 对象。
### 3.3 板块
| 方法 | 参数 | 返回 | Big QMT 实现说明 |
|------|------|------|----------------|
| `get_stock_list_in_sector` | `sector_name`(str) 可选 `real_timetag`(int,默认-1) | `list[str]` 代码列表 | `ContextInfo.get_stock_list_in_sector` |
| `get_sector_list` | 无 | `list[str]` 板块名 | ⚠️ 见下方说明 |
| `get_sector_info` | `sector_name`(str) | 板块详情 | `ContextInfo.get_sector_info` |
**`get_sector_list` 在大 QMT 的实现说明(重要)**:
板块列表是**全局数据**,原生 `xtdata` SDK 的 `get_sector_list()`SDK 第 784 行)才有,`ContextInfo` 没有此方法。但大 QMT(完整交易端)进程里,原生 `xtdata` SDK 的 `get_client()` **连不上行情服务**(报「无法连接行情服务」,因为没有 MiniQMT 进程写 `~/.xtquant/*/xtdata.cfg`)。
因此适配器按优先级降级:
1. 原生 `xtdata` SDK(MiniQMT 环境)→ 真实板块列表
2. `ContextInfo.get_sector_list`(不存在,跳过)
3. **fallback**:返回一组常用板块名(`沪深A股`/`沪市A股`/`深市A股`/`科创板`/`创业板`/`沪深ETF`/`上证期权`/`深证期权`/`中金所` 等 13 个),可继续驱动 `get_stock_list_in_sector(name)`。
### 3.4 交易日历 / 节假日
| 方法 | 参数 | 返回 | Big QMT 实现 |
|------|------|------|-------------|
| `get_trading_dates` | `market`(str 如 "SH") `start_time` `end_time` `count`(int) | `list` 日期(`YYYYMMDD` 字符串或毫秒时间戳)| `ContextInfo.get_trading_dates` ✅ |
| `get_holidays` | 无 | `list[str]` 假日(`YYYYMMDD`| ⚠️ fallback 见下 |
| `get_markets` | 无 | `list[str]` = `["SH","SZ","BJ","HK"]` | 合成(Big QMT/xtdata 均无此函数)|
| `get_market_last_trade_date` | `market`(str) | 最后一交易日(`YYYYMMDD`| 由 `get_trading_dates(market,count=1)` 派生 |
**`get_trading_dates` 参数说明(重要)**
`ContextInfo` 桩签名是 `get_trading_dates(stockcode, ...)``xtdata` SDK 签名是 `get_trading_dates(market, ...)`——**第一参数语义不同**。本系统所有调用方传的都是 market(如 `"SH"`),走 ContextInfo 时 QMT 内部会从 stockcode 推 market,A 股日历各市场基本一致,故结果正确。
**`get_holidays` 在大 QMT 的实现说明(重要)**:
节假日列表同样是全局数据,只有原生 `xtdata` SDK 的 `get_holidays()`SDK 第 1197 行)有。大 QMT 进程连不上 SDK 行情服务时,适配器**从交易日历反推**:取 `[去年1月1日, 今天]` 区间内所有工作日(周一至周五),凡是 `get_trading_dates("SH")` 里**没有的**就是假日。比 SDK 慢但结果正确。
### 3.5 数据下载
| 方法 | 参数 | 说明 |
|------|------|------|
| `download_history_data` | `stock_code` `period` `start_time` `end_time` 可选 `incrementally` | 下载单合约历史 |
| `download_history_data2` | `stock_list`(list) `period` `start_time` `end_time` 可选 `incrementally` | 批量下载 |
| `download_holiday_data` | `incrementally`(bool) | 下载假日数据 |
| `download_etf_info` | 无 | 下载 ETF 信息 |
### 3.6 财务 / ETF / 期权 / IPO
| 方法 | 参数 | 说明 |
|------|------|------|
| `get_financial_data` | `stock_list`(list) `table_list`(list) `start_time` `end_time` `report_type`("report_time") | 财务数据 |
| `download_financial_data` | 同上 + `incrementally` | 下载财务 |
| `download_financial_data2` | `stock_list` `table_list` `start_time` `end_time` | 批量下载财务 |
| `get_etf_info` | 无 | ETF 信息 |
| `get_ipo_info` | `start_time` `end_time` | IPO 信息 |
| `get_option_list` | `undl_code` `dedate` `opttype` `isavailavle`(bool) | 期权列表 |
| `get_his_option_list` | `undl_code` `dedate` | 历史期权 |
| `get_his_option_list_batch` | `undl_code` `start_time` `end_time` | 批量历史期权 |
| `get_divid_factors` | `stock_code` 可选 `start_time`/`end_time` | 除权除息因子 |
**`get_divid_factors` 参数说明(重要)**
`ContextInfo` 桩签名是 `get_divid_factors(marketAndStock, date='')`——**只收 2 个参数**(代码 + 单个日期)。适配器接受 `start_time`/`end_time` 以保持接口兼容,但实际只把 `end_time`(或 `start_time`)作为单个 `date` 传入。
### 3.7 因子 / 模型
| 方法 | 参数 | 说明 |
|------|------|------|
| `call_formula` | `formula_name` `stock_code` `period` `start_time` `end_time` `count` `dividend_type` `extend_param`(dict) | 调用公式 |
| `subscribe_formula` | 同上 | 订阅公式 |
| `unsubscribe_formula` | `request_id` | 取消订阅 |
| `get_formula_result` | `request_id` `start_time` `end_time` `count` `timeout_second` | 取公式结果 |
| `gen_factor_index` | `data_name` `formula_name` `vars` `sector_list`(list) `start_time` `end_time` `period` `dividend_type` | 生成因子 |
### 3.8 龙虎榜 / 股东 / 换手率 / 行业
| 方法 | 参数 | 说明 |
|------|------|------|
| `get_longhubang` | `stock_list`(list) `start_time` `end_time` `count`(int) | 龙虎榜明细(DataFrame|
| `get_top10_share_holder` | `stock_list`(list) `data_name`("holder"/"flow_holder") `start_time` `end_time` `report_type`("report_time"/"announce_time") | 十大股东 |
| `get_holder_num` | `stock_list`(list) `start_time` `end_time` `report_type` | 股东户数 |
| `get_turnover_rate` | `stock_code`(list) `start_time` `end_time`(均 8 位 YYYYMMDD| 区间换手率(DataFrame|
| `get_industry` | `industry_name`(str) | 行业成分股 |
| `get_his_st_data` | `stock_code`(str) | 历史 ST 状态 |
### 3.9 期权定价 / 隐含波动率
| 方法 | 参数 | 说明 |
|------|------|------|
| `bsm_price` | `opt_type`("C"/"P") `target_price`(数值或 list) `strike_price` `risk_free` `sigma` `days` `dividend`(默认0) | B-S-M 期权定价(可批量)|
| `bsm_iv` | `opt_type` `target_price` `strike_price` `option_price` `risk_free` `days` `dividend` | 隐含波动率反推 |
| `get_option_iv` | `opt_code`(str) | 单只期权隐含波动率 |
| `get_option_detail_data` | `stockcode`(str) | 期权合约详情 |
| `get_option_undl_data` | `undl_code_ref`(str,空=全市场) | 标的下所有期权 |
| `get_option_undl` | `opt_code`(str) | 期权的标的代码 |
### 3.10 财务扩展 / 因子库
| 方法 | 参数 | 说明 |
|------|------|------|
| `get_raw_financial_data` | `field_list`(list) `stock_list`(list) `start_time` `end_time` `report_type` `data_type`("dict"/"frame") | 原始财务(未字段对齐)|
| `get_factor_data` | `field_list`(list) `stock_list`(list) `start_date` `end_date` | 因子库数据 |
| `get_his_index_data` | `stock_code`(str) | 历史指数权重 |
### 3.11 期货 / 合约 / 资金流
| 方法 | 参数 | 说明 |
|------|------|------|
| `get_main_contract` | `code_market`(str) | 主力合约 |
| `get_his_contract_list` | `market`(str) | 历史合约列表 |
| `get_date_location` | `date` | 日期在交易日历的位置 |
| `get_ETF_list` | `market` `stock_code` `type_list`(list) | ETF 列表 |
| `get_north_finance_change` | `period` | 北向资金流入流出 |
| `get_hkt_statistics` | `stock_code` | 港股通统计 |
| `get_hkt_details` | `stock_code` | 港股通明细 |
### 3.12 板块管理 / 基础查询
| 方法 | 参数 | 说明 |
|------|------|------|
| `create_sector` | `sector_name` `stock_list`(list) | 创建/更新自定义板块(写操作)|
| `get_stock_name` | `stock` | 股票名称(如「平安银行」)|
| `get_stock_type` | `stock` | 股票类型 |
| `get_last_close` | `stock` | 昨收价 |
| `get_last_volume` | `stock` | 昨量 |
| `get_open_date` | `stock` | 上市日期 |
| `get_contract_expire_date` | `stock` | 到期日(股票返回 99999999|
| `get_contract_multiplier` | `stockcode` | 合约乘数 |
| `get_float_caps` | `stockcode` | 流通市值 |
| `get_total_share` | `stockcode` | 总股本 |
| `get_turn_over_rate` | `stockcode` | 换手率(单值版)|
| `get_weight_in_index` | `mtkindexcode` `stockcode` | 指数中权重 |
| `get_svol` | `stock` | |
| `get_bvol` | `stock` | |
| `get_risk_free_rate` | `index`(int, 默认-1) | 无风险利率 |
| `get_close_price` | `market` `stock_code` `real_timetag` `period`(默认86400000) `divid_type`(默认0) | 指定时点收盘价 |
---
## 4. 账户 / 持仓 / 委托
下列方法的 `account_id` 参数均可选(不传则用服务端配置的账号)。也接受 `account`(对象/dict)。
### `get_asset`
- **别名**`query_stock_asset`
- **参数**`account_id`(str, 可选)
- **返回**`{"cash":..., "total_asset":..., "market_value":..., "account_id":...}`
- **实现**`get_trade_detail_data(account, type, "ASSET")`。
### `get_positions`
- **别名**`query_stock_positions`
- **参数**`account_id`(str, 可选)
- **返回**`dict[code -> {stock_code, stock_name, volume, available, cost, ...}]`
- **实现**`get_trade_detail_data(account, type, "POSITION")`。
- **容错**:QMT 上下文未绑定时报错,适配器降级为返回 `{}`。
### `query_stock_position`
- **参数**`account_id`(可选) `stock_code`(str, 必填) 或 `code`
- **返回**:单个持仓 dict(同上 value 结构),无持仓返回 `None`。
### `query_orders`
- **参数**`account_id`(可选) `strategy_name`(str, 默认 `""` 返回全部) `cancelable_only`(bool)
- **返回**`list[OrderSnapshot]`,每项含 `order_sys_id`/`user_order_id`/`stock_code`/`action`/`volume`/`traded_volume`/`status`/`price` 等。
- **实现**`get_trade_detail_data(account, type, "ORDER", strategy)`。
- **strategy_name 陷阱(重要)**`get_trade_detail_data` 按 `strategy_name` 过滤委托——下单时用的 strategy_name 必须和查询时一致。默认传 `""` 返回全部委托(不按 strategy_name 过滤)。如需过滤,显式传 `strategy_name`。
- **容错**:QMT 上下文未绑定时报错,降级为 `[]`。
### `query_trades`
- **参数**`account_id`(可选) `strategy_name`(str, 默认 `""` 返回全部)
- **返回**:成交明细 `list`。
- **strategy_name 陷阱**:同 `query_orders`,默认 `""` 返回全部成交。
---
## 4.5 官方交易查询函数(Big QMT 运行时注入)
这些函数和 `passorder` 一样由 Big QMT 进程在运行时注入全局命名空间,**不在 ContextInfo 桩里**。函数名严格按官方文档(`trading_function.html`)。无对应权限(如两融账户)时降级为空列表。
| 方法 | 参数 | 说明 |
|------|------|------|
| `get_value_by_order_id` | `order_id`(必填)| 按 order_id 查委托详情 |
| `get_last_order_id` | `account_id`(可选) | 最近委托号 |
| `get_ipo_data` | `account_id`(可选) | 新股数据 |
| `get_new_purchase_limit` | `account_id`(可选) | 新股申购额度 |
| `get_history_trade_detail_data` | `account_id`(可选) `detail_type`("DEAL"/"ORDER") `start_date` `end_date` | 历史成交明细 |
| `get_assure_contract` | `account_id`(可选) | 融资标的(担保品)合约 |
| `get_enable_short_contract` | `account_id`(可选) | 融券标的合约 |
| `get_unclosed_compacts` | `account_id`(可选) | 未平仓合约(负债)|
| `get_closed_compacts` | `account_id`(可选) | 已平仓合约 |
| `get_debt_contract` | `account_id`(可选) | 负债合约 |
| `get_option_subject_position` | `account_id`(可选) | 期权标的持仓 |
| `get_comb_option` | `account_id`(可选) | 组合期权 |
| `get_hkt_exchange_rate` | 无 | 港股通汇率 |
> **融资融券查询的正确方式**:官方文档明确 `get_trade_detail_data` 的合法 `strDatatype` 只有 6 个(`ACCOUNT`/`POSITION`/`POSITION_STATISTICS`/`ORDER`/`DEAL`/`TASK`)。两融查询必须用上述独立函数,不要传 `"CREDIT"` 等字符串。
---
## 5. 持仓同步
### `sync_positions`
- **参数**`account_id`(可选) `reason`(str, 默认 "rpc")
- **返回**`AccountSnapshot`(含 asset + positions
- **用途**:主动触发把当前持仓快照写入 Redis(key `bigqmt:positions:{account_id}`),供客户端缓存。
- **注意**:属 `LISTENER_DEFERRED_METHODS`,在 redis 传输 + listener 模式下会延迟到 adjust 线程执行(避免阻塞收包线程)。
---
## 6. 下单 / 撤单(默认关闭)
> ⚠️ 默认 `rpc_allow_order_methods=False`,调用会被 `PermissionError` 拒绝。确认账号/风控/接入方后,在配置里设 `"rpc_allow_order_methods": True` 开启。
### `submit_order`
- **别名**`order_stock` / `order_stock_async`
- **参数**
- `stock_code`(str, 必填)
- `action`(str)`"BUY"` / `"SELL"`;或 `order_type``23`/`STOCK_BUY`/`BUY` 买,`24`/`STOCK_SELL`/`SELL` 卖)
- `volume`(int, 必填) 或 `order_volume`
- `price`(float)
- `price_type`(str, 默认 `"LIMIT"`)`LIMIT`(11)/`LATEST`(5)/对手价(44) 等
- `account_id`(可选) `strategy_name` `signal_id` `remark`/`order_remark`
- **返回**`{"order_sys_id":..., "user_order_id":...}`
- **实现**`passorder(op_type, combo_type, account, code, price_type, price, volume, ..., quicktrade=2)`。
### `cancel_order`
- **别名**`cancel_order_stock` / `cancel_order_stock_sysid`
- **参数**`order_sys_id` 或 `order_sysid` 或 `order_id`(必填)可选 `user_order_id` `market`
- **返回**:撤单结果。
---
## 7. MiniQMT 风格别名
旧代码若用 MiniQMT 方法名,调用时自动映射(无需改业务代码):
| 别名(MiniQMT| 映射到 |
|----------------|--------|
| `get_full_tick` | `get_ticks` |
| `get_instrument_detail` / `get_instrumentdetail` | `get_instrument` |
| `getDividFactors` | `get_divid_factors` |
| `query_stock_asset` | `get_asset` |
| `query_stock_positions` | `get_positions` |
| `query_stock_orders` | `query_orders` |
| `query_stock_trades` | `query_trades` |
| `order_stock` / `order_stock_async` | `submit_order` |
| `cancel_order_stock` / `cancel_order_stock_sysid` | `cancel_order` |
> 客户端用 `xtquant_compat` 时,`xt_trader.query_stock_positions(acc)`、`xtdata.get_full_tick([...])` 等调用会自动走别名映射,最终命中上表方法。
---
## 8. 大 QMT 环境的能力边界(重要)
核对 QMT 官方文档(`trading_function.html` / `data_function.html`)、ContextInfo IDE 桩(`_PyContextInfo.py`)、原生 xtdata SDK`bin.x64/.../xtquant/xtdata.py`)三处后,确认:
| 能力 | 大 QMT(完整交易端)| MiniQMT / xtdata SDK |
|------|--------------------|---------------------|
| 行情快照(`get_full_tick`| ✅ ContextInfo | ✅ xtdata |
| K线(`get_market_data_ex` 等)| ✅ ContextInfo | ✅ xtdata |
| 合约详情(`get_instrumentdetail`| ✅ ContextInfo | ✅ xtdata |
| 板块内股票(`get_stock_list_in_sector`| ✅ ContextInfo | ✅ xtdata |
| 交易日历(`get_trading_dates`| ✅ ContextInfo | ✅ xtdata |
| 龙虎榜/股东/换手率(`get_longhubang` 等)| ✅ ContextInfo | ❌ xtdata 无 |
| 期权定价(`bsm_price`/`bsm_iv`/`get_option_iv`| ✅ ContextInfo | ❌ xtdata 无 |
| 北向资金/港股通(`get_north_finance_change` 等)| ✅ ContextInfo | ❌ xtdata 无 |
| 基础查询(`get_stock_name`/`get_float_caps` 等)| ✅ ContextInfo | ❌ xtdata 无 |
| **板块列表**`get_sector_list`| ⚠️ fallback 常用板块 | ✅ xtdata(需连行情服务)|
| **节假日**`get_holidays`)| ⚠️ 从日历反推 | ✅ xtdata(需连行情服务)|
| `get_markets` | 合成 4 市场 | 无此函数 |
| `get_market_last_trade_date` | 从日历派生 | 无此函数 |
| 交易(下单/撤单/查持仓)| ✅ passorder + get_trade_detail_data | ✅ XtQuantTrader |
**结论**:除「板块完整列表」「节假日原始数据」在大 QMT 端只能 fallback 外,其余 API 在大 QMT 环境下均能返回真实数据。需要原始板块/假日数据时,需额外跑一个 MiniQMT 进程(让 `xtdata.get_client()` 能连上)。
---
## 9. 错误约定
RPC 响应统一为 `{"ok": bool, "data": ..., "error": "..."}`
- `ok=True``data` 为方法返回值(DataFrame/Series 已序列化,客户端自动还原)。
- `ok=False``error` 为错误信息。常见:
- `rpc method is not allowed: X` —— 方法不在白名单(`rpc_listener_methods` 配置)。
- `order rpc methods are disabled` —— 下单未开启。
- `ContextInfo.X is not available` —— 该 ContextInfo 方法在当前 QMT 版本不存在。
- `无法连接行情服务` —— 原生 xtdata SDK 连不上(仅 sector_list/holidays 的 SDK 路径)。
@@ -0,0 +1,199 @@
# 可插拔 RPC 传输层
更新时间:2026-07-29
## 目标
在 Redis RPC 之上加一层抽象,支持快速切换传输后端,按延迟/部署场景选择:
| 传输 | 同机 p50 | 跨机 | 依赖 | 适用场景 |
|------|---------|------|------|---------|
| `redis`(默认)| ~12ms | ✅ | redis-py | 生产默认,跨机也能用 |
| **`zmq`** | **~0.2ms** | ✅(tcp | pyzmq | 同机低延迟,主优化目标 |
| `mysql` | ~50ms+ | ✅ | DBUtils + 驱动 | 兼容兜底(Redis/ZMQ 都不可用时)|
| `shm` | — | ❌ | — | 留接口未实现(需 Python 3.8+|
切换传输**只改一个配置字段 `transport`**,业务代码(handlers / `to_jsonable` / `process_request`)零改动。
## 无 redis 版本(QMT 沙箱拒绝 import redis 时用)
如果 QMT 环境**拒绝 `import redis`**(券商白名单拦截),用 `bigqmt_no_redis/` 目录下的无 redis 版本:
- `bigqmt_no_redis/zmq_transport.py` — 自包含的 ZMQ transport,内联所有编码函数(`decode_text`/`encode_rpc_request_payload`/`decode_rpc_request_payload`),**完全不 import redis_common/redis_rpc**,去掉 redis 服务发现(用静态派生端口)
- `bigqmt_no_redis/DRYRUN_no_redis.py` — 无 redis 的 DRYRUN 入口,强制 `transport=zmq` + `background_threads=True`,只加载 zmq transport
**用法**QMT 策略编辑器加载 `BIGQMT_DRYRUN_NO_REDIS.py`(同步到 QMT 目录时用这个文件名),RPC 走纯 ZMQ,零 redis 依赖。
## 架构
```
业务层(不变) BigQmtRpcHandlers / process_request / to_jsonable
│ request/response dict (JSON)
┌───────────────▼────────────────┐
│ RpcTransport 抽象接口 │
│ send_request / start_receiving│
│ send_response / stop │
└───┬────────┬─────────┬─────────┘
┌──────────▼┐ ┌────▼───┐ ┌──▼─────┐ ┌──────┐
│ Redis │ │ ZMQ │ │ MySQL │ │ SHM │
│ (默认) │ │ (低延迟)│ │(兼容) │ │(stub)│
└────────────┘ └────────┘ └────────┘ └──────┘
```
传输层只负责"请求/响应怎么在网络上走",不碰业务语义。抽象接口见
`src/bigqmt_signal_trader/transports/base.py`
- `send_request(request, timeout)` — 客户端发请求并阻塞等响应
- `start_receiving(on_request)` — 服务端开始接收,每个请求回调 `on_request`
- `send_response(request, response)` — 服务端回包(路由信息从 request 读)
- `stop()` — 释放资源
## 配置怎么切换
### 服务端(QMT 进程)
`bigqmt_signal_trader_local_config.py``BIGQMT_REDIS_CONFIG` 里设置 `transport` 字段:
```python
BIGQMT_REDIS_CONFIG = {
"transport": "zmq", # 默认 "redis"。可选: redis/zmq/mysql/shm
"zmq": {
"bind_address": "tcp://127.0.0.1:5560", # Windows 同机使用 TCP 回环
},
"rpc_background_threads": False,
"schedule_adjust": True,
"schedule_adjust_interval": "100nMilliSecond",
}
```
> **注意(ZMQ**ZMQ 支持由 QMT 官方 `adjust` 回调排空请求。低延迟实盘建议设置
> `rpc_background_threads=False`,并把 `schedule_adjust_interval` 设置为
> `100nMilliSecond`,避免后台 Python 线程受 QMT 进程 GIL 调度影响。
> MySQL/SHM 仍会自动启用后台接收线程。
> 端口不写时按账号自动派生 `tcp://127.0.0.1:{15560 + 账号%100}`(同机回环)。
> Linux 同机也可使用 `ipc:///tmp/bigqmt_rpc.sock`。
`transport=mysql` 时可额外配置:
```python
BIGQMT_REDIS_CONFIG["mysql"] = {
"driver": "pymysql", # 或 mysql.connector
"host": "127.0.0.1", "port": 3306,
"user": "rpc", "password": "***", "database": "bigqmt_rpc",
"pool_config": {
"mincached": 1, "maxcached": 4,
"maxshared": 3, "maxconnections": 8,
},
}
```
**不指定 `transport` = `"redis"` = 完全保持现状。**
### 客户端
`BigQmtRpcClient` 同样读 `transport` 字段(从 client config 或环境变量 `BIGQMT_RPC_TRANSPORT`):
```python
BIGQMT_REDIS_CONFIG = {
"transport": "zmq",
"zmq": {"connect_address": "tcp://127.0.0.1:5560"}, # 指向服务端 bind 地址
# ...
}
```
或环境变量:`export BIGQMT_RPC_TRANSPORT=zmq`
## 各传输说明
### Redis`transport: redis`,默认)
完全保持原有行为:
- 客户端 `RPUSH` 请求到 `bigqmt:rpc:queue:{account_id}``BLPOP` 响应 list
- 服务端 `brpop` 取请求 → 三路回包(`SETEX` key + `RPUSH` list + `PUBLISH` channel
- 保留 b64 股票代码混淆编码
现有 14 个测试、所有模板字符串、配置全部不变。
### ZMQ`transport: zmq`,低延迟)
- 服务端 ROUTER socket bind,客户端 DEALER socket connect
- 用 ZMQ 原生 identity 路由(`reply_*` 字段忽略)
- Windows 用 `tcp://127.0.0.1:port`ZMQ 在 Windows 不支持 `ipc://`
- Linux 同机可用 `ipc://` 更快(绕过 TCP 栈)
- 实测同机 tcp 回环:**p50 = 0.2ms**(比 Redis 快 ~60 倍);
在大 QMT 全终端进程内实测 ping **p50 ≈ 0.3ms**20 次 0 个 >50ms)。
注意:ZMQ transport 的 `stop()` 由 ROUTER 接收线程自己关闭 socketWindows
上跨线程 close socket 会触发 signaler 断言)。
> **⚠️ 在大 QMT 进程内跑 zmq 的两个必要条件(都已自动处理,勿手动关):**
>
> 1. **`background_threads` 必须为 True** —— ZMQ 的 ROUTER 只有在后台线程里才
> 起接收循环;否则只 bind 不收包,客户端全部超时。`_build_rpc_service` 已对
> 非 redis 传输**自动置 True**,无需在 config 里写。
> 2. **`schedule_adjust` 必须保持开** —— `run_time("adjust", interval)` 是我们注册的
> **RPC 队列 drain 定时器**(`adjust` 不是 QMT 内置回调,QMT 只自动调 init/handlebar;
> handlebar 里 `return adjust(...)`)。deferred 档的交易查询要靠它在主线程执行;关掉就没
> 有主线程 drain 点。它也是后台线程拿 GIL 窗口的节奏源:`schedule_adjust_interval` 越小
> inline 尾延迟越低(500ms→~490ms,100ms→热循环~100ms 但烧 CPU,**200ms 折中**)。
> 详见 `docs/BIG_QMT_REDIS_RPC.md` 的「延迟模式」。
### MySQL`transport: mysql`,兼容兜底)
-`requests` / `responses` 两张表轮询
- 通过 **DBUtils `PooledDB`** 连接池管理连接,避免频繁开关
- 跨驱动:支持 pymysql / mysql.connector / sqlite3paramstyle 自动适配)
- `DELETE-then-INSERT` 写响应,兼容 MySQL 和 sqlite
- 延迟较高(~50ms+,受轮询间隔限制),仅作 Redis/ZMQ 不可用时的兜底
连接池配置(`pool_config`):
```python
"pool_config": {
"mincached": 1, # 空闲连接数
"maxcached": 4, # 最大缓存连接
"maxshared": 3, # 最大共享连接
"maxconnections": 8, # 最大连接数
}
```
注意:sqlite 连接线程绑定,sqlite 测试需 `check_same_thread=False` + `maxshared=0`
### SHM`transport: shm`,未实现)
留接口,`send_request` 会抛 `TransportError`。Python 3.8+ 的
`multiprocessing.shared_memory` 或自定义 mmap 环形缓冲区可后续实现。
## 实测延迟对比(同机)
基准脚本:`python bench_transports.py -n 100`
```
redis n=100 min=10.86 p50=12.22 p90=14.77 p99=290.60 avg=24.99 ms
zmq n=100 min=0.15 p50=0.21 p90=0.33 p99=20.62 avg=0.43 ms
```
## 切换检查清单
1. 服务端和客户端的 `transport` 字段**必须一致**
2. zmq:不写 `zmq` 块时端口按账号自动派生 `tcp://127.0.0.1:{15560+账号%100}`
两端一致;要跨机或自定义端口时才写 `connect_address`/`bind_address`
3. zmq/mysql`background_threads``_build_rpc_service` 自动开,**不用手动配**
4. zmq(大 QMT 进程内):**保持 `schedule_adjust` 开**(默认就是开),否则 adjust
空转占满 GIL 饿死接收线程 → RPC 超时
5. mysql:两端连同一个数据库,schema 自动创建
6. 切回 redis:删掉 `transport` 字段或设为 `"redis"`,无需改其他配置
## 文件结构
```
src/bigqmt_signal_trader/transports/
├── __init__.py # 导出 build_transport, RpcTransport
├── base.py # RpcTransport 抽象基类
├── redis_transport.py # Redis 实现(默认,零行为变更)
├── zmq_transport.py # ZMQ ROUTER/DEALER 实现
├── mysql_transport.py # MySQL + DBUtils 连接池
├── shm_transport.py # 共享内存 stub
└── factory.py # build_transport(name, config) 工厂
```
测试:`tests/bigqmt_signal_trader/test_transports.py`9 个测试,含 ZMQ/MySQL 往返)
@@ -0,0 +1,174 @@
# subscribe_whole_quote 全推行情 — 真机联调验证报告
> 日期:2026-08-10(周一,交易日)
> 环境:本地客户端 + Windows QMT 服务端(内网联调)
> 版本:`feat/impl_subscribe_whole_quote` 分支(commit 7e0d67d 及之后修复)
> 文档:`docs/SUBSCRIBE_WHOLE_QUOTE_PUSH.md`(设计),本文档为真实环境验证结果
---
## 1. 环境与部署
### 1.1 拓扑
```
本地客户端 (venv, Python 3.10)
└─ bigqmt_signal_trader (editable 安装, 指向仓库 src/)
├─ BigQmtRpcClient ── redis (内网, db5) ──► 服务端 RPC
└─ RedisQuotePushChannel (pub/sub bigqmt:quote_push:{acct}:{topic})
│ redis pub/sub
Windows 服务端 (QMT 交易端, Python 3.6)
└─ <QMT python 目录>/ (策略 BIGQMT_REDIS_DRYRUN)
├─ bigqmt_signal_trader_strategy.py ── 启动 QuoteSubscriptionManager
├─ quote_subscription_manager.py ── 引用计数订阅管理
└─ quote_push_channel.py ── 推送通道(服务端, json 兜底编码)
```
### 1.2 部署动作
| 步骤 | 内容 | 结果 |
|---|---|---|
| 1 | 修复本地开发 venv(uv 重建 Python 3.10) | ✅ |
| 2 | `uv pip install -e /path/xtquant_big_convert[redis,msgpack]` 部署到 venv | ✅ |
| 3 | 全量替换服务端 40 个 bigqmt 文件为本地当前版本(逐文件 MD5 校验一致,保留 `local_config.py` 生产配置) | ✅ |
| 4 | 服务端 `full_tick_cache_enabled: True`(既有配置,无需改动) | ✅ |
> 关键教训:初期误判"服务端文件已是最新"(大小写哈希比对看串),实际除 3 个新文件外其余 19 个均为旧版,导致订阅 RPC 报 `method is not allowed`。全量替换 + 程序化 MD5 校验后解决。
---
## 2. 验证过程与结果
### 2.1 阶段一:基础链路(09:00-09:06)
| # | 验证项 | 结果 | 证据 |
|---|---|---|---|
| 1 | RPC 链路存活 | ✅ | `get_full_tick` 2.1s 返回盘前快照 |
| 2 | `subscribe_whole_quote` RPC 允许 | ✅ | 282ms 返回 seq(修复前报 `method is not allowed`,因服务端旧版 `redis_rpc.py``READ_METHODS |= QUOTE_SUBSCRIPTION_METHODS`) |
| 3 | 初始快照 prime | ✅ | 订阅后立即回调完整快照(lastPrice 11.19 昨收) |
| 4 | redis 推送通道 | ✅ | 模拟发布 → 客户端实时收到 |
| 5 | 竞价真实推送(09:15:27) | ✅ | stockStatus=12 集合竞价,盘口 397/23 |
**发现 Bug #1:msgpack/json 编码不对称**
- 现象:客户端推送线程 `msgpack.exceptions.ExtraData: unpack(b) received extra data` 崩溃
- 根因:服务端 QMT 内置 Python **无 msgpack**(json 兜底编码),客户端**有 msgpack**(按 msgpack 解码 json 文本 → 首字节 `{` 被当整数 + 尾随字节)
- 修复:`decode_push_payload` msgpack 失败时回退 json(测试驱动:红→绿)
### 2.2 阶段二:数据正确性(09:45-09:48,连续竞价)
**单标的 000001.SZ(60s)**:21 笔推送,间隔 min=2.21s / max=3.11s / avg=2.96s,>4s 的 0 个;time/volume/amount 单调性零违规。
**20 只活跃股(沪深300 成交额 top20, 120s)**:
| 指标 | 结果 |
|---|---|
| 每只推送次数 | 41~42 次(120s / 3s ≈ 40,高度一致) |
| 最大间隔 | 3.1~3.3s(全部 < 4s) |
| 平均间隔 | 2.93~2.99s |
| gap>4s | 0(全部 20 只) |
| 数据单调性(vol/amt/time) | 0 违规(全部 20 只) |
结论:**每 3 秒一份推送、零丢失、零乱序、零数据回退**,覆盖主板/创业板/科创板。
### 2.3 阶段三:多标的规模(09:36-09:40)
| 标的数 | 订阅耗时 | 初始快照 | 60s 增量推送 | 覆盖 | 错误 |
|---|---|---|---|---|---|
| 20 只 | 1.1s | 4 次/20 只 | 61 次 | 20/20 | 0 |
| 50 只 | 0.8s | 7 次/50 只 | 140 次 | 50/50 | 0 |
| 100 只 | 2.1s | 19 次/100 只 | 336 次 | 100/100 | 0 |
结论:推送量随标的数线性增长,覆盖完整,订阅耗时稳定,零错误。
### 2.4 阶段四:心跳与超时回收(A 组,09:53-09:56)
| # | 验证项 | 结果 | 证据 |
|---|---|---|---|
| A1 | 正常心跳保活 | ✅ | 90s 31 次推送,间隔 2.90s |
| A2 | 心跳超时回收(kill 不发退订) | ✅ | 之后同 client_id 重连安全 |
| A4 | 同 client_id 重连恢复 | ✅ | 45s 16 次推送,推送恢复 |
| A5 | 正常退订 + 再订阅 | ✅ | 退订后 10s 0 次,再订阅 11 次恢复 |
### 2.5 阶段五:多 client 并发(B 组,09:57-10:00)
| # | 验证项 | 结果 | 证据 |
|---|---|---|---|
| B1 | 两 client 同组合 | ✅ | A=7 B=7 各自收推 |
| B2 | 组合去重共享订阅 | ✅ | 推送节奏一致(7=7),服务端只建 1 个订阅 |
| B3 | 一方退订对方持续 | ✅ | A 退订后 A=0 B=5 |
| B4 | 全退订拆订阅 | ✅ | 无推送 |
| B5 | 不同组合互不干扰 | ✅ | 000001 只有 A 收,000002 只有 B 收 |
| B6 | 同 client 多 sub_id | ✅(修复后) | 见 Bug #2 |
| B7 | 混合组合隔离 | ✅ | 000001 双方收,000002 只有 E 收 |
**发现 Bug #2:客户端订阅线程泄漏**
- 现象:B6 首测失败(退订 sub1 后 sub2 偶发收不到),深挖发现订阅/退订时 `_sync_subscriber_locked` 无脑新起线程、旧线程不停止(3 个 `bigqmt-quote-push-sub` 线程并存),多线程消费同一 pubsub 有竞态
- 修复:topic 集合 diff——不变则复用,变化则先 stop 旧线程再起新线程,变空则停(测试驱动)
### 2.6 阶段六:异常与边界(C 组,10:05-10:18)
| # | 验证项 | 结果 | 证据 |
|---|---|---|---|
| C1 | 重复订阅幂等 | ✅ | 15s 11 次推送 |
| C2 | 空代码列表 | ✅ | `ValueError: code_list is required` |
| C3 | 非法代码容错 | ✅ | 未崩,无推送 |
| C4 | 同 topic 多 sub_id 退订隔离 | ✅(修复后) | 见 Bug #3 |
| C5 | 拔线重连 | ✅ | A2/A4 覆盖 |
**发现 Bug #3:服务端引用计数粒度错误**
- 现象:C4 首测失败——同 client 两个 sub_id 订阅同一组合,退订一个后,另一个的推送停止(组合被整体拆掉)
- 根因:`_Combo.clients``client_id` 粒度,但订阅单元是 `(client_id, sub_id)`;退订一个 sub 时 `_remove_client_locked` 把整个 client 移出,组合错误拆解
- 修复:改为 `(client_id, sub_id)` 粒度(含 subscribe/unsubscribe/keepalive/reaper 四处)(测试驱动,新增单测覆盖)
### 2.7 阶段七:服务端重启恢复(C6,10:27-10:31)
| 验证轮次 | 客户端行为 | 结果 |
|---|---|---|
| C6v1(修复前) | 无自动重放 | ❌ 重启后推送永久中断(140s 静默) |
| C6v2(keepalive 失败重放) | 只靠 keepalive 失败检测 | ❌ 未触发——重启窗口内 keepalive 被 redis 队列兜住"成功",检测不到 |
| C6v3(静默检测重放) | 推送静默超阈值自动重放 | ✅ 中断 42s 后自动恢复,后续 27 次推送正常 |
**发现 Bug #4:服务端重启后订阅丢失,客户端无自动恢复**
- 根因:文档承诺的"client 检测断连→自动重放"**从未实现**(`replay_subscriptions` 无生产调用点);且仅靠 keepalive 失败检测不可靠(redis 请求队列在重启窗口内缓冲,keepalive 不抛异常)
- 修复:心跳循环增加**推送静默检测**——订阅期间超过 N 个心跳周期(默认 10,≈30s)无推送到达,自动重放订阅(测试驱动,新增 2 个单测)
### 2.8 阶段八:配置验证(D 组,10:32)
| # | 验证项 | 结果 |
|---|---|---|
| D1 | `BIGQMT_QUOTE_HEARTBEAT_SECONDS` env 生效 | ✅ 1s 心跳,30s 10 次推送 |
| D2 | `heartbeat_timeout_seconds` 配置生效 | 单测覆盖(修改需重启策略,跳过) |
---
## 3. 发现并修复的 Bug 汇总(4 个)
| # | Bug | 位置 | 根因 | 修复 |
|---|---|---|---|---|
| 1 | 推送解码崩溃 `ExtraData` | `quote_push_channel.py` | 服务端无 msgpack(json 兜底)与客户端 msgpack 解码不对称 | msgpack 失败回退 json |
| 2 | 订阅线程泄漏/竞态 | `whole_quote_session.py` | `_sync_subscriber_locked` 每次新起线程不停止旧的 | topic 集合 diff,复用/停止 |
| 3 | 同 client 多 sub 退订误拆组合 | `quote_subscription_manager.py` | 引用计数按 client 粒度而非 (client, sub) 粒度 | 改 (client_id, sub_id) 粒度 |
| 4 | 服务端重启后订阅丢失 | `whole_quote_session.py` | 自动重放从未接线;keepalive 被 redis 队列兜住检测不到重启 | 推送静默检测自动重放 |
全部按 TDD 修复(红→绿),同步部署到服务端。
---
## 4. 测试基线
- 全量:`266 passed, 3 skipped, 1 failed`
- 唯一失败:`test_transports.py::MysqlTransportTest::test_round_trip`(`No module named 'dbutils'`,本地 venv 未装 mysql extra)——**预先存在,与本次改动无关**
- 新增测试:
- `test_quote_push_channel.py`:json 解码回退 2 个
- `test_whole_quote_client.py`:线程复用/重启 4 个 + 自动重放 2 个
- `test_quote_subscription_manager.py`:(client, sub) 粒度退订 1 个
---
## 5. 结论
1. **全链路功能正确**:订阅 RPC、初始快照 prime、redis 推送通道、增量推送(3s 节奏)、多标的(20/50/100)、多 client 共享订阅、退订隔离、心跳保活/超时回收、服务端重启自动恢复,全部通过。
2. **数据正确性**:单标的 21/60s、20 只活跃股 41-42/120s,间隔 2.93-3.0s 稳定,零丢失、零乱序、零回退。
3. **发现并修复 4 个真实 bug**,均经 TDD 验证,全量测试无回归。
4. **遗留**:D2(超时配置生效)仅单测覆盖;mysql 测试环境缺 DBUtils(预先存在);`docs/SUBSCRIBE_WHOLE_QUOTE_PUSH.md` 中"自动重放"设计描述现已实现,可保持同步。
@@ -0,0 +1,317 @@
# subscribe_whole_quote 真推送方案(对齐 miniqmt
> 状态:**待评审**(方案已成型,未动实现。实现严格按 TDD:先红→绿→回归)
> 分支:`feat/impl_subscribe_whole_quote`
## 1. 背景与现状诊断
### 1.1 miniqmt 语义(目标行为)
`xtdata.subscribe_whole_quote(code_list, callback)` 在 miniqmt 里是**真订阅**
- 注册后,行情服务**持续推送**,每个行情周期触发一次 `callback(data)``data``{code: tick_dict}` 的全推快照。
- 返回一个 `seq`(订阅句柄);`unsubscribe_quote(seq)` 后推送停止。
- 全市场用板块代码:`["SH"]``["SZ"]``["SH","SZ"]`(也支持 `"BJ"``"HK"`)。
### 1.2 当前实现(client 端假订阅,server 端空转)
- **client**`xtquant_compat.py:781`):`subscribe_whole_quote` 只是 `publish_event("subscribe_whole_quote", ...)` 到 redis stream`bigqmt:quote_events:{account_id}`),然后**同步调一次 `get_full_tick` 触发一次 callback** 就返回 `seq`。之后**再无任何推送**。`callback` 不被持有,纯一次性。
- **server**:**全仓库没有任何代码消费 `quote_events` stream**,也没有任何代码调用大 QMT 的 `ContextInfo.subscribe_whole_quote` / `xtdata.subscribe_whole_quote`。事件发出去石沉大海。
- `unsubscribe_quote(seq)` 同样只发事件 + 删 redis 订阅记录,无实际效果。
**结论:miniqmt 的「注册 → 持续推送 → 回调」语义当前完全没有实现。**
### 1.3 transport 现状
- `RpcTransport``transports/base.py`)是**纯请求/响应**模型:client `send_request`、server `start_receiving`/`send_response`。**没有 server→client 的主动推送通道**。
- zmq transport 用 ROUTER/DEALER,请求响应式;redis transport 的 pubsub 只用于 RPC 请求/响应通道,不用于行情推送。
### 1.4 可复用的资产
- `full_tick_cache`server 周期 `ContextInfo.get_full_tick` 拉快照写 redis,client 读缓存。**是轮询拉取,不是推送**,但有现成的 demand/TTL 机制(本方案不采用它做数据面,仅作对比参考)。
- `BigQmtRpcHandlers``redis_rpc.py:333`):server 端白名单 dispatch`market_data` 适配器持有 `ContextInfo` —— server 端订阅管理器复用此路径访问大 QMT 行情接口。
- 参考实现:`quant-qmt-proxy``SubscriptionManager` 已验证 `xtdata.subscribe_whole_quote(["SH","SZ"], callback)` 推送模式 + 心跳超时(默认 60s)在本环境可行。
---
## 2. 已确认的决策(来自讨论)
| # | 决策点 | 结论 |
|---|--------|------|
| Q1 | 数据通路 | **方案 A:真推送**。server 端大 QMT 订阅回调 → 推送通道 → client callback。新增 server→client 推送通道。 |
| Q2 | 去重粒度 | **按组合去重**`frozenset(code_list)` 规范化后作为订阅单元 key。不同 client 传相同集合 → 共享同一个大 QMT 订阅。 |
| Q3 | 引用计数 & keepalive | client 分配 `client_id`,周期发 keepalive(带 `client_id`+组合);server 维护 `{组合: {client_id: last_seen}}`,**超时阈值 = 10 个心跳周期**未收到才认为该 client 消亡;组合所有 client 消亡后才真正退订大 QMT。 |
| Q4 | server 重启恢复 | **client 重放**:client 记忆自己的订阅集合,检测到断连/server 重启后自动重放订阅;server 无状态、靠 client 重放/keepalive 重建订阅。**server 订阅表落盘不做**(实现阶段决定:client 重放已覆盖恢复路径,落盘引入 QMT 环境文件 IO 复杂度,无额外收益)。 |
| Q5 | 大 QMT 行情源 | **ContextInfo 优先**server 用策略进程内 `ContextInfo.subscribe_whole_quote`。"建/退订阅"收敛为可替换适配层,按真实环境实测微调(见 §7 风险 1)。 |
| Q6 | 推送通道落地 | **zmq + redis 同阶段交付**:同一 `QuotePushChannel` 抽象下两个实现,按部署 transport 选择。 |
| Q7 | 推送编码 | **高效编码:msgpack 优先,json 兜底**。msgpack 作为可选依赖(`optional-dependencies`),全市场推送建议安装;未装时退化为 json。 |
---
## 3. 总体架构
```
┌─────────────┐ subscribe_whole_quote ┌──────────────────────────────────┐
│ client A │ ────────────────────────► │ server (大 QMT 进程) │
│ (xtdata) │ RPC: subscribe_whole │ QuoteSubscriptionManager │
└─────────────┘ _quote {client_id, │ ├─ 组合去重 frozenset │
│ codes, sub_id} │ ├─ refcount {combo: {cid: ts}} │
┌─────────────┐ │ └─ ContextInfo/xtdata. │
│ client B │ ────────────────────────► │ subscribe_whole_quote( │
└─────────────┘ 同组合 → 共享同一订阅 │ codes, on_push) │
│ └──────────┬───────────────────────┘
keepalive RPC │ 周期心跳 {client_id, sub_id} │ 大 QMT 行情回调 on_push(data)
──────────┼──────────────────────────► │
│ ▼
│ ┌──────────────────────────┐
行情推送 │ ◄────────────────────── │ QuotePusher (PUB socket)│
(PUB/SUB) │ topic=combo, {data} │ 按组合 topic 广播 │
│ └──────────────────────────┘
```
**三条逻辑通道**(对应三个职责):
1. **控制面 RPC(已有 transport 复用)**`subscribe_whole_quote` / `unsubscribe_whole_quote` / `quote_keepalive` 三个新 RPC 方法,走现有请求/响应 transportredis 或 zmq)。
2. **数据面推送(新增)**server→client 单向 PUB/SUB 通道,承载行情推送。
3. **大 QMT 行情源**server 端 `ContextInfo.subscribe_whole_quote`(或 `xtdata.subscribe_whole_quote`)回调。
---
## 4. 详细设计
### 4.1 订阅单元 keyQ2:组合去重)
```python
def combo_key(code_list):
"""规范化组合 → 唯一 key。顺序无关、大小写统一、去空白。"""
return ",".join(sorted({str(c).strip().upper() for c in (code_list or []) if str(c).strip()}))
```
- `["SH","SZ"]``["sz","SH"]` → 同一 key `"SH,SZ"`,共享同一个大 QMT 订阅。
- 全市场 `["SH"]`、标的组合 `["000001.SZ","600000.SH"]` 都是合法 key。
### 4.2 client 端(`BigQmtXtData.subscribe_whole_quote` 重写)
- 入参 `code_list, callback` 不变(对齐 miniqmt 签名)。
- 生成 `client_id`(进程级唯一,复用 zmq DEALER identity 或 `uuid4`,**进程生命周期内稳定**,持久化到本地文件以便重启后识别同一 client)。
- 为本次订阅分配 `sub_id`(沿用现有 `_next_seq()`)。
- 记录到 client 侧订阅表 `{sub_id: {codes, callback, combo_key}}`**用于重放恢复**)。
- 发 RPC `subscribe_whole_quote {client_id, sub_id, codes}` → server 返回 `{combo_key, push_endpoint, push_topic}`
- 启动/复用一个 **SUB 接收线程**,订阅 `push_topic`,每收到一帧 → 解析 → 调用所有匹配该 topic 的本地 callback。
- 启动/复用一个 **keepalive 线程**,每 `heartbeat_interval` 秒对所有活跃 `sub_id``quote_keepalive {client_id, sub_id}`
- **初始全量打底**:大 QMT 全推回调是**增量**的(见 §4.3 调研结论),不保证订阅后立即给全量。client 在订阅成功后**先主动调一次 `get_full_tick(code_list)` 触发 callback 打底**,随后由增量推送驱动 callback。这既保留现有"订阅即给一帧"的行为,又对齐大 QMT 的增量语义。
- 返回 `sub_id`
`unsubscribe_quote(sub_id)`:发 RPC `unsubscribe_whole_quote {client_id, sub_id}`,从本地表删除;该 sub_id 停止 keepalive。返回 0(对齐 miniqmt)。
### 4.3 server 端 `QuoteSubscriptionManager`(新模块)
挂在 server 进程内,持有 **ContextInfo**Q5:优先用策略进程内 `ContextInfo.subscribe_whole_quote`;复用 `market_data` 适配器的 ContextInfo 引用)与 `QuotePusher`
**大 QMT 订阅适配层**Q5ContextInfo 优先)。已调研确认大 QMT 真实签名(见下方"调研结论"),适配层对外只暴露两个方法:
```python
class QuoteSourceAdapter: # ContextInfo 优先实现
def subscribe(self, codes, on_push) -> handle: ...
def unsubscribe(self, handle) -> None: ...
```
**调研结论(真实大 QMT 环境,官方文档 + 隔壁 quant-qmt-proxy 实测交叉验证)**
| 项 | 结论 |
|---|---|
| 订阅签名 | `ContextInfo.subscribe_whole_quote(code_list, callback=None)` |
| `code_list` | 市场代码 `['SH','SZ']` 或品种代码 `['600000.SH','000001.SZ']` |
| 返回值 | `int` 订阅号 `subId`**`< 0` 表示失败**quant-qmt-proxy 实测) |
| 退订 | `ContextInfo.unsubscribe_quote(subId)`(与 `subscribe_quote` 共用同一退订方法) |
| **回调数据** | **增量推送**:每次回调只含**有变化**品种的最新 tick;`get_full_tick` 才是全量快照 |
| 回调线程 | 独立于 `handlebar` 的推送线程(官方建议回调内不阻塞、扔队列处理) |
`QuoteSubscriptionManager` 只跟 `QuoteSourceAdapter` 打交道,不直接碰 ContextInfo——`subscribe` 内部调 `ContextInfo.subscribe_whole_quote(codes, on_push)``unsubscribe` 内部调 `ContextInfo.unsubscribe_quote(handle)`;若实测仍有出入,只改这一层。
**核心状态(内存,权威)**
```python
{
combo_key: {
"codes": [...], # 原始 code_list
"qmt_sub_handle": <大QMT订阅句柄>, # subscribe_whole_quote 返回值
"clients": {client_id: last_seen_ts}, # 引用计数 + 心跳
"topic": push_topic,
}
}
```
另维护 `sub_id → (client_id, combo_key)` 反向索引,用于按 sub_id 退订。
**三个 RPC handler**(加入 `BigQmtRpcHandlers.allowed_methods`,走现有 dispatch):
- `subscribe_whole_quote {client_id, sub_id, codes}`
- `key = combo_key(codes)`
- 若该 combo 不存在:调 `ContextInfo.subscribe_whole_quote(codes, on_push)` 建订阅,记录 handle`on_push` 闭包绑定 `key`;建 `topic`
- `clients[client_id] = now`;登记 `sub_id → (client_id, key)`
- 返回 `{combo_key, topic, push_endpoint}`
- `unsubscribe_whole_quote {client_id, sub_id}`
-`sub_id` 找到 `(client_id, key)``clients.pop(client_id)`,删除 `sub_id` 索引。
- 若该 combo 的 `clients` 为空 → `ContextInfo.unsubscribe_whole_quote(handle)`(或对应退订 API),销毁 combo。
- 返回 `{}`
- `quote_keepalive {client_id, sub_id}`
-`sub_id` 找 combo`clients[client_id] = now`。返回 `{}`
**reaper(后台周期任务,挂在 server 的 adjust/调度循环上)**
-`reap_interval` 秒扫描:对每 combo,删除 `now - last_seen > 10 * heartbeat_interval` 的 client。
- combo 的 `clients` 清空后 → 退订大 QMT、销毁 combo。
**on_push 回调**(大 QMT 行情线程触发):
- 收到 `data``{code: tick}`)→ 调 `QuotePusher.publish(topic, data)`
- **注意线程安全**:大 QMT 回调在行情线程,zmq PUB socket 的 send 需串行化(入队给专属发送线程,或加锁)。参照 zmq transport 已有的"socket 只能由创建它的线程关闭/使用"约束,推送也走队列 + 专属线程。
### 4.4 推送通道(新增 `QuotePushChannel`zmq + redis 同阶段交付)
同一抽象下**两个实现同阶段交付**(Q6),按 client 当前 transport 选择:
```python
class QuotePushChannel: # 抽象
def publish(self, topic, data) -> None: ... # server 端
def subscribe(self, topic, on_msg) -> None: ... # client 端
```
**zmq PUB/SUB 实现**(无 redis 部署的原生通道):
- server 端绑定一个 `PUB` socket(独立于现有 ROUTER,单独端口,地址随 RPC discovery 下发或在 subscribe 响应里返回 `push_endpoint`)。
- client 端 `SUB` socket connect`setsockopt(SUBSCRIBE, topic)` 按组合过滤。
- 帧格式:`[topic_bytes][payload_bytes]`
- topic = `combo_key`(即 `"SH,SZ"`),SUB 端精确匹配前缀即可。
**redis pub/sub 实现**redis 部署):
- 复用现有 redis 连接,channel 名 `bigqmt:quote_push:{account_id}:{combo_key}`
- server `publish`、client `subscribe` 同一 channel。
**编码(Q7msgpack 优先,json 兜底)**
- 推送 payload 用 **msgpack** 序列化(对 `{code: {field: number}}` 这类结构,比 json 快数倍、体积更小,是全市场推送的标准选择)。
- msgpack 列为**可选依赖**`pyproject.toml``optional-dependencies`,与 redis/mysql 同组织方式),避免给最小安装(仅 pyzmq)增加硬依赖。
- 未安装 msgpack 时**退化为 json**stdlib),保证功能可用、仅吞吐降级。编码选择封装在 `QuotePushChannel` 内部,对上层透明。
> 取舍说明:zmq PUB/SUB 是 **fire-and-forget**client 掉线期间推送被丢弃(符合行情推送语义——增量丢了就等下一帧,无需逐条补发)。**但增量推送不自带全量**,client 重连/重放后必须由 §4.2 的"初始全量打底"`get_full_tick`)重建本地状态,再接收增量。这与 miniqmt 行为一致。
### 4.5 keepalive 与超时(Q3
- `heartbeat_interval`(client 发心跳周期),**默认 3s**。
- 超时阈值 = `10 * heartbeat_interval = 30s`(Q3 已确认 10 个周期)。server 端某 client 超过 30s 无心跳 → 判定消亡,从所有 combo 的 `clients` 移除。
- 心跳与数据面解耦:即便行情静默(盘后用快照),心跳照发,保证引用计数准确。
### 4.6 server 重启恢复(Q4
- **client 重放**client 的 SUB 接收线程检测到推送通道断开/server ping 失败 → 触发重连;重连成功后,对本地订阅表里**所有活跃 sub_id 重新发 `subscribe_whole_quote`**(幂等:server 按 `(client_id, combo)` 去重,重放不会重复建大 QMT 订阅)。
- **server 落盘(不做)**:实现阶段决定 server 订阅表**不落盘**。client 重放已覆盖恢复路径(重启后 client 重放即可重建全部订阅),落盘只增加 QMT 环境文件 IO 与状态一致性复杂度,无额外收益。
- **幂等性**`subscribe_whole_quote` handler 对相同 `(client_id, sub_id, combo)` 重复调用安全(重建 last_seen,不重复建大 QMT 订阅)。
---
## 5. 配置项
server 端(`config["quote_push"]`,见 `bigqmt_signal_trader_strategy.py`):
| 配置键 | 默认 | 说明 |
|------|------|------|
| `enabled` | `true` | 是否启用全推推送服务 |
| `heartbeat_timeout_seconds` | `30.0` | client 无心跳超时阈值(= 10 个心跳周期 × 3s) |
| `zmq_bind_address` | RPC zmq 端口 + 1 | zmq PUB 绑定地址(仅 transport=zmq 时用) |
> 推送通道跟随 RPC transportzmq/redis),reaper 挂在 RPC drain 上(随 drain 周期执行,无独立 interval)。server 订阅表不落盘(见 §4.6)。
client 端(`bigqmt_signal_trader_client_config.py` / 环境变量):
| 配置 | 默认 | 说明 |
|------|------|------|
| `BIGQMT_QUOTE_CLIENT_ID` | 自动生成并持久化到 `~/.cache/bigqmt/quote_client_id` | client 唯一 id(重启稳定,用于重放识别) |
| `BIGQMT_QUOTE_HEARTBEAT_SECONDS` | `3.0` | 心跳周期(环境变量),须 < server 超时 |
---
## 6. TDD 实施计划(先红→绿→回归)
按依赖顺序分 6 个增量,每个增量都是「先写失败测试 → 实现 → 回归全套」。
**阶段 1 — 组合 key + 引用计数核心(纯逻辑,无 IO)**
- 红:`tests/bigqmt_signal_trader/test_quote_subscription_manager.py`
- `combo_key` 顺序无关/大小写统一。
- 两个 client 订阅同组合 → 只建一次大 QMT 订阅(mock ContextInfo),refcount=2。
- 一个 client 退 → 不退大 QMT;全部退 → 退一次大 QMT。
- 心跳超时:构造 `last_seen` 过期 → reaper 移除 clientcombo 空 → 退订。
- 绿:`src/bigqmt_signal_trader/quote_subscription_manager.py``combo_key` + `QuoteSubscriptionManager`ContextInfo 用注入的 mock)。
- 回归:全套测试。
**阶段 2 — server RPC handler 接线**
- 红:`subscribe_whole_quote` / `unsubscribe_whole_quote` / `quote_keepalive` 三个方法经 `BigQmtRpcHandlers.handle` 可达、白名单放行、参数校验、幂等重放。
- 绿:在 `redis_rpc.py` 加 handler 方法 + `allowed_methods`,注入 `QuoteSubscriptionManager`
- 回归。
**阶段 3 — 推送通道抽象 + zmq/redis 双实现 + 编码**
- 红:`tests/bigqmt_signal_trader/test_quote_push_channel.py`
- `QuotePushChannel` 接口(`publish(topic, data)` / `subscribe(topic, on_msg)`)。
- zmq PUB/SUB 回环:bind PUB → SUB connect → publish → SUB 收到且 topic 过滤正确。
- redis pub/sub 回环:fake redis 下 publish → subscribe 收到。
- 编码:msgpack 可用时 payload 用 msgpack(解出结构与原始一致),未装时退化 json。
- 绿:`src/bigqmt_signal_trader/quote_push_channel.py`(抽象 + zmq 实现 + redis 实现 + msgpack/json 编码选择)。
- 回归。
**阶段 4 — server on_push → 推送通道接线**
- 红:mock ContextInfo 触发 `on_push(data)``QuotePushChannel.publish` 被以正确 topic+data 调用;线程安全(并发回调不竞态)。
- 绿:`QuoteSubscriptionManager``QuotePushChannel`
- 回归。
**阶段 5 — client 端重写(订阅 + SUB 接收 + keepalive 线程)**
- 红:`tests/bigqmt_signal_trader/test_whole_quote_client.py`
- `subscribe_whole_quote` 发正确 RPC、注册本地 callback、启动 keepalive。
- 收到推送帧 → 触发对应 callback。
- `unsubscribe_quote` 发 RPC、停心跳。
- 重放:模拟断连后 → 对所有活跃 sub_id 重发 subscribe。
- 绿:重写 `BigQmtXtData.subscribe_whole_quote` / `unsubscribe_quote`,新增 client 侧 SUB 接收与 keepalive 线程、`client_id` 管理。
- 回归。
**阶段 6 — 端到端 + 多 client 共享 + server 重启恢复**
- 红:端到端测试(in-proc fake server + 两 client
- 两 client 订同组合 → 各自 callback 都收到推送;server 只对大 QMT 建一次订阅。
- 一 client 退 → 另一个仍收;全退 → server 退订大 QMT。
- server "重启"(重建 manager + 推送通道)→ client 重放 → 恢复推送。
- client 静默超 30s → server 清引用 → 退订。
- 绿:补齐集成胶水(server 启动时装配 manager+push channelclient 重连逻辑)。
- 回归:全套 + 现有 `test_all_apis.py` 端到端不破坏。
---
## 7. 风险与开放问题
1. **大 QMT 订阅/退订 API(已调研确认,风险解除)**:签名、返回值(`int` 订阅号,`<0` 失败)、退订方法 `unsubscribe_quote(subId)` 均已确认(见 §4.3 调研结论)。`QuoteSourceAdapter` 仍保留,作为唯一接触 ContextInfo 的层,便于真实环境联调时微调。
2. **行情回调线程模型**:大 QMT `on_push` 在独立的推送线程触发(非 `handlebar` 线程),zmq send 必须跨线程安全(队列 + 专属发送线程);官方亦建议回调内不阻塞、扔队列。阶段 4 专门覆盖。
3. **增量推送语义**:大 QMT 全推回调是**增量**(只推变化品种),不是全量快照。client 端必须用 `get_full_tick` 打底 + 增量更新(§4.2),不能假设订阅后即得全量。这点与早期假设不同,已在 §4.2/§4.4 修正。
4. **全市场推送量级**`["SH","SZ"]` 全推增量仍可能每帧数千条。已按 Q7 采用 **msgpack** 编码(可选依赖,未装退化 json)压低开销;PUB 广播吞吐仍需在真实环境实测,若仍不足再评估压缩/分片(不过早优化)。
5. **msgpack 依赖**:新增可选依赖 `msgpack``optional-dependencies`,对齐 redis/mysql 的组织方式),最小安装(仅 pyzmq)不受影响;未装时推送通道退化 json 编码。
6. **与 full_tick_cache 关系**:二者独立。full_tick_cache 服务 `get_full_tick` 按需拉取;本方案服务 `subscribe_whole_quote` 推送。不冲突,不合并。
---
## 8. 交付物清单(已全部交付)
- 新增:`src/bigqmt_signal_trader/quote_subscription_manager.py``combo_key``QuoteSubscriptionManager``QuoteSourceAdapter`/`ContextInfoQuoteSource``build_quote_subscription_service`
- 新增:`src/bigqmt_signal_trader/quote_push_channel.py`(抽象 + zmq/redis 实现 + msgpack/json 编码)
- 新增:`src/bigqmt_signal_trader/whole_quote_session.py`(client 端订阅会话:订阅表 + 推送路由 + 心跳线程 + 重放)
- 修改:`src/bigqmt_signal_trader/redis_rpc.py``QUOTE_SUBSCRIPTION_METHODS` + 3 个 handler + 白名单 + `quote_subscription_manager` 注入)
- 修改:`src/bigqmt_signal_trader/xtquant_compat.py``subscribe_whole_quote`/`unsubscribe_quote` 重写 + session 懒建 + client_id 持久化 + push channel 选择 + `get_full_tick` 打底)
- 修改:`src/bigqmt_signal_trader_strategy.py`server 启动装配 `_build_quote_subscription_service` + publisher 启动 + reaper 挂 RPC drain
- 修改:`pyproject.toml``optional-dependencies` 增加 `msgpack`
- 新增测试:`test_quote_subscription_manager.py` / `test_quote_push_channel.py` / `test_quote_on_push_wiring.py` / `test_whole_quote_client.py` / `test_xtdata_whole_quote.py` / `test_quote_subscription_service.py` / `test_whole_quote_e2e.py`
- 文档:本文档 + `RPC_API_REFERENCE.md` §2.5 增补 3 个新方法 + `bigqmt_signal_trader_client_config.example.py` 增补 client 配置
**实现期间 TDD 抓到的两个真实 bug**
1. **`_sub_index` 键冲突**:两 client 各自 `sub_id` 从 1 开始,server 以单 `sub_id` 为键互相覆盖 → 引用计数错乱、"全退才退订"失效。e2e 测试暴露后改为 `(client_id, sub_id)` 复合键。这是多 client 场景的核心正确性问题,单 client 测试无法覆盖。
2. **`load_client_config``quote_client_id` 键**:导致 client_id 配置读不到、静默退回持久化文件路径。
**未实现**:server 订阅表落盘兜底(§4.6,经决策不做,靠 client 重放恢复)。
**待真实环境联调**(不阻塞交付,均已在 §7 标注):`ContextInfo.subscribe_whole_quote` 实际句柄/退订微调、`["SH","SZ"]` 全推吞吐实测、zmq/redis 推送通道在真实部署的连通性。
@@ -0,0 +1,222 @@
# MiniQMT 无损替换兼容层
更新时间:2026-07-01
## 目标
把原来依赖 MiniQMT 的调用:
```python
from xtquant.xttrader import XtQuantTrader
from xtquant.xttype import StockAccount
from xtquant import xtdata, xtconstant
```
替换为“大 QMT 策略进程 + Redis RPC”的远程调用,同时尽量保持业务代码继续使用:
```python
xt_trader.query_stock_positions(acc)
xt_trader.query_stock_asset(acc)
xt_trader.query_stock_orders(acc)
xt_trader.query_stock_trades(acc)
xt_trader.order_stock(...)
xt_trader.order_stock_async(...)
xt_trader.cancel_order_stock_sysid(...)
xtdata.get_full_tick(...)
```
## 接入方式一:显式导入新包
适合先灰度,不影响机器上的真实 `xtquant` 包。
```python
from bigqmt_signal_trader.xtquant_compat import (
StockAccount,
configure,
xt_trader,
xtdata,
)
from bigqmt_signal_trader import xtquant_compat as xtconstant
configure()
acc = StockAccount(xt_trader.client.account_id, "STOCK")
positions = xt_trader.query_stock_positions(acc)
ticks = xtdata.get_full_tick(["600000.SH"])
```
这类写法的优点是替换范围小,适合先在 `core/trader.py` 或独立测试脚本里验证查询链路。`configure()` 会原地更新已导入的 `xt_trader` / `xtdata` 对象,所以可以先 `from ... import xt_trader`,再调用 `configure()`
## 接入方式二:用 `xtquant` shim 替换老 import
适合最终切换。把本仓库的 `src` 放到 `PYTHONPATH` 最前面后,老代码里的:
```python
from xtquant.xttrader import XtQuantTrader, XtQuantTraderCallback
from xtquant.xttype import StockAccount
from xtquant import xtdata, xtconstant
```
会命中本仓库提供的 `src/xtquant/` shim。这样主业务代码基本不用改,只需要在本地私有配置文件里设置 Redis 和账号:
```python
# D:\gjzqqmt\xtquant_big_convert\src\bigqmt_signal_trader_client_config.py
BIGQMT_ACCOUNT_ID = "YOUR_ACCOUNT_ID"
BIGQMT_RPC_TIMEOUT_SECONDS = 6.0
BIGQMT_REDIS_CONFIG = {
"host": "YOUR_REDIS_HOST",
"port": 6379,
"db": 5,
"username": "",
"password": "******",
}
BIGQMT_FULL_TICK_CACHE_CONFIG = {
"enabled": False,
"demand_ttl_seconds": 10,
"cache_ttl_seconds": 10,
"wait_seconds": 3.5,
}
```
然后启动前只需要确认本仓库的 `src``PYTHONPATH` 最前面:
```powershell
$env:PYTHONPATH = "D:\gjzqqmt\xtquant_big_convert\src;$env:PYTHONPATH"
```
如果同一台机器仍然安装了真实 MiniQMT 的 `xtquant` 包,要确认 `D:\gjzqqmt\xtquant_big_convert\src` 位于 `PYTHONPATH` 最前面,否则 Python 会先加载真实 `xtquant`
## 推荐落地步骤
1. 大 QMT 侧先运行 `BIGQMT_REDIS_DRYRUN` / `bigqmt_signal_trader_redis_rpc_runtime.py`,保持 `rpc_allow_order_methods=False`
2. 原策略侧用显式导入方式跑查询自检:资产、持仓、单票五档行情、`["SH","SZ"]` 全市场行情。
3. 查询链路稳定后,把原项目中 `core/trader.py` 的初始化切到兼容层,但仍保持远程下单关闭。
4. 对比 MiniQMT 与大 QMT 返回的资产、持仓、委托、成交字段,确认业务字段都能读到。
5. 只在确认风控、账号、委托价型都正确后,在大 QMT 私有配置里打开 `rpc_allow_order_methods=True`
6. 最终切换时再使用 `xtquant` shim,让旧 import 保持不变。
## 当前已兼容的方法
| MiniQMT 调用 | 兼容状态 | 说明 |
|---|---|---|
| `XtQuantTrader(path, session_id)` | 已兼容 | 构造本地 RPC 客户端,不连接 MiniQMT |
| `register_callback()` | 已兼容 | 保存 callbackRPC 暂不推送回调 |
| `start()` / `connect()` / `subscribe()` | 已兼容 | 返回 `0``subscribe()` 会补账号 |
| `query_stock_asset(acc)` | 已兼容 | 返回对象含 `cash``available_cash``total_asset``market_value` |
| `query_stock_positions(acc)` | 已兼容 | 返回对象列表,含 `stock_code``volume``can_use_volume``avg_price``price` |
| `query_stock_position(acc, code)` | 已兼容 | 返回单只持仓对象或 `None` |
| `query_stock_orders(acc, cancelable_only=False)` | 已兼容 | 返回对象列表,含 `order_type``order_status``order_volume``traded_volume``order_sysid` |
| `query_stock_trades(acc)` | 已兼容 | 返回对象列表,含 `order_type``traded_volume``traded_price` |
| `order_stock()` / `order_stock_async()` | 已兼容 | 需要大 QMT 本地配置打开 `rpc_allow_order_methods=True` |
| `cancel_order_stock_sysid()` | 已兼容 | 需要大 QMT 本地配置打开 `rpc_allow_order_methods=True` |
| `xtdata.get_full_tick(codes)` | 已兼容 | 默认直接 RPC 调用;支持单票、ETF、`["SH", "SZ"]` 全市场;可选打开 Redis 快照缓存 |
| `xtdata.get_instrument_detail(code)` | 已兼容 | 映射到大 QMT `get_instrumentdetail()` |
| `xtdata.get_instrument_type(code)` | 已接入 | 优先调大 QMT;不支持时按代码前缀做基础判断 |
| `xtdata.subscribe_quote(...)` / `subscribe_whole_quote(...)` | Redis 订阅兼容 | 写入 `bigqmt:quote_subscriptions:{account_id}`,并向 `bigqmt:quote_events:{account_id}` 发事件;callback 会收到一次当前快照/历史数据 |
| `xtdata.unsubscribe_quote(seq)` | Redis 事件兼容 | 不强依赖大 QMT 反订阅 API,直接删除 Redis 订阅表并推送 `unsubscribe_quote` 事件 |
| `xtdata.get_market_data(...)` | 已接入 RPC | 透传到大 QMT `ContextInfo.get_market_data`,返回 DataFrame/字典结构会自动 JSON 化再还原 |
| `xtdata.get_market_data_ex(...)` | 已接入 RPC | 大 QMT 不支持 `get_market_data_ex` 时回退到 `get_market_data` |
| `xtdata.get_local_data(...)` | 已接入 RPC | 大 QMT 不支持 `get_local_data` 时回退到 `get_market_data` |
| `xtdata.get_stock_list_in_sector(...)` | 已接入 RPC | 优先调大 QMT;失败时对 `"沪深A股"``get_full_tick(["SH","SZ"])` 过滤 |
| `xtdata.get_sector_list()` / `get_sector_info()` | 已接入 RPC | 依赖大 QMT `ContextInfo` 是否支持 |
| `xtdata.get_divid_factors(...)` | 已接入 RPC | 依赖大 QMT `ContextInfo` 是否支持 |
| `xtdata.download_history_data(...)` / `download_history_data2(...)` | 已接入 RPC | 依赖大 QMT `ContextInfo` 是否支持 |
| `xtdata.get_trading_dates(...)` / `get_holidays()` / `download_holiday_data()` | 已接入 RPC | 依赖大 QMT `ContextInfo` 是否支持 |
| `xtdata.get_ipo_info(...)` | 已接入 RPC | 行情侧新股资料;交易侧 `query_ipo_data()` 仍是占位 |
| `xtdata.get_etf_info()` / `download_etf_info()` | 已接入 RPC | 依赖大 QMT `ContextInfo` 是否支持 |
| `xtdata.get_option_list(...)` / 历史期权列表 | 已接入 RPC | 依赖大 QMT `ContextInfo` 是否支持 |
| `xtdata.get_financial_data(...)` / `download_financial_data(...)` | 已接入 RPC | 支持 DataFrame 返回值序列化 |
| `xtdata.call_formula(...)` / `subscribe_formula(...)` / `unsubscribe_formula(...)` / `get_formula_result(...)` | 已接入 RPC | 对应截图里的模型调用/订阅能力,依赖大 QMT `ContextInfo` 是否支持 |
| `xtdata.gen_factor_index(...)` | 已接入 RPC | 对应生成因子数据,依赖大 QMT `ContextInfo` 是否支持 |
| `query_ipo_data()` / `query_new_purchase_limit()` | 占位兼容 | 当前返回空结果,打新需要后续补大 QMT 等价能力 |
## 下单开关
大 QMT 本地配置默认关闭远程下单。要真正替换 MiniQMT 下单,需要在 QMT 本地私有配置中显式开启:
```python
BIGQMT_REDIS_CONFIG = {
"host": "YOUR_REDIS_HOST",
"port": 6379,
"db": 5,
"username": "",
"password": "******",
"rpc_allow_order_methods": True,
}
```
开启后,`price_type` 会从客户端透传到大 QMT `passorder()`,不会再固定成默认限价。
## 最小自检脚本
这个脚本只读,不会下单:
```python
from bigqmt_signal_trader.xtquant_compat import StockAccount, configure, xt_trader, xtdata
configure()
acc = StockAccount(xt_trader.client.account_id, "STOCK")
asset = xt_trader.query_stock_asset(acc)
positions = xt_trader.query_stock_positions(acc)
tick = xtdata.get_full_tick(["600000.SH"])
all_a = xtdata.get_stock_list_in_sector("沪深A股")
print("cash:", asset.cash)
print("total_asset:", asset.total_asset)
print("positions:", len(positions), positions[:3])
print("bid5:", tick["600000.SH"]["bidPrice"])
print("ask5:", tick["600000.SH"]["askPrice"])
print("hs_a_count:", len(all_a))
```
如果想验证最终 shim 方式:
```python
from xtquant.xttrader import XtQuantTrader
from xtquant.xttype import StockAccount
from xtquant import xtdata, xtconstant
trader = XtQuantTrader("", 12345)
acc = StockAccount(trader.client.account_id, "STOCK")
assert trader.connect() == 0
assert trader.subscribe(acc) == 0
print(xtconstant.STOCK_BUY)
print(trader.query_stock_asset(acc))
print(xtdata.get_full_tick(["600000.SH"]))
```
## 验证命令
```powershell
cd D:\gjzqqmt\xtquant_big_convert
python -B -m unittest discover -s tests\bigqmt_signal_trader
```
实盘前建议先只跑查询链路:
```python
from bigqmt_signal_trader.xtquant_compat import StockAccount, configure, xt_trader, xtdata
configure()
acc = StockAccount(xt_trader.client.account_id)
print(xt_trader.query_stock_asset(acc))
print(xt_trader.query_stock_positions(acc)[:3])
print(xtdata.get_full_tick(["600000.SH"]))
```
## 注意事项
- `subscribe_quote()` / `subscribe_whole_quote()` 当前通过 Redis 记录订阅意图,并给 callback 推一次当前数据;持续行情推送需要独立的 Redis 行情生产者消费 `bigqmt:quote_subscriptions:{account_id}`
- `get_full_tick()` 默认直接 RPC 现拉;如果全市场 payload 过大,再在客户端和 QMT 本地配置里打开 Redis 快照缓存。
- `unsubscribe_quote(seq)` 当前按你的要求直接写 Redis:删除订阅表并推送 `unsubscribe_quote` 事件,不等待大 QMT 确认。
- `get_stock_list_in_sector("沪深A股")` 的本地兜底会通过 `get_full_tick(["SH", "SZ"])` 过滤 A 股,速度取决于大 QMT 全市场快照返回耗时。
- 历史行情、财务、ETF、期权、模型/因子等接口已经接到 RPC,但实际是否可用取决于大 QMT 策略环境里的 `ContextInfo` 是否暴露同名方法。
- `query_ipo_data()` / `query_new_purchase_limit()` 当前返回空结果,打新逻辑不能直接视为无损替换。
- RPC 下单默认关闭;打开前必须确认大 QMT 页面正在运行正确账号的 RPC 策略。
@@ -0,0 +1,142 @@
# QMT 原生 ZMQ 回测桥接
## 1. 目标和边界
正式模式是在 **QMT 回测进程内部**运行一个 ZMQ 服务,把 QMT 当前回测 Bar、
账户、持仓、委托和成交桥接给外部策略。QMT 是唯一的行情推进器、回测引擎、
账户系统和撮合器。
`BIGQMT_ZMQ_BACKTEST.py` 与现有实盘 RPC 入口完全分离:
- 不导入或修改 `bigqmt_signal_trader`
- 使用独立端口 `tcp://127.0.0.1:16662`
- 只允许 `ContextInfo.do_back_test=true` 的 QMT 回测上下文;
- 协议固定返回 `live_ready=false`
- ZMQ 后台线程只接收请求和排队,不直接调用 QMT API;
- `passorder`、撤单和账户查询只在 QMT `handlebar` 回调线程执行。
项目仍保留端口 `16661` 的 CSV 独立回测工具,用于脱离 QMT 的协议测试。该工具
使用本地 `BacktestEngine/SimulatedBroker`,不是 QMT 原生回测服务,二者不能混用。
## 2. 运行时序
1. QMT 加载 `BIGQMT_ZMQ_BACKTEST.py` 并调用 `init(ContextInfo)`
2. 入口确认当前是 QMT 回测模式,绑定 QMT 注入的 `passorder``cancel`
`get_trade_detail_data`,然后启动 ZMQ 服务。
3. QMT 调用 `handlebar` 时,服务发布当前 Bar,并等待外部策略完成这一 Bar 的决策。
4. 外部策略调用 `submit_order``cancel_order`;ZMQ 线程只把命令放入当前 Bar 队列。
5. 外部策略调用 `next_bar` 后,QMT 回调线程排空命令并调用 QMT API,然后把控制权
交还 QMT。QMT 自己撮合并推进下一根 Bar。
6. QMT 的 `order_callback``deal_callback` 以及账户查询结果会进入 ZMQ 状态;
QMT 调用 `stop/after_backtest` 后,下一次 `next_bar` 返回 `done=true`
外部策略超时不释放当前 Bar 时,桥接会抛出超时错误并停止继续下单,避免 QMT
静默跑完整段历史而外部策略没有参与。
## 3. QMT 端安装与配置
同步以下内容到正在运行的 QMT `python` 目录:
```text
src/bigqmt_backtest/
src/BIGQMT_ZMQ_BACKTEST.py
```
编辑 `BIGQMT_ZMQ_BACKTEST.py` 顶部配置:
```python
BACKTEST_ZMQ_CONFIG = {
"bind_endpoint": "tcp://127.0.0.1:16662",
"run_id": "", # 空值会按启动时间生成
"account_id": "你的QMT回测账号",
"account_type": "STOCK",
"strategy_name": "ZMQ_BACKTEST",
"combo_type": 1101,
"quick_trade": 2,
"market_price_type": 5,
"limit_price_type": 11,
"bar_wait_timeout_seconds": 60,
"require_qmt_backtest": True,
}
```
入口是 GBK/ASCII`bigqmt_backtest` 包使用 UTF-8。在 QMT 中创建回测任务并且只加载
`BIGQMT_ZMQ_BACKTEST.py`,不要使用“启动本地 Python”。启动日志会打印实际
`run_id`、端口和账号。
`account_id` 必须填写,服务会调用 `ContextInfo.set_account(account_id)`,外部订单最终
通过以下 QMT 原生接口提交:
```text
passorder(23/24, 1101, account_id, symbol, price_type, price, quantity,
strategy_name, quick_trade, client_order_id, ContextInfo)
```
撮合价格、成交时间、手续费、资金和持仓均以 QMT 回测结果为准,桥接不再计算第二套
结果。
## 4. 外部策略启动
安装客户端包:
```powershell
python -m pip install -e .
```
启动 QMT 回测后,在外部 Python 运行:
```powershell
python examples/zmq_backtest_strategy.py `
--endpoint tcp://127.0.0.1:16662 `
--symbol 600000.SH `
--fast 5 `
--slow 20
```
没有传 `--run-id` 时,客户端先调用 `describe` 发现 QMT 本次运行的 `run_id`。同一运行
只允许首个调用 `start``client_id` 控制。
## 5. 协议
ZMQ 使用 `REQ/REP`,请求包含:
```json
{
"schema_version": 1,
"request_id": "唯一幂等键",
"run_id": "qmt-native-20260719-120000",
"client_id": "strategy-a",
"method": "start",
"params": {}
}
```
| 方法 | QMT 原生模式含义 |
|---|---|
| `ping` / `describe` | 探活、发现 `run_id` 和确认 `engine_owner=QMT` |
| `start` | 外部策略挂接并等待 QMT 第一根 Bar;不启动第二个引擎 |
| `submit_order` | 把订单意图排入当前 Bar,返回 `QUEUED` |
| `cancel_order` | 把撤单意图排入当前 Bar |
| `next_bar` | 释放当前 Bar;QMT 线程执行命令并等待 QMT 下一根 Bar |
| `state` | 返回 QMT 缓存的资金、持仓和当前 Bar |
| `history` | 返回 QMT 已经发布给外部策略的历史 Bar,不泄露未来数据 |
| `orders` / `fills` | 返回 QMT 委托/成交查询及回调归一化结果 |
| `finish` | 外部策略解除挂接;QMT 回测报告仍由 QMT 生成 |
响应包含 `execution_backend=QMT_NATIVE``execution_mode=QMT_BACKTEST`
`live_ready=false`。相同 `request_id` 的重试返回缓存响应。
## 6. CSV 独立模式
仅当不启动 QMT、需要验证协议或外部策略逻辑时使用:
```powershell
python -m bigqmt_backtest.server `
--data examples/backtest_bars.example.csv `
--config examples/backtest_config.example.json `
--run-id demo-001 `
--bind tcp://127.0.0.1:16661
```
该模式响应 `execution_backend=LOCAL_SIM`,本地产出 `result.json`、委托、成交、资金
曲线等证据。它不会调用 QMT,也不能代表 QMT 原生撮合结果。
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,7 @@
datetime,symbol,open,high,low,close,volume,prev_close
2026-01-05 09:30:00,600000.SH,10.00,10.05,9.98,10.02,100000,9.95
2026-01-05 09:31:00,600000.SH,10.02,10.08,10.01,10.07,120000,9.95
2026-01-05 09:32:00,600000.SH,10.07,10.12,10.06,10.11,110000,9.95
2026-01-05 09:33:00,600000.SH,10.11,10.13,10.05,10.06,130000,9.95
2026-01-06 09:30:00,600000.SH,10.08,10.10,10.00,10.02,150000,10.06
2026-01-06 09:31:00,600000.SH,10.02,10.04,9.96,9.98,140000,10.06
1 datetime symbol open high low close volume prev_close
2 2026-01-05 09:30:00 600000.SH 10.00 10.05 9.98 10.02 100000 9.95
3 2026-01-05 09:31:00 600000.SH 10.02 10.08 10.01 10.07 120000 9.95
4 2026-01-05 09:32:00 600000.SH 10.07 10.12 10.06 10.11 110000 9.95
5 2026-01-05 09:33:00 600000.SH 10.11 10.13 10.05 10.06 130000 9.95
6 2026-01-06 09:30:00 600000.SH 10.08 10.10 10.00 10.02 150000 10.06
7 2026-01-06 09:31:00 600000.SH 10.02 10.04 9.96 9.98 140000 10.06
@@ -0,0 +1,22 @@
{
"initial_cash": 1000000,
"initial_positions": {},
"buy_commission_rate": 0.0003,
"sell_commission_rate": 0.0003,
"min_commission": 5,
"stamp_tax_rate": 0.0005,
"transfer_fee_rate": 0.00001,
"slippage_bps": 0,
"max_volume_participation": 0.1,
"price_limit_rate": 0.1,
"lot_size": 100,
"time_in_force": "NEXT_BAR",
"seed": 0,
"fee_schedule": "a_share_2023_08_28",
"market_rules_version": "a_share_v1",
"strategy_name": "ma_example",
"parameters": {
"fast": 2,
"slow": 3
}
}
@@ -0,0 +1,69 @@
"""Example external moving-average strategy for the ZMQ backtest bridge."""
import argparse
from bigqmt_backtest.client import BacktestZmqClient
from bigqmt_backtest.strategy import ExternalStrategyRunner
class MovingAverageStrategy(object):
def __init__(self, symbol, fast=5, slow=20):
self.symbol = symbol
self.fast = int(fast)
self.slow = int(slow)
self.sequence = 0
def on_bar(self, context, bars):
if self.symbol not in bars:
return []
rows = context.history(self.symbol, count=self.slow, fields=["close"])
if len(rows) < self.slow:
return []
closes = [float(row["close"]) for row in rows]
fast_value = sum(closes[-self.fast :]) / self.fast
slow_value = sum(closes) / self.slow
position = context.positions.get(self.symbol, {})
quantity = int(position.get("quantity") or 0)
available = int(position.get("available") or 0)
self.sequence += 1
if fast_value > slow_value and quantity == 0:
return [
{
"client_order_id": "ma-buy-%d" % self.sequence,
"symbol": self.symbol,
"side": "BUY",
"quantity": 100,
"order_type": "MARKET",
}
]
if fast_value < slow_value and available > 0:
return [
{
"client_order_id": "ma-sell-%d" % self.sequence,
"symbol": self.symbol,
"side": "SELL",
"quantity": available,
"order_type": "MARKET",
}
]
return []
def main():
parser = argparse.ArgumentParser()
parser.add_argument("--endpoint", default="tcp://127.0.0.1:16661")
parser.add_argument("--run-id", default="", help="Optional; discovered from QMT when omitted")
parser.add_argument("--symbol", required=True)
parser.add_argument("--fast", type=int, default=5)
parser.add_argument("--slow", type=int, default=20)
args = parser.parse_args()
with BacktestZmqClient(args.endpoint, args.run_id, client_id="ma-example") as client:
result = ExternalStrategyRunner(
client,
MovingAverageStrategy(args.symbol, fast=args.fast, slow=args.slow),
).run()
print(result)
if __name__ == "__main__":
main()
@@ -0,0 +1,160 @@
# coding: utf-8
"""Live API smoke test + latency bench (read-only, safe for live account).
Covers every read method grouped by category. Reports per-call status and
latency, plus a category summary. Does NOT call any order/cancel method.
"""
import sys
import time
sys.path.insert(0, r"D:\gjzqqmt\xtquant_big_convert\src")
sys.path.insert(0, r"D:\国金证券QMT交易端_lemo\python")
import bigqmt_signal_trader.xtquant_compat as compat
compat.configure()
client = compat.get_default_client()
ACCOUNT = client.account_id
print("account:", ACCOUNT, "| transport:", client.transport_name)
print("=" * 78)
# (category, method, params)
GROUPS = [
("系统", [
("ping", {}),
]),
("行情快照", [
("get_full_tick", {"codes": ["000001.SZ"]}),
("get_ticks", {"codes": ["000001.SZ", "600000.SH"]}),
]),
("合约/品种", [
("get_instrument", {"code": "000001.SZ"}),
("get_instrument_type", {"code": "000001.SZ"}),
("get_stock_name", {"stock": "000001.SZ"}),
("get_last_close", {"stock": "000001.SZ"}),
("get_float_caps", {"stockcode": "000001.SZ"}),
("get_total_share", {"stockcode": "000001.SZ"}),
("get_contract_multiplier", {"stockcode": "000001.SZ"}),
]),
("K线/历史", [
("get_market_data_ex", {"field_list": ["close"], "stock_list": ["000001.SZ"], "period": "1d", "count": 5}),
("get_market_data", {"field_list": ["close"], "stock_list": ["000001.SZ"], "period": "1d", "count": 5}),
("get_local_data", {"field_list": ["close"], "stock_list": ["000001.SZ"], "period": "1d", "count": 5}),
("get_divid_factors", {"stock_code": "000001.SZ", "end_time": "20250101"}),
]),
("板块", [
("get_sector_list", {}),
("get_stock_list_in_sector", {"sector_name": "沪深A股"}),
("get_sector_info", {"sector_name": "沪深A股"}),
]),
("交易日历/时间", [
("get_trading_dates", {"market": "SH", "count": 5}),
("get_holidays", {}),
("get_markets", {}),
("get_market_last_trade_date", {"market": "SH"}),
("get_trading_calendar", {"market": "SH", "start_time": "20250601", "end_time": "20250615"}),
("get_date_location", {"date": "20250701"}),
("datetime_to_timetag", {"datetime_str": "20250701150000", "format": "%Y%m%d%H%M%S"}),
("timetag_to_datetime", {"timetag": 1751353200000, "format": "%Y%m%d %H:%M:%S"}),
]),
("财务/因子", [
("get_financial_data", {"stock_list": ["000001.SZ"], "table_list": ["CAPITAL"], "start_time": "20240101", "end_time": "20241231"}),
]),
("ETF/期权/期货", [
("get_etf_info", {}),
("get_main_contract", {"code_market": "IF"}),
("get_his_contract_list", {"market": "IF"}),
]),
("期权定价", [
("bsm_price", {"opt_type": "C", "target_price": 3.0, "strike_price": 2.8, "risk_free": 0.03, "sigma": 0.3, "days": 30}),
("bsm_iv", {"opt_type": "C", "target_price": 3.0, "strike_price": 2.8, "option_price": 0.25, "risk_free": 0.03, "days": 30}),
]),
("龙虎榜/资金流", [
("get_longhubang", {"stock_list": ["000001.SZ"], "start_time": "20250101", "end_time": "20250630"}),
("get_turnover_rate", {"stock_code": ["000001.SZ"], "start_time": "20250601", "end_time": "20250630"}),
("get_industry", {"industry_name": "银行"}),
("get_north_finance_change", {"period": "1d"}),
]),
("账户查询", [
("get_asset", {}),
("get_positions", {}),
("query_stock_position", {"stock_code": "000001.SZ"}),
("query_orders", {}),
("query_trades", {}),
]),
("官方交易函数", [
("get_ipo_data", {}),
("get_new_purchase_limit", {}),
("get_hkt_exchange_rate", {}),
("get_value_by_order_id", {"order_id": "1"}),
("get_last_order_id", {}),
]),
("融资融券(普通账户应空)", [
("get_assure_contract", {}),
("get_unclosed_compacts", {}),
("get_debt_contract", {}),
("get_enable_short_contract", {}),
]),
]
results = [] # (category, method, status, ms, summary)
def summarize(d):
if d is None:
return "None"
if isinstance(d, dict):
if not d:
return "{}"
if "__bigqmt_type__" in d:
return "[%s cols=%d rec=%d]" % (d.get("__bigqmt_type__"), len(d.get("columns") or []), len(d.get("records") or []))
k = list(d.keys())[:2]
return "{%s...}(%d)" % (k, len(d))
if isinstance(d, list):
return "[len=%d]" % len(d)
return repr(d)[:40]
for category, methods in GROUPS:
print("\n--- %s ---" % category)
for method, params in methods:
t0 = time.time()
try:
data = client.call(method, params)
ms = (time.time() - t0) * 1000
status = "OK"
results.append((category, method, status, ms, summarize(data)))
except Exception as e:
ms = (time.time() - t0) * 1000
status = "FAIL"
results.append((category, method, status, ms, str(e)[:40]))
r = results[-1]
print(" [%-4s %6.1fms] %-28s %s" % (r[2], r[3], r[1], r[4]))
# Summary
print("\n" + "=" * 78)
print("=== 汇总 ===")
ok = [r for r in results if r[2] == "OK"]
fail = [r for r in results if r[2] == "FAIL"]
print("通过 %d / 失败 %d / 总计 %d" % (len(ok), len(fail), len(results)))
print("\n=== 按类别 ===")
cats = {}
for r in results:
cats.setdefault(r[0], []).append(r)
for cat, items in cats.items():
o = sum(1 for i in items if i[2] == "OK")
avg = sum(i[3] for i in items) / len(items)
print(" %-22s %d/%d avg=%.1fms" % (cat, o, len(items), avg))
print("\n=== 延迟分布 (OK) ===")
lat = sorted(i[3] for i in ok)
if lat:
p50 = lat[len(lat) // 2]
p90 = lat[int(len(lat) * 0.9)]
print(" n=%d min=%.1fms p50=%.1fms p90=%.1fms max=%.1fms" % (len(lat), lat[0], p50, p90, lat[-1]))
if fail:
print("\n=== 失败明细 ===")
for r in fail:
print(" %-28s %s" % (r[1], r[4]))
@@ -0,0 +1,63 @@
[build-system]
requires = ["setuptools>=68", "wheel"]
build-backend = "setuptools.build_meta"
[project]
name = "xtquant-big-convert"
version = "0.2.2"
description = "Big QMT RPC bridge and MiniQMT-compatible adapter layer (redis/zmq/mysql transports)"
readme = "README.md"
requires-python = ">=3.8"
license = {text = "MIT"}
authors = [
{name = "litaolemo"},
]
keywords = ["qmt", "quant", "trading", "rpc", "redis", "zmq", "bigqmt", "miniqmt"]
classifiers = [
"Development Status :: 4 - Beta",
"Intended Audience :: Developers",
"License :: OSI Approved :: MIT License",
"Programming Language :: Python :: 3",
"Programming Language :: Python :: 3.8",
"Programming Language :: Python :: 3.9",
"Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11",
"Topic :: Office/Business :: Financial :: Investment",
]
dependencies = [
"pyzmq>=25.0.0",
]
[project.optional-dependencies]
redis = ["redis>=5.0.0"]
mysql = ["pymysql>=1.0.0", "DBUtils>=3.0.0"]
# Faster/smaller wire encoding for whole-quote push (falls back to json if absent).
msgpack = ["msgpack>=1.0.0"]
dev = [
"pytest>=7.0.0",
"pytest-cov>=4.0.0",
]
[project.urls]
Homepage = "https://github.com/litaolemo/xtquant_big_convert"
Repository = "https://github.com/litaolemo/xtquant_big_convert.git"
Issues = "https://github.com/litaolemo/xtquant_big_convert/issues"
[tool.setuptools]
package-dir = {"" = "src"}
py-modules = [
"BIGQMT_REDIS_DRYRUN",
"BIGQMT_ZMQ_BACKTEST",
"bigqmt_signal_trader_strategy",
"bigqmt_signal_trader_redis_rpc_runtime",
"bigqmt_signal_trader_redis_dryrun",
"bigqmt_signal_trader_dryrun",
"bigqmt_signal_trader_diagnostic",
]
[tool.setuptools.packages.find]
where = ["src"]
include = ["bigqmt_signal_trader*", "bigqmt_backtest*", "xtquant*"]
[tool.setuptools.package-data]
bigqmt_signal_trader = ["*.md"]
@@ -0,0 +1,346 @@
---
name: qmt-trader
description: "通过统一 CLI 脚本驱动大 QMT 迅投量化交易端的全部能力,含实时行情查询、K线历史数据、账户资产与持仓查询、委托与成交查询、买入卖出下单、撤单、板块龙虎榜北向资金财务数据等,并内置 xtquant_big_convert 桥接服务的安装部署引导(装包/同步 QMT 端文件/配置/启动验证/排错)。适用于大模型辅助量化交易分析、行情研判、持仓监控、半自动下单等场景。当用户需要查看股票行情、分析K线、查询持仓资产、查看今日委托成交、下单买卖、撤单、查询北向资金龙虎榜财务数据,或需要安装部署 QMT RPC 桥接服务时触发此 skill。"
---
# QMT Trader — 大模型驱动的 QMT 交易/行情工具
## 概述
本 skill 提供一个确定性 CLI 脚本 `scripts/qmt.py`,让大模型通过命令行调用大 QMT 的全部
交易与行情能力,避免每次现场写 Python 代码。所有命令默认输出 JSON(便于解析),加 `--table`
切换人类可读表格。
**前置条件**:本 skill 依赖 xtquant_big_convert 桥接服务已部署运行。若 `ping` 失败或用户尚未部署,
先按下文「首次部署」引导完成:装包 → 同步 QMT 端文件 → 写配置 → QMT 里运行入口 → 验证。
## 首次部署(只需一次,AI 逐步引导用户完成)
部署分两端:**客户端**(跑本 skill/策略的开发机)和**服务端**(大 QMT 客户端内置 Python)。
### 第 1 步:客户端安装包
```bash
pip install "xtquant-big-convert[redis]" # redis 传输(默认,推荐)
# 或 zmq 同机低延迟:pip install xtquant-big-convert(基础版已含 pyzmq
```
> 没发布到 PyPI 的私有 fork 用源码安装:`git clone <repo> && cd xtquant_big_convert && pip install -e .[redis]`
### 第 2 步:把服务端文件同步到 QMT 的 python 目录
需要拷 4 项到大 QMT 的 `python` 目录(如 `D:\国金证券QMT交易端\python\`):
```
bigqmt_signal_trader/ (整个包,pip 装的在 site-packages 里)
bigqmt_signal_trader_strategy.py
bigqmt_signal_trader_redis_rpc_runtime.py
BIGQMT_REDIS_DRYRUN.py (★ QMT 编辑器入口,GBK 编码)
```
pip 安装后的文件位置可以用这条命令定位(输出目录里就有全部 4 项):
```bash
python -c "import bigqmt_signal_trader_strategy as m, os; print(os.path.dirname(m.__file__))"
```
> QMT 沙箱若拒绝 `import redis`(部分券商白名单拦截),改用仓库里的 `bigqmt_no_redis/` 无 redis 版本(自包含 ZMQ 传输)。
### 第 3 步:创建 QMT 端私有配置
在 QMT 的 `python` 目录创建 `bigqmt_signal_trader_local_config.py`(含账号密码,**不要提交 git**):
```python
# coding: utf-8
BIGQMT_ACCOUNT_ID = "资金账号"
BIGQMT_REDIS_CONFIG = {
"host": "Redis地址", "port": 6379, "db": 5, "password": "Redis密码",
"rpc_allow_order_methods": False, # 下单开关,默认关闭;确认风控后改 True
"rpc_process_in_listener": True,
"rpc_listener_methods": ("*",),
"rpc_background_threads": False, # 若切 zmq/mysql 传输必须改 True
"schedule_adjust": True,
"schedule_adjust_interval": "500nMilliSecond",
}
```
> 切 zmq:配置里加 `"transport": "zmq"` 并把 `rpc_background_threads` 改 `True`QMT 端需装 pyzmq 19.0.2Python 3.6 最后支持的版本)。
### 第 4 步:在 QMT 策略编辑器运行入口
QMT 策略编辑器里**只加载运行 `BIGQMT_REDIS_DRYRUN.py` 一个文件**(它自动 import 其余模块)。
若 QMT 装在非默认路径且用 exec 方式加载,需改文件里 `_known_qmt_python_dir()` 的 fallback 路径。
启动成功标志(QMT 输出面板):
```
[bigqmt_shell] local redis config loaded keys=[...]
[bigqmt_shell] local account config loaded=True
[bigqmt_rpc] started channel=bigqmt:rpc:req:你的账号
[bigqmt_signal_trader] init ok
```
### 第 5 步:客户端配置 + 验证
客户端用环境变量(或 `bigqmt_signal_trader_client_config.py`)指向同一套 Redis/账号:
```powershell
$env:BIGQMT_ACCOUNT_ID="资金账号"
$env:BIGQMT_REDIS_HOST="Redis地址"; $env:BIGQMT_REDIS_PORT="6379"
$env:BIGQMT_REDIS_DB="5"; $env:BIGQMT_REDIS_PASSWORD="Redis密码"
```
然后验证(redis ~13ms / zmq ~0.7ms 为正常):
```bash
python scripts/qmt.py ping
```
### 部署排错速查
| 现象 | 排查 |
|------|------|
| `ping` 超时 | 客户端/服务端 transport 不一致(一边 redis 一边 zmq);QMT 端服务没启动;Redis 地址/密码/db 不一致 |
| QMT 面板报 `import redis` 被拒 | 换 `bigqmt_no_redis/` 无 redis 版本 |
| 启动了但查询全空 | 账号没对上:服务端 `BIGQMT_ACCOUNT_ID` vs 客户端 `BIGQMT_ACCOUNT_ID`QMT 需在实盘模式 |
| 下单报 `ORDER_DISABLED` | 正常保护,服务端配置 `rpc_allow_order_methods``True` 才放行 |
| 详细错误日志 | QMT python 目录下 `logs/bigqmt_*.log`(保留 7 天),排错首选 |
## 快速开始
### 第 0 步:确认连通性
```bash
python scripts/qmt.py ping
```
返回 `ok: true``latency_ms` 合理(redis ~13ms / zmq ~0.7ms)即表示服务端就绪。
### 第 1 步:一键快照(资产+持仓+委托+成交)
```bash
python scripts/qmt.py snapshot
```
一次 RPC 往返返回账户全景,适合快速了解当前状态。
## 命令速查
### 行情分析
| 命令 | 用途 | 示例 |
|------|------|------|
| `tick <codes...>` | 实时五档盘口 | `tick 600000.SH 000001.SZ` |
| `kline <code>` | K线/历史行情 | `kline 600000.SH --period 1d --count 60 --dividend front` |
| `instrument <code>` | 合约详情 | `instrument 600000.SH` |
| `sector [name]` | 板块成分股/板块列表 | `sector "沪深A股"` |
| `trading-dates` | 交易日历 | `trading-dates --count 10` |
| `north` | 北向资金 | `north --period 1d` |
| `longhubang <code>` | 龙虎榜 | `longhubang 600000.SH --count 5` |
| `financial <codes...>` | 财务数据 | `financial 000001.SZ --tables Capital.CAPITAL` |
| `download <codes...>` | 下载历史数据 | `download 600654.SH --period 1d --dividend front` |
| `quote-subscribe <codes...>` | 实时全推订阅 | `quote-subscribe SH SZ --max 10` |
### 账户/持仓/委托
| 命令 | 用途 | 示例 |
|------|------|------|
| `account` | 账户资产 | `account` |
| `positions [code]` | 持仓列表 | `positions` / `positions 600000.SH` |
| `orders` | 今日委托 | `orders --cancelable` |
| `trades` | 今日成交 | `trades` |
| `snapshot` | 一键全景 | `snapshot` |
### 下单/撤单
| 命令 | 用途 | 示例 |
|------|------|------|
| `buy <code> <volume>` | 买入 | `buy 600000.SH 100 --price 7.50` |
| `sell <code> <volume>` | 卖出 | `sell 600000.SH 100 --price 7.50` |
| `cancel <order_id>` | 撤单 | `cancel 12345 --market SH` |
> 下单命令支持 `--dry-run`(只打印不下单)、`--latest`(最新价)、`--strategy`、`--remark`。
### 扩展查询(高频)
| 命令 | 用途 | 示例 |
|------|------|------|
| `holiday` | 节假日列表 | `holiday` |
| `stock-name <code>` | 股票名称 | `stock-name 600000.SH` |
| `instrument-type <code>` | 品种类型 | `instrument-type 600000.SH` |
| `divid-factors <code>` | 除权除息因子 | `divid-factors 600000.SH` |
| `market-times [market]` | 日内交易时段 | `market-times SH` |
| `trading-calendar [market]` | 交易日历(含时段) | `trading-calendar SH` |
| `option-list <code>` | 期权列表 | `option-list 510050.SH` |
| `bsm-price ...` | BSM 期权定价 | `bsm-price C 3.0 2.8 0.03 0.3 30` |
| `bsm-iv ...` | BSM 隐含波动率 | `bsm-iv C 3.0 2.8 0.25 0.03 30` |
| `hkt-stats <code>` | 港股通统计 | `hkt-stats 600000.SH` |
| `hkt-details <code>` | 港股通明细 | `hkt-details 600000.SH` |
| `hkt-rate` | 港股通汇率 | `hkt-rate` |
| `top10-holder <code>` | 十大股东 | `top10-holder 600000.SH` |
| `holder-num <code>` | 股东户数 | `holder-num 600000.SH` |
| `ipo` / `ipo-limit` | 新股数据/申购额度 | `ipo` |
| `credit-assure` | 融资担保品合约 | `credit-assure` |
| `credit-short` | 融券标的合约 | `credit-short` |
| `credit-debt` | 负债合约 | `credit-debt` |
| `his-st <code>` | 历史 ST 数据 | `his-st 600000.SH` |
| `index-weight <index>` | 指数权重 | `index-weight 000300.SH` |
| `industry <name>` | 行业成分 | `industry 银行` |
| `sector-info [name]` | 板块详情 | `sector-info 沪深A股` |
| `local-data <code>` | 本地缓存数据 | `local-data 600000.SH` |
| `timetag2dt <ms>` | 毫秒时间戳转日期 | `timetag2dt 1751353200000` |
| `dt2timetag <dt>` | 日期转毫秒时间戳 | `dt2timetag 20250701150000` |
### 通用 RPC(兜底所有方法)
`rpc <method> [json_params]` 可调用**任意白名单方法**(含未列出的,如 `get_l2_quote` / `call_formula` / `get_raw_financial_data` 等):
```bash
python scripts/qmt.py rpc get_holidays
python scripts/qmt.py rpc get_stock_name '{"stock":"600000.SH"}'
python scripts/qmt.py rpc get_l2_quote '{"stock_code":"600000.SH","count":5}'
python scripts/qmt.py rpc call_formula '{"formula_name":"MA","stock_code":"600000.SH","period":"1d"}'
```
## 典型工作流
### 场景一:行情分析
分析某只股票的技术面:
```bash
# 1. 看实时盘口
python scripts/qmt.py tick 600000.SH
# 2. 拉最近 60 根日 K(前复权),输出含 MA5/MA20/MA60 统计
python scripts/qmt.py kline 600000.SH --period 1d --count 60 --dividend front
# 3. 看合约详情(名称、上市日、最小变动价位等)
python scripts/qmt.py instrument 600000.SH
# 4. 看近期龙虎榜
python scripts/qmt.py longhubang 600000.SH --count 5
```
### 场景二:持仓监控
```bash
# 一键看全景
python scripts/qmt.py snapshot
# 只看持仓(含浮动盈亏)
python scripts/qmt.py positions
# 看可撤委托
python scripts/qmt.py orders --cancelable
```
### 场景三:下单交易
```bash
# 0. 先看当前价
python scripts/qmt.py tick 600000.SH
# 1. 干跑确认参数
python scripts/qmt.py buy 600000.SH 100 --price 7.50 --dry-run
# 2. 真实下单(限价 7.50 买 100 股)
python scripts/qmt.py buy 600000.SH 100 --price 7.50 --strategy my_strat
# 3. 确认委托进了系统
python scripts/qmt.py orders
# 4. 需要时撤单
python scripts/qmt.py cancel <order_sysid> --market SH
```
### 场景四:批量行情分析
```bash
# 同时看多只股票的盘口
python scripts/qmt.py tick 600000.SH 000001.SZ 600519.SH
# 看板块成分股
python scripts/qmt.py sector "沪深A股"
# 看北向资金流向
python scripts/qmt.py north
```
## 安全须知
1. **下单默认关闭**:服务端 `rpc_allow_order_methods` 默认 `False`。必须由人工在服务端配置中
显式开启后才能下单,否则 `buy`/`sell`/`cancel` 会报 `ORDER_DISABLED` 错误。
2. **下单前先看价**:始终先用 `tick` 确认当前价格,避免下出明显不合理的委托。
3. **超时防重复**:如果 `buy`/`sell``ORDER_TIMEOUT`,委托可能已提交。**先用 `orders` 查询确认**
不要直接重试,避免重复下单。
4. **strategy_name 一致性**:下单时的 `--strategy` 和查询时的 `--strategy` 必须一致。
查全部委托用 `orders --strategy ""`(空字符串=不过滤)。
5. **实盘模式**:QMT 必须运行在实盘模式(非模拟/模型交易)才能收到完整回报。
## 脚本说明
### scripts/qmt.py
统一 CLI 入口,包含以下子命令:
**基础查询**
- `ping` — 连通性检测(含延迟测量)
- `account` — 查询账户资产(现金/冻结/总资产/市值)
- `positions [code]` — 查询持仓(含浮动盈亏计算)
- `orders [--cancelable] [--strategy ""]` — 查询今日委托(含语义化状态名)
- `trades [--strategy ""]` — 查询今日成交
- `snapshot` — 一键全景(资产+持仓+委托+成交)
**行情**
- `tick <codes...>` — 实时五档盘口(含涨跌幅计算)
- `kline <code> [--period 1d] [--count N] [--dividend front]` — K线(含 MA5/20/60 统计)
- `instrument <code>` — 合约详情
- `sector [name]` — 板块成分股/板块列表
- `trading-dates [--count N]` — 交易日历
- `north [--period 1d]` — 北向资金
- `longhubang <code> [--count N]` — 龙虎榜
- `financial <codes...> [--tables T1,T2]` — 财务数据
- `download <codes...>` — 下载历史数据到服务端
- `quote-subscribe <codes...> [--max N] [--timeout S]` — 实时全推行情订阅
**扩展查询**
- `holiday` — 节假日列表
- `stock-name <code>` — 股票名称
- `instrument-type <code>` — 品种类型
- `divid-factors <code>` — 除权除息因子
- `market-times [market]` — 日内交易时段
- `trading-calendar [market]` — 交易日历(含时段)
- `option-list <code>` — 期权列表
- `bsm-price` / `bsm-iv` — BSM 期权定价/隐含波动率
- `hkt-stats` / `hkt-details` / `hkt-rate` — 港股通统计/明细/汇率
- `top10-holder <code>` / `holder-num <code>` — 十大股东/股东户数
- `ipo` / `ipo-limit` — 新股数据/申购额度
- `credit-assure` / `credit-short` / `credit-debt` — 融资融券查询
- `his-st <code>` — 历史 ST 数据
- `index-weight <index>` — 指数权重
- `industry <name>` — 行业成分
- `sector-info [name]` — 板块详情
- `local-data <code>` — 本地缓存数据
- `timetag2dt` / `dt2timetag` — 时间戳转换
**交易**
- `buy <code> <volume> [--price P] [--latest]` — 买入下单
- `sell <code> <volume> [--price P] [--latest]` — 卖出下单
- `cancel <order_id> [--market SH]` — 撤单
**通用兜底**
- `rpc <method> [json_params]` — 调用任意白名单方法(未列出的方法都能这样调)
**配置自动发现**:脚本会自动把仓库 `src/` 加入 `sys.path`(开发模式直接运行,无需 pip install),并自动发现 QMT 的 python 目录(读 `local_config.py` 里的 transport 配置)。配置从环境变量(`BIGQMT_ACCOUNT_ID`/`BIGQMT_REDIS_HOST` 等)或配置文件读取。
**输出格式**:默认 JSON`ok`/`data`/`ts` 三字段),加 `--table` 切换表格输出。错误返回 `ok: false` + `error`/`detail`/`code`,退出码 1。
## 参考
详细的 API 参数、返回值结构、常量定义和已知陷阱见 `references/api_reference.md`
当命令速查不够用时(如需要直接 RPC 调用、查看信用交易类型、了解回调系统等),查阅该文件。
@@ -0,0 +1,523 @@
# QMT API 参考手册
本文档是 `qmt-trader` skill 的完整 API 参考。当 SKILL.md 的速查不够用时,查阅本文件获取
参数细节、返回值结构和已知陷阱。
---
## 1. 初始化与配置
### 配置来源(优先级从高到低)
1. **环境变量**
| 变量 | 默认 | 说明 |
|------|------|------|
| `BIGQMT_ACCOUNT_ID` | — | 资金账号 |
| `BIGQMT_REDIS_HOST` | `127.0.0.1` | Redis 地址 |
| `BIGQMT_REDIS_PORT` | `6379` | Redis 端口 |
| `BIGQMT_REDIS_DB` | `5` | Redis DB |
| `BIGQMT_REDIS_PASSWORD` | — | Redis 密码 |
| `BIGQMT_RPC_TRANSPORT` | `redis` | 传输方式 redis/zmq |
| `BIGQMT_RPC_TIMEOUT_SECONDS` | `6.0` | RPC 超时 |
2. **配置文件** `bigqmt_signal_trader_client_config.py`(在 PYTHONPATH 中,gitignored
3. **备选配置文件** `bigqmt_signal_trader_local_config.py`
### Python 初始化
```python
from bigqmt_signal_trader.xtquant_compat import StockAccount, configure, xt_trader, xtdata
configure() # 从配置/环境变量初始化
acc = StockAccount(xt_trader.client.account_id, "STOCK")
```
---
## 2. 行情数据 API
### 2.1 get_full_tick — 实时五档盘口
```python
xtdata.get_full_tick(code_list)
```
- **参数**: `code_list: list[str]`,如 `["000001.SZ", "600000.SH"]`;也支持整市场 `["SH"]`, `["SZ"]`
- **返回**: `dict[code -> dict]`,每只含 `lastPrice`/`open`/`high`/`low`/`lastClose`/`volume`/`amount`/
`bidPrice`(10档)/`askPrice`(10档)/`bidVol`/`askVol`/`time`/`stime`
- **CLI**: `python qmt.py tick 600000.SH 000001.SZ`
- **注意**: 整市场快照数据量大(5000+ 股),超时自动设 30 秒
### 2.2 get_market_data_ex — K线/历史行情
```python
xtdata.get_market_data_ex(
field_list=None, # ["close","open","high","low","volume","amount"] 或 None=全部
stock_list=None, # ["000001.SZ"]
period="1d", # "1d"/"1m"/"5m"/"15m"/"30m"/"60m"/"tick"
start_time="", # "YYYYMMDD" 或 "YYYYMMDDHHMMSS"
end_time="",
count=-1, # -1=不限
dividend_type="none", # "none"/"front"(前复权)/"back"(后复权)
fill_data=True, # 是否填充缺失
)
```
- **返回**: `dict[code -> pandas.DataFrame]`index 是时间戳字符串,列含 `time`(epoch ms)/`open`/`high`/`low`/`close`/`volume`/`amount`
- **CLI**: `python qmt.py kline 600000.SH --period 1d --count 60 --dividend front`
- **自愈**: 请求复权但服务端缺原始数据时(返回全 0),自动触发下载+重试
- **陷阱**: 前/后复权必须先在服务端下载原始数据,否则返回全 0(已自愈但仍可能首次慢)
### 2.3 get_instrument_detail — 合约详情
```python
xtdata.get_instrument_detail(stock_code) # 别名 get_instrumentdetail
```
- **返回**: `dict`,含名称/上市日/合约乘数/最小变动价位等约 30 字段
- **CLI**: `python qmt.py instrument 600000.SH`
### 2.4 get_stock_list_in_sector — 板块成分股
```python
xtdata.get_stock_list_in_sector(sector_name) # 如 "沪深A股", "科创板", "创业板"
```
- **返回**: `list[str]` 代码列表
- **CLI**: `python qmt.py sector "沪深A股"`
### 2.5 get_sector_list — 板块列表
```python
xtdata.get_sector_list()
```
- **返回**: `list[str]`
- **CLI**: `python qmt.py sector`
- **注意**: 大 QMT 环境 fallback 返回 13 个常用板块名(非完整列表)
### 2.6 get_trading_dates — 交易日历
```python
xtdata.get_trading_dates(market="SH", start_time="", end_time="", count=-1)
```
- **CLI**: `python qmt.py trading-dates --count 10`
### 2.7 get_north_finance_change — 北向资金
```python
xtdata.get_north_finance_change(period="1d")
```
- **CLI**: `python qmt.py north`
### 2.8 get_longhubang — 龙虎榜
```python
xtdata.get_longhubang(stock_list=["600000.SH"], start_time="", end_time="", count=5)
```
- **返回**: `pandas.DataFrame`
- **CLI**: `python qmt.py longhubang 600000.SH --count 5`
### 2.9 get_financial_data — 财务数据
```python
xtdata.get_financial_data(
stock_list=["000001.SZ"],
table_list=["Capital.CAPITAL"], # 表名
start_time="", end_time="",
)
```
- **CLI**: `python qmt.py financial 000001.SZ --tables Capital.CAPITAL`
### 2.10 download_history_data2 — 下载历史数据
```python
xtdata.download_history_data2(
stock_list=["600654.SH"], period="1d",
start_time="20240101", dividend_type="front",
)
```
- **返回**: `{"finished": N, "total": M}`
- **CLI**: `python qmt.py download 600654.SH --period 1d --start 20240101 --dividend front`
### 2.11 subscribe_whole_quote — 全推行情订阅
```python
sub_id = xtdata.subscribe_whole_quote(["SH","SZ"], callback=on_quote)
# ... 运行策略 ...
xtdata.unsubscribe_quote(sub_id)
```
- **机制**: 服务端真推送(非轮询),增量推送有变化的品种
- **CLI**: `python qmt.py quote-subscribe SH SZ --max 10 --timeout 30`
- **心跳**: 客户端 3 秒一次 keepalive,服务端重启后自动恢复
---
## 3. 账户/持仓/委托查询 API
### 3.1 query_stock_asset — 查询资产
```python
asset = xt_trader.query_stock_asset(acc)
```
- **返回属性**: `account_id` / `cash`(可用现金) / `frozen_cash` / `total_asset` / `market_value`
- **CLI**: `python qmt.py account`
- **容错**: RPC 失败时从 Redis 缓存 `bigqmt:positions:{account_id}` 读取
### 3.2 query_stock_positions — 查询全部持仓
```python
positions = xt_trader.query_stock_positions(acc)
```
- **返回属性**: `stock_code` / `stock_name` / `volume`(总持仓) / `can_use_volume`(可用) /
`avg_price`(成本) / `price`(最新价) / `market_value` / `frozen_volume` / `yesterday_volume`
- **CLI**: `python qmt.py positions [code]`
### 3.3 query_stock_position — 查询单只持仓
```python
pos = xt_trader.query_stock_position(acc, "600000.SH")
```
- **返回**: 单个对象或 `None`
### 3.4 query_stock_orders — 查询委托
```python
orders = xt_trader.query_stock_orders(acc, cancelable_only=False, strategy_name="")
```
- **返回属性**: `stock_code` / `order_type`(23=BUY,24=SELL) / `order_status` /
`order_volume` / `traded_volume` / `price` / `order_sysid` / `order_remark`
- **CLI**: `python qmt.py orders [--cancelable] [--strategy ""]`
- **⚠️ strategy_name 陷阱**: 下单时的 strategy_name 必须和查询时一致。服务端默认 `""` 返回全部;
客户端 `BigQmtXtTrader` 默认 `"bigqmt_signal_trader"`。用 `""` 查全部最安全。
### 3.5 query_stock_trades — 查询成交
```python
trades = xt_trader.query_stock_trades(acc, strategy_name="")
```
- **返回属性**: `stock_code` / `order_type` / `traded_volume` / `traded_price` /
`traded_at` / `order_sysid` / `trade_id`
- **CLI**: `python qmt.py trades`
### 3.6 委托状态码
| 值 | 常量 | 含义 |
|----|------|------|
| 48 | ORDER_UNREPORTED | 未申报 |
| 49 | ORDER_WAIT_REPORTING | 等待申报 |
| 50 | ORDER_REPORTED | 已申报 |
| 51 | ORDER_REPORTED_CANCEL | 已申报撤单 |
| 52 | ORDER_PARTSUCC_CANCEL | 部成撤单 |
| 53 | ORDER_PART_CANCEL | 部撤 |
| 54 | ORDER_CANCELED | 已撤 |
| 55 | ORDER_PART_SUCC | 部分成交 |
| 56 | ORDER_SUCCEEDED | 全部成交 |
| 57 | ORDER_JUNK | 废单 |
| 255 | ORDER_UNKNOWN | 未知 |
可撤状态: 49, 50, 55
---
## 4. 下单 API
### 4.1 order_stock — 同步下单
```python
from bigqmt_signal_trader.xtquant_compat import STOCK_BUY, STOCK_SELL, FIX_PRICE, LATEST_PRICE
order_id = xt_trader.order_stock(
acc, # StockAccount
stock_code, # "600000.SH"
order_type, # STOCK_BUY(23) / STOCK_SELL(24)
order_volume, # int,委托数量
price_type, # FIX_PRICE(11) / LATEST_PRICE(5)
price, # float,限价单价格(最新价时传 0)
strategy_name, # str
order_remark, # struser_order_id
)
```
- **返回**: `order_sys_id`(字符串) 或 `-1`(失败)
- **CLI**: `python qmt.py buy 600000.SH 100 --price 7.50 [--strategy s] [--remark r]`
- **CLI**: `python qmt.py sell 600000.SH 100 --price 7.50`
- **⚠️ 权限**: 服务端默认 `rpc_allow_order_methods=False`,必须显式开启才能下单
- **⚠️ 超时**: 超时后委托可能已提交,先查 `query_orders` 确认,避免重复下单
### 4.2 order_stock_async — 异步下单
```python
seq = xt_trader.order_stock_async(acc, code, order_type, vol, price_type, price, strategy, remark)
```
- **返回**: seq(结果通过 callback 回调)
### 4.3 order_stock_batch — 批量下单
```python
results = xt_trader.order_stock_batch(acc, orders, batch_id="")
# orders: list[dict],每项含 stock_code/action/volume/price/price_type/strategy_name
```
- **上限**: 500 条/批
### 4.4 信用交易委托类型
| 常量 | 值 | 用途 |
|------|-----|------|
| CREDIT_BUY | 23 | 担保品买入 |
| CREDIT_SELL | 24 | 担保品卖出 |
| CREDIT_FIN_BUY | 27 | 融资买入 |
| CREDIT_SLO_SELL | 28 | 融券卖出 |
| CREDIT_BUY_SECU_REPAY | 29 | 买券还券 |
| CREDIT_DIRECT_SECU_REPAY | 30 | 直接还券 |
| CREDIT_SELL_SECU_REPAY | 31 | 卖券还款 |
| CREDIT_DIRECT_CASH_REPAY | 32 | 直接还款 |
---
## 5. 撤单 API
### 5.1 cancel_order_stock_sysid
```python
success = xt_trader.cancel_order_stock_sysid(acc, market, order_sysid)
# market: "SH" / "SZ" / ""
```
- **CLI**: `python qmt.py cancel <order_sysid> --market SH`
### 5.2 cancel_order_stock
```python
success = xt_trader.cancel_order_stock(acc, order_id)
# 等价于 cancel_order_stock_sysid(acc, "", order_id)
```
---
## 6. 回调系统
```python
from bigqmt_signal_trader.xtquant_compat import XtQuantTraderCallback
class MyCallback(XtQuantTraderCallback):
def on_stock_order(self, order): ... # 委托变更
def on_stock_trade(self, trade): ... # 成交推送
def on_order_error(self, error): ... # 委托错误
def on_cancel_error(self, error): ... # 撤单错误
def on_order_stock_async_response(self, resp): ...
def on_account_status(self, status): ...
xt_trader.register_callback(MyCallback())
xt_trader.start()
xt_trader.connect()
xt_trader.subscribe(acc)
```
事件推送通过 Redis pubsub 频道:
- `bigqmt:exec:order:{account_id}`
- `bigqmt:exec:trade:{account_id}`
- `bigqmt:exec:order_error:{account_id}`
- `bigqmt:exec:cancel_error:{account_id}`
---
## 7. 关键陷阱速查
### 7.1 strategy_name 不匹配
- 下单用 `strategy_name="rpc_test"` → 查询用 `strategy_name="bigqmt_signal_trader"` → 返回空
- **解决**: 查询时传 `strategy_name=""` 返回全部,或保持一致
### 7.2 下单静默失败
- `passorder` 调用成功但委托没进系统(QMT 风控拒绝但没报错)
- **解决**: 服务端下单后等 0.5 秒查 `query_orders` 确认;检查返回的 `server_error` 字段
### 7.3 复权 K 线返回全 0
- 服务端缺原始数据时,前/后复权返回的 close 全是 0.0
- **解决**: 先 `download_history_data2` 下载原始数据(客户端有自愈机制)
### 7.4 Transport 不匹配
- 客户端 redis / 服务端 zmq → ping 超时
- **解决**: 两端 `transport` 字段保持一致
### 7.5 QMT 必须运行在实盘模式
- 模拟模式下委托进 QMT 界面但不在真实委托队列,`query_orders` 查不到
- `order_stock` 返回 -1,触发 `on_order_error`
### 7.6 整市场快照数据量大
- `get_full_tick(["SH"])` 返回 5000+ 股完整盘口
- **解决**: 启用 `full_tick_cache` 或增大超时(已自动设 30 秒)
### 7.7 全推行情是增量的
- `subscribe_whole_quote` 的大 QMT 回调只推有变化的品种
- **解决**: 订阅成功后客户端自动调一次 `get_full_tick` 打底
### 7.8 下单超时与重复下单
- `order_stock` 超时 → 委托可能已提交但没收到响应
- **解决**: 超时后先查 `query_orders`/`query_trades` 确认状态,再决定是否重试
---
## 8. 常量速查
### 交易常量
| 常量 | 值 | 用途 |
|------|-----|------|
| STOCK_BUY | 23 | 股票买入 |
| STOCK_SELL | 24 | 股票卖出 |
| FIX_PRICE | 11 | 限价/指定价 |
| LATEST_PRICE | 5 | 最新价 |
| MARKET_PEER_PRICE_FIRST | 44 | 对手方最优价 |
### 账号类型
| 常量 | 值 |
|------|-----|
| FUTURE_ACCOUNT | 1 |
| SECURITY_ACCOUNT | 2 |
| CREDIT_ACCOUNT | 3 |
| FUTURE_OPTION_ACCOUNT | 5 |
| STOCK_OPTION_ACCOUNT | 6 |
### 期货委托类型(部分)
| 常量 | 值 | 用途 |
|------|-----|------|
| FUTURE_OPEN_LONG | 0 | 开多 |
| FUTURE_CLOSE_LONG_TODAY | 2 | 平今多 |
| FUTURE_OPEN_SHORT | 3 | 开空 |
| FUTURE_CLOSE_SHORT_TODAY | 4 | 平今空 |
| FUTURE_CLOSE_LONG_HISTORY | 6 | 平昨多 |
| FUTURE_CLOSE_SHORT_HISTORY | 7 | 平昨空 |
---
## 9. 直接 RPC 调用(绕过兼容层)
当兼容层方法不够用时,可直接调 RPC:
```python
from bigqmt_signal_trader.redis_rpc import call_redis_rpc
import redis
r = redis.Redis(host="...", port=6379, db=5, password="...")
resp = call_redis_rpc(r, "ACCOUNT_ID", "get_full_tick", {"codes": ["000001.SZ"]})
print(resp["data"]["000001.SZ"]["lastPrice"])
```
- **万能入口**: `xtdata.call_method("get_float_caps", stockcode="000001.SZ")`
- **方法别名映射**:
- `get_full_tick``get_ticks`
- `get_instrument_detail``get_instrument`
- `query_stock_asset``get_asset`
- `query_stock_positions``get_positions`
- `query_stock_orders``query_orders`
- `query_stock_trades``query_trades`
- `order_stock``submit_order`
- `cancel_order_stock``cancel_order`
### RPC 响应结构
```json
{
"ok": true,
"data": {...},
"error": "",
"server_error": "",
"handled_at": "2024-07-01 15:00:00"
}
```
- `ok=true`: `data` 为方法返回值(DataFrame 已序列化,客户端自动还原 pandas 对象)
- `ok=false`: `error` 为错误信息
- `server_error`: 额外诊断(如 passorder 提交但委托未进系统)
---
## 10. 可用 RPC 方法白名单(117 个只读 + 3 个下单/撤单)
### 行情快照
`get_ticks`/`get_full_tick`, `get_instrument`/`get_instrument_detail`, `get_instrument_type`,
`get_stock_name`, `get_stock_type`, `get_last_close`, `get_last_volume`, `get_float_caps`,
`get_total_share`, `get_turn_over_rate`, `get_weight_in_index`, `get_contract_multiplier`,
`get_contract_expire_date`, `get_open_date`, `get_svol`, `get_bvol`, `get_risk_free_rate`,
`is_stock_type`, `get_cb_info`
### K线/历史
`get_market_data`, `get_market_data_ex`, `get_local_data`, `get_close_price`, `get_index_weight`
### L2 行情(需 L2 权限)
`get_l2_quote`, `get_l2_order`, `get_l2_transaction`, `subscribe_l2thousand`
### 板块
`get_stock_list_in_sector`, `get_sector_list`, `get_sector_info`, `create_sector`, `add_sector`, `remove_sector`
### 交易日历/时段
`get_trading_dates`, `get_holidays`, `get_markets`, `get_market_last_trade_date`,
`get_date_location`, `get_trading_calendar`, `get_trade_times`
### 数据下载
`download_history_data`, `download_history_data2`, `download_holiday_data`,
`download_etf_info`, `download_cb_data`, `download_history_contracts`,
`download_index_weight`, `download_sector_data`
### 财务/因子
`get_financial_data`, `download_financial_data`, `download_financial_data2`,
`get_raw_financial_data`, `get_factor_data`
### ETF/期权/期货
`get_etf_info`, `get_ipo_info`, `get_option_list`, `get_his_option_list`,
`get_his_option_list_batch`, `get_option_detail_data`, `get_option_undl_data`,
`get_option_undl`, `get_ETF_list`, `get_main_contract`, `get_his_contract_list`
### 期权定价
`bsm_price`, `bsm_iv`, `get_option_iv`
### 龙虎榜/股东
`get_longhubang`, `get_top10_share_holder`, `get_holder_num`, `get_turnover_rate`,
`get_industry`, `get_his_st_data`, `get_his_index_data`
### 资金流
`get_north_finance_change`, `get_hkt_statistics`, `get_hkt_details`, `get_hkt_exchange_rate`
### 因子/模型
`call_formula`, `subscribe_formula`, `unsubscribe_formula`, `get_formula_result`, `gen_factor_index`
### 时间转换(纯本地)
`datetime_to_timetag`, `timetag_to_datetime`
### 账户查询
`get_asset`, `get_positions`, `query_stock_position`, `query_orders`, `query_trades`,
`get_history_trade_detail_data`, `get_value_by_order_id`, `get_last_order_id`
### 融资融券(需两融权限)
`get_assure_contract`, `get_enable_short_contract`, `get_unclosed_compacts`,
`get_closed_compacts`, `get_debt_contract`
### 期权持仓
`get_option_subject_position`, `get_comb_option`
### 持仓同步
`sync_positions`
### 下单/撤单(需开启 rpc_allow_order_methods
`submit_order`/`order_stock`, `submit_orders_batch`/`order_stock_batch`,
`cancel_order`/`cancel_order_stock`/`cancel_order_stock_sysid`
### 全推行情
`subscribe_whole_quote`, `unsubscribe_whole_quote`, `quote_keepalive`
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,108 @@
# coding: utf-8
"""Run the full test suite from a single entry point.
Groups tests by area and prints a clear per-group + total report. Optional
live/API tests (need a running QMT + redis) are skipped by default.
Usage:
python run_all_tests.py # all offline tests (default)
python run_all_tests.py -v # verbose
python run_all_tests.py --live # also run live RPC tests (needs QMT running)
python run_all_tests.py --group signal_trader # only one group
python run_all_tests.py --group backtest
"""
import argparse
import os
import subprocess
import sys
import time
ROOT = os.path.dirname(os.path.abspath(__file__))
SRC = os.path.join(ROOT, "src")
# Test groups: (name, paths, requires_live)
GROUPS = [
("signal_trader", [os.path.join("tests", "bigqmt_signal_trader")], False),
("backtest", [os.path.join("tests", "bigqmt_backtest")], False),
]
LIVE_GROUP = ("live_api", ["test_all_apis.py"], True)
def run_group(name, paths, verbose):
"""Run a pytest group, return (passed, failed, skipped, seconds)."""
cmd = [sys.executable, "-m", "pytest"] + paths + ["-q" if not verbose else "-v"]
t0 = time.time()
proc = subprocess.run(cmd, cwd=ROOT, capture_output=True, text=True)
elapsed = time.time() - t0
out = (proc.stdout or "") + (proc.stderr or "")
# Parse pytest summary like "290 passed in 8.5s" / "1 failed, 289 passed, 3 skipped"
passed = failed = skipped = 0
for line in out.splitlines():
line = line.strip()
if not any(tok in line for tok in ("passed", "failed", "skipped", "error")):
continue
for part in line.split(","):
words = part.strip().split()
if len(words) >= 2 and words[0].isdigit():
num = int(words[0])
if words[1].startswith("passed"):
passed += num
elif words[1].startswith("failed") or words[1].startswith("error"):
failed += num
elif words[1].startswith("skipped"):
skipped += num
return passed, failed, skipped, elapsed, out, proc.returncode
def main():
parser = argparse.ArgumentParser(description="Run all bigqmt tests")
parser.add_argument("-v", "--verbose", action="store_true")
parser.add_argument("--live", action="store_true", help="also run live RPC tests (needs QMT)")
parser.add_argument("--group", help="run only this group (signal_trader/backtest/live_api)")
args = parser.parse_args()
groups = list(GROUPS)
if args.live:
groups.append(LIVE_GROUP)
if args.group:
groups = [g for g in groups if g[0] == args.group]
if not groups:
print("Unknown group: %s (available: %s)" % (args.group, ", ".join(g[0] for g in GROUPS + [LIVE_GROUP])))
return 1
print("=" * 70)
print("Big QMT Bridge - 全量测试")
print("=" * 70)
total_passed = total_failed = total_skipped = 0
total_time = 0.0
failed_groups = []
for name, paths, needs_live in groups:
print("\n--- %s ---" % name)
passed, failed, skipped, elapsed, out, rc = run_group(name, paths, args.verbose)
total_passed += passed
total_failed += failed
total_skipped += skipped
total_time += elapsed
status = "PASS" if failed == 0 and rc == 0 else "FAIL"
print(" %s: %d passed, %d failed, %d skipped (%.1fs)" % (status, passed, failed, skipped, elapsed))
if failed or rc != 0:
failed_groups.append(name)
if not args.verbose:
# print the failing part of the output for visibility
tail = "\n".join(out.splitlines()[-20:])
print(tail)
print("\n" + "=" * 70)
print("=== 汇总 ===")
print("通过 %d / 失败 %d / 跳过 %d / 总计 %d" % (total_passed, total_failed, total_skipped, total_passed + total_failed + total_skipped))
print("总耗时 %.1fs" % total_time)
if failed_groups:
print("失败分组: %s" % ", ".join(failed_groups))
return 1
print("全部通过 ✅")
return 0
if __name__ == "__main__":
sys.exit(main())
@@ -0,0 +1,254 @@
#coding:gbk
"""QMT bridge entry using the same file-loader pattern as qmt_realtime strategies.
Broker QMT strategy sandboxes may reject local package names through their
normal ``import`` allowlist. The realtime QMT strategies in gupiao_ztfx load
their colocated helpers through ``importlib.util.spec_from_file_location``.
This entry applies path-based loading to the bridge package, including its
internal relative imports, while leaving all standard-library and QMT imports
untouched. This terminal's spec loader ignores custom builtins for nested
package imports, so local bridge files are compiled explicitly after resolving
their path.
"""
import builtins as _builtins
import importlib as _importlib
import os
import sys
import types
_LOCAL_ROOTS = (
"bigqmt_signal_trader",
"bigqmt_signal_trader_strategy",
"bigqmt_signal_trader_redis_rpc_runtime",
"bigqmt_signal_trader_local_config",
)
_ORIGINAL_IMPORT = _builtins.__import__
_ORIGINAL_IMPORT_MODULE = _importlib.import_module
_ORIGINAL_RELOAD = _importlib.reload
def _known_qmt_python_dir():
# Find the QMT python dir from sys.path instead of a hardcoded path, so
# the bridge loads regardless of broker install location or launch mode
# (editor / paste-run / exec). Falls back to empty when not found.
for p in sys.path:
if p and r"\python" in p and os.path.isdir(p):
return p
return ""
try:
_SOURCE_ROOT = os.path.dirname(os.path.abspath(__file__))
except Exception:
_SOURCE_ROOT = _known_qmt_python_dir()
if not _SOURCE_ROOT:
_SOURCE_ROOT = _known_qmt_python_dir()
def _is_local_module(name):
return any(name == root or name.startswith(root + ".") for root in _LOCAL_ROOTS)
def _resolve_name(name, module_globals, level):
if not level:
return name
package = (module_globals or {}).get("__package__") or (module_globals or {}).get("__name__", "")
if not package:
raise ImportError("relative import without package")
for unused in range(level - 1):
if "." not in package:
raise ImportError("relative import beyond top-level package")
package = package.rsplit(".", 1)[0]
return package + ("." + name if name else "")
def _find_local_source(name):
relative = name.replace(".", os.sep)
dirs = []
if _SOURCE_ROOT:
dirs.append(_SOURCE_ROOT)
for p in sys.path:
if p and os.path.isdir(p) and p not in dirs:
dirs.append(p)
for d in dirs:
package_init = os.path.join(d, relative, "__init__.py")
if os.path.isfile(package_init):
return package_init, True
module_file = os.path.join(d, relative + ".py")
if os.path.isfile(module_file):
return module_file, False
raise ModuleNotFoundError("local source not found: %s" % name, name=name)
def _set_parent_attribute(name, module):
if "." not in name:
return
parent_name, child_name = name.rsplit(".", 1)
parent = _load_local_module(parent_name)
setattr(parent, child_name, module)
def _load_local_module(name):
existing = sys.modules.get(name)
if existing is not None:
return existing
source_path, is_package = _find_local_source(name)
if "." in name:
_load_local_module(name.rsplit(".", 1)[0])
module = types.ModuleType(name)
module.__file__ = source_path
module.__package__ = name if is_package else name.rpartition(".")[0]
if is_package:
module.__path__ = [os.path.dirname(source_path)]
module_builtins = dict(_builtins.__dict__)
module_builtins["__import__"] = _local_import
module.__dict__["__builtins__"] = module_builtins
module.__dict__["__bigqmt_load_local_module"] = _load_local_module
sys.modules[name] = module
# QMT native allowlist rejects the root package eager exports.
if name == "bigqmt_signal_trader":
return module
try:
with open(source_path, "rb") as source_file:
source = source_file.read()
exec(compile(source, source_path, "exec"), module.__dict__)
except Exception:
sys.modules.pop(name, None)
raise
_set_parent_attribute(name, module)
return module
def _local_import(name, module_globals=None, module_locals=None, fromlist=(), level=0):
absolute_name = _resolve_name(name, module_globals, level)
if not _is_local_module(absolute_name):
return _ORIGINAL_IMPORT(name, module_globals, module_locals, fromlist, level)
module = _load_local_module(absolute_name)
for child in fromlist or ():
if child != "*":
try:
_load_local_module(absolute_name + "." + child)
except ModuleNotFoundError:
pass
if fromlist:
return module
return _load_local_module(absolute_name.split(".", 1)[0])
def _local_import_module(name, package=None):
if _is_local_module(name):
return _load_local_module(name)
return _ORIGINAL_IMPORT_MODULE(name, package)
def _local_reload(module):
if _is_local_module(getattr(module, "__name__", "")):
return _load_local_module(module.__name__)
return _ORIGINAL_RELOAD(module)
def _clear_local_modules():
for name in list(sys.modules):
if _is_local_module(name):
sys.modules.pop(name, None)
def _stop_previous_rpc_service():
"""Release the previous QMT strategy's socket before clearing its module.
QMT can re-execute this entry in the same Python process. The old strategy
module owns the RPC service and its ZMQ ROUTER socket, so dropping that
module from ``sys.modules`` first would make the service unreachable and
leave its port bound for the next strategy start.
"""
previous = sys.modules.get("bigqmt_signal_trader_strategy")
reset = getattr(previous, "reset_app", None)
if not callable(reset):
return
try:
reset()
print("[bigqmt_shell] previous rpc service stopped")
except Exception as exc:
# Continue the reload so a broken old instance does not prevent QMT
# from reporting its normal startup error.
print("[bigqmt_shell] previous rpc service stop failed: %s" % exc)
_stop_previous_rpc_service()
_clear_local_modules()
_importlib.import_module = _local_import_module
_importlib.reload = _local_reload
print("[bigqmt_shell] importlib entry source_root=%s" % _SOURCE_ROOT)
def _fallback_account_id():
for name in ("BIGQMT_ACCOUNT_ID", "account", "account_id", "accountID"):
value = globals().get(name)
if value:
return str(value)
return ""
try:
_local_import("bigqmt_signal_trader.adapters.redis_common", globals(), fromlist=("*",))
_local_import("bigqmt_signal_trader.redis_rpc", globals(), fromlist=("*",))
_strategy = _local_import("bigqmt_signal_trader_strategy", globals(), fromlist=("*",))
_strategy.reset_app()
except Exception as bridge_preload_error:
print("[bigqmt_shell] bridge preload failed: %s" % bridge_preload_error)
_runtime = _local_import("bigqmt_signal_trader_redis_rpc_runtime", globals(), fromlist=("*",))
def _load_local_config():
return _local_import("bigqmt_signal_trader_local_config", globals(), fromlist=("*",))
try:
_config = _load_local_config()
BIGQMT_REDIS_CONFIG = getattr(_config, "BIGQMT_REDIS_CONFIG", {})
print("[bigqmt_shell] local redis config loaded keys=%s" % sorted((BIGQMT_REDIS_CONFIG or {}).keys()))
_runtime.configure_runtime_redis(BIGQMT_REDIS_CONFIG)
except Exception as redis_config_error:
print("[bigqmt_shell] local redis config load failed: %s" % redis_config_error)
try:
_config = _load_local_config()
BIGQMT_ACCOUNT_ID = getattr(_config, "BIGQMT_ACCOUNT_ID", "")
print("[bigqmt_shell] local account config loaded=%s" % bool(BIGQMT_ACCOUNT_ID))
_runtime.configure_runtime_account(BIGQMT_ACCOUNT_ID)
except Exception as account_config_error:
print("[bigqmt_shell] local account config load failed: %s" % account_config_error)
account_id = _fallback_account_id()
if account_id:
_runtime.configure_runtime_account(account_id)
try:
qmt_extra = {}
for function_name in (
"get_history_trade_detail_data", "get_value_by_order_id", "get_last_order_id",
"get_ipo_data", "get_new_purchase_limit", "get_assure_contract",
"get_enable_short_contract", "get_unclosed_compacts", "get_closed_compacts",
"get_debt_contract", "get_option_subject_position", "get_comb_option",
"get_hkt_exchange_rate",
"download_history_data", "download_history_data2",
):
if function_name in globals():
qmt_extra[function_name] = globals()[function_name]
print("[bigqmt_shell] down_history_data bound=%s" % ("down_history_data" in qmt_extra))
_runtime.bind_runtime_api(
passorder_func=globals().get("passorder"),
cancel_func=globals().get("cancel"),
get_trade_detail_data_func=globals().get("get_trade_detail_data"),
extra_funcs=qmt_extra or None,
)
except NameError:
pass
init = _runtime.init
handlebar = _runtime.handlebar
adjust = _runtime.adjust
order_callback = _runtime.order_callback
deal_callback = _runtime.deal_callback
@@ -0,0 +1,152 @@
#coding:gbk
"""Isolated QMT backtest entry for external ZMQ strategies.
This file is ASCII-only. It loads only the bigqmt_backtest package and never
loads or mutates the live bridge package.
"""
import builtins as _builtins
import os
import sys
import types
BACKTEST_ZMQ_CONFIG = {
"bind_endpoint": "tcp://127.0.0.1:16662",
"run_id": "",
"account_id": "",
"account_type": "STOCK",
"strategy_name": "ZMQ_BACKTEST",
"combo_type": 1101,
"quick_trade": 2,
"market_price_type": 5,
"limit_price_type": 11,
"bar_wait_timeout_seconds": 60,
"require_qmt_backtest": True,
}
_LOCAL_ROOT = "bigqmt_backtest"
_ORIGINAL_IMPORT = _builtins.__import__
def _known_qmt_python_dir():
for p in sys.path:
if p and r"\python" in p and os.path.isdir(p):
return p
return ""
try:
_SOURCE_ROOT = os.path.dirname(os.path.abspath(__file__))
except Exception:
_SOURCE_ROOT = _known_qmt_python_dir()
if not _SOURCE_ROOT:
_SOURCE_ROOT = _known_qmt_python_dir()
def _is_local(name):
return name == _LOCAL_ROOT or name.startswith(_LOCAL_ROOT + ".")
def _resolve_name(name, module_globals, level):
if not level:
return name
package = (module_globals or {}).get("__package__") or ""
if not package:
raise ImportError("relative import without package")
for unused in range(level - 1):
package = package.rsplit(".", 1)[0]
return package + (("." + name) if name else "")
def _find_source(name):
relative = name.replace(".", os.sep)
dirs = []
if _SOURCE_ROOT:
dirs.append(_SOURCE_ROOT)
for p in sys.path:
if p and os.path.isdir(p) and p not in dirs:
dirs.append(p)
for d in dirs:
package_init = os.path.join(d, relative, "__init__.py")
if os.path.isfile(package_init):
return package_init, True
module_file = os.path.join(d, relative + ".py")
if os.path.isfile(module_file):
return module_file, False
raise ModuleNotFoundError("local source not found: %s" % name, name=name)
def _load_local_module(name):
existing = sys.modules.get(name)
if existing is not None:
return existing
source_path, is_package = _find_source(name)
if "." in name:
_load_local_module(name.rsplit(".", 1)[0])
module = types.ModuleType(name)
module.__file__ = source_path
module.__package__ = name if is_package else name.rpartition(".")[0]
if is_package:
module.__path__ = [os.path.dirname(source_path)]
module_builtins = dict(_builtins.__dict__)
module_builtins["__import__"] = _local_import
module.__dict__["__builtins__"] = module_builtins
sys.modules[name] = module
if name == _LOCAL_ROOT:
return module
try:
with open(source_path, "rb") as source_file:
source = source_file.read()
exec(compile(source, source_path, "exec"), module.__dict__)
except Exception:
sys.modules.pop(name, None)
raise
if "." in name:
parent_name, child_name = name.rsplit(".", 1)
setattr(_load_local_module(parent_name), child_name, module)
return module
def _local_import(name, module_globals=None, module_locals=None, fromlist=(), level=0):
absolute_name = _resolve_name(name, module_globals, level)
if not _is_local(absolute_name):
return _ORIGINAL_IMPORT(name, module_globals, module_locals, fromlist, level)
module = _load_local_module(absolute_name)
for child in fromlist or ():
if child != "*":
try:
_load_local_module(absolute_name + "." + child)
except ModuleNotFoundError:
pass
if fromlist:
return module
return _load_local_module(absolute_name.split(".", 1)[0])
for _name in sorted(
[name for name in list(sys.modules) if _is_local(name)],
key=lambda item: item.count("."),
reverse=True,
):
sys.modules.pop(_name, None)
_runtime = _load_local_module("bigqmt_backtest.qmt_runtime")
_runtime.configure(**BACKTEST_ZMQ_CONFIG)
_runtime.bind_qmt_api(
passorder_func=globals().get("passorder") or getattr(_builtins, "passorder", None),
cancel_func=globals().get("cancel") or getattr(_builtins, "cancel", None),
get_trade_detail_data_func=(
globals().get("get_trade_detail_data")
or getattr(_builtins, "get_trade_detail_data", None)
),
)
init = _runtime.init
handlebar = _runtime.handlebar
order_callback = _runtime.order_callback
deal_callback = _runtime.deal_callback
stop = _runtime.stop
after_backtest = _runtime.after_backtest
@@ -0,0 +1,23 @@
"""Isolated ZMQ bridge for QMT-native and standalone backtests.
This package deliberately does not import ``bigqmt_signal_trader``. The live
bridge and both backtest backends therefore have separate module state,
identities, and order gateways. QMT-native mode never uses the local broker.
"""
from .client import BacktestZmqClient
from .data_feed import CsvBarFeed, InMemoryBarFeed
from .engine import BacktestConfig, BacktestEngine
from .protocol import BacktestBridgeProtocol
__all__ = [
"BacktestBridgeProtocol",
"BacktestConfig",
"BacktestEngine",
"BacktestZmqClient",
"CsvBarFeed",
"InMemoryBarFeed",
]
__version__ = "1.0.0"
@@ -0,0 +1,5 @@
from .server import main
if __name__ == "__main__":
main()
@@ -0,0 +1,358 @@
"""A-share simulated broker used only by the standalone backtest runtime."""
import json
from decimal import Decimal
from .models import (
BacktestFill,
BacktestOrder,
Position,
ZERO,
decimal_value,
json_number,
money,
normalize_symbol,
round_price,
)
ACTIVE_ORDER_STATUSES = ("PENDING", "PARTIALLY_FILLED")
class SimulatedBroker(object):
def __init__(self, config):
self.config = config
self.cash = money(config.initial_cash)
self.positions = {}
for symbol, payload in dict(config.initial_positions or {}).items():
data = dict(payload or {})
position = Position(
symbol,
quantity=data.get("quantity", data.get("volume", 0)),
available=data.get("available"),
today_buy=data.get("today_buy", 0),
avg_cost=data.get("avg_cost", data.get("cost", 0)),
)
if position.quantity > 0:
self.positions[position.symbol] = position
self.orders_list = []
self.fills_list = []
self._client_order_ids = {}
self._client_order_fingerprints = {}
self._order_sequence = 0
self._fill_sequence = 0
self._trading_date = None
self.total_fees = ZERO
self.turnover = ZERO
def _new_order(self, payload, frame_index, submitted_at):
self._order_sequence += 1
return BacktestOrder(
order_id="bt-order-%06d" % self._order_sequence,
client_order_id=payload.get("client_order_id"),
symbol=payload.get("symbol"),
side=payload.get("side"),
quantity=payload.get("quantity"),
order_type=payload.get("order_type", "MARKET"),
limit_price=payload.get("limit_price", payload.get("price")),
submitted_index=frame_index,
submitted_at=submitted_at,
time_in_force=payload.get("time_in_force", self.config.time_in_force),
)
def _reject(self, order, reason):
order.status = "REJECTED"
order.reject_reason = str(reason)
return order
def _reserved_sell(self, symbol):
return sum(
order.remaining
for order in self.orders_list
if order.symbol == symbol and order.side == "SELL" and order.status in ACTIVE_ORDER_STATUSES
)
def submit(self, payload, frame_index, submitted_at):
payload = dict(payload or {})
client_order_id = str(payload.get("client_order_id") or "")
if client_order_id and client_order_id in self._client_order_ids:
limit_value = payload.get("limit_price", payload.get("price"))
fingerprint_payload = {
"symbol": normalize_symbol(payload.get("symbol")),
"side": str(payload.get("side") or "").upper(),
"quantity": int(payload.get("quantity") or 0),
"order_type": str(payload.get("order_type") or "MARKET").upper(),
"limit_price": None if limit_value in (None, "") else float(decimal_value(limit_value)),
"time_in_force": str(payload.get("time_in_force", self.config.time_in_force)).upper(),
}
fingerprint = json.dumps(fingerprint_payload, sort_keys=True, separators=(",", ":"))
if self._client_order_fingerprints.get(client_order_id) != fingerprint:
raise ValueError("client_order_id reused with different order payload")
return self._client_order_ids[client_order_id]
try:
symbol = normalize_symbol(payload.get("symbol"))
side = str(payload.get("side") or "").upper()
quantity = int(payload.get("quantity") or 0)
order_type = str(payload.get("order_type") or "MARKET").upper()
if side not in ("BUY", "SELL"):
raise ValueError("side must be BUY or SELL")
if quantity <= 0:
raise ValueError("quantity must be positive")
if order_type not in ("MARKET", "LIMIT"):
raise ValueError("order_type must be MARKET or LIMIT")
if order_type == "LIMIT" and decimal_value(payload.get("limit_price", payload.get("price"))) <= 0:
raise ValueError("positive limit_price is required for LIMIT order")
payload.update({"symbol": symbol, "side": side, "quantity": quantity, "order_type": order_type})
order = self._new_order(payload, frame_index, submitted_at)
except Exception as exc:
self._order_sequence += 1
order = BacktestOrder(
"bt-order-%06d" % self._order_sequence,
client_order_id,
payload.get("symbol") or "UNKNOWN",
payload.get("side") or "UNKNOWN",
int(payload.get("quantity") or 0),
payload.get("order_type") or "MARKET",
payload.get("limit_price", payload.get("price")),
frame_index,
submitted_at,
payload.get("time_in_force", self.config.time_in_force),
)
self._reject(order, "invalid_order:%s" % exc)
self.orders_list.append(order)
return order
self.orders_list.append(order)
if client_order_id:
self._client_order_ids[client_order_id] = order
fingerprint_payload = {
"symbol": order.symbol,
"side": order.side,
"quantity": order.quantity,
"order_type": order.order_type,
"limit_price": None if order.limit_price is None else float(order.limit_price),
"time_in_force": order.time_in_force,
}
self._client_order_fingerprints[client_order_id] = json.dumps(
fingerprint_payload, sort_keys=True, separators=(",", ":")
)
lot_size = self.config.lot_size
if order.side == "BUY" and order.quantity % lot_size:
return self._reject(order, "buy_quantity_not_round_lot")
if order.side == "SELL":
position = self.positions.get(order.symbol)
available = 0 if position is None else max(position.available - self._reserved_sell(order.symbol) + order.quantity, 0)
if available <= 0:
return self._reject(order, "t_plus_one_unavailable")
if order.quantity > available:
return self._reject(order, "insufficient_sellable")
if order.quantity % lot_size and order.quantity != available:
return self._reject(order, "sell_quantity_not_round_lot")
return order
def cancel(self, order_id):
for order in self.orders_list:
if order.order_id == str(order_id) or order.client_order_id == str(order_id):
if order.status not in ACTIVE_ORDER_STATUSES:
return order
order.status = "CANCELLED"
order.reject_reason = "cancelled_by_strategy"
return order
raise KeyError("order not found: %s" % order_id)
def _settle_trading_day(self, trading_date):
if self._trading_date == trading_date:
return
if self._trading_date is not None:
for position in self.positions.values():
position.available = position.quantity
position.today_buy = 0
self._trading_date = trading_date
def _limits(self, order, bar):
prev_close = decimal_value(bar.get("prev_close") or bar.get("close"))
if bar.get("price_limit_rate") not in (None, ""):
rate = decimal_value(bar.get("price_limit_rate"))
else:
pure = order.symbol.split(".", 1)[0]
if order.symbol.endswith(".BJ"):
rate = Decimal("0.30")
elif pure.startswith(("300", "301", "688", "689")):
rate = Decimal("0.20")
else:
rate = decimal_value(self.config.price_limit_rate)
up_limit = bar.get("up_limit")
down_limit = bar.get("down_limit")
up_limit = round_price(order.symbol, up_limit if up_limit not in (None, "") else prev_close * (Decimal("1") + rate))
down_limit = round_price(order.symbol, down_limit if down_limit not in (None, "") else prev_close * (Decimal("1") - rate))
return up_limit, down_limit
def _match_price(self, order, bar):
if bool(bar.get("suspended")) or float(bar.get("volume") or 0) <= 0:
return None, "suspended_or_no_volume"
open_price = round_price(order.symbol, bar["open"])
high = round_price(order.symbol, bar["high"])
low = round_price(order.symbol, bar["low"])
up_limit, down_limit = self._limits(order, bar)
if order.side == "BUY" and open_price == high == low == up_limit:
return None, "limit_up_locked"
if order.side == "SELL" and open_price == high == low == down_limit:
return None, "limit_down_locked"
if order.order_type == "LIMIT" and not down_limit <= order.limit_price <= up_limit:
return None, "limit_price_outside_daily_range"
if order.order_type == "MARKET":
price = open_price
elif order.side == "BUY":
if low > order.limit_price:
return None, "limit_not_crossed"
price = min(open_price, order.limit_price)
else:
if high < order.limit_price:
return None, "limit_not_crossed"
price = max(open_price, order.limit_price)
slip = decimal_value(self.config.slippage_bps) / Decimal("10000")
if order.side == "BUY":
price = min(round_price(order.symbol, price * (Decimal("1") + slip)), up_limit)
else:
price = max(round_price(order.symbol, price * (Decimal("1") - slip)), down_limit)
return price, ""
def _fees(self, side, amount):
rate = self.config.buy_commission_rate if side == "BUY" else self.config.sell_commission_rate
commission = max(amount * decimal_value(rate), decimal_value(self.config.min_commission)) if rate else ZERO
stamp = amount * decimal_value(self.config.stamp_tax_rate) if side == "SELL" else ZERO
transfer = amount * decimal_value(self.config.transfer_fee_rate)
return money(commission), money(stamp), money(transfer)
def _volume_cap(self, order, bar, used_volume=0):
raw = int(float(bar.get("volume") or 0) * float(self.config.max_volume_participation))
cap = max((raw // self.config.lot_size) * self.config.lot_size - int(used_volume), 0)
return min(order.remaining, cap)
def _affordable_buy_quantity(self, quantity, price):
quantity = (int(quantity) // self.config.lot_size) * self.config.lot_size
while quantity > 0:
amount = money(price * quantity)
fees = sum(self._fees("BUY", amount), ZERO)
if self.cash >= amount + fees:
return quantity
quantity -= self.config.lot_size
return 0
def _apply_fill(self, order, quantity, price, frame_index, filled_at):
amount = money(price * quantity)
commission, stamp, transfer = self._fees(order.side, amount)
self._fill_sequence += 1
fill = BacktestFill(
"bt-fill-%06d" % self._fill_sequence,
order,
quantity,
price,
commission,
stamp,
transfer,
frame_index,
filled_at,
)
fees = fill.total_fee
position = self.positions.get(order.symbol)
if order.side == "BUY":
if position is None:
position = Position(order.symbol)
self.positions[order.symbol] = position
old_cost = position.avg_cost * position.quantity
self.cash = money(self.cash - amount - fees)
position.quantity += quantity
position.today_buy += quantity
position.avg_cost = (old_cost + amount + fees) / position.quantity
else:
if position is None or position.available < quantity:
raise RuntimeError("sellable quantity changed before fill")
self.cash = money(self.cash + amount - fees)
position.quantity -= quantity
position.available -= quantity
position.realized_pnl += amount - fees - position.avg_cost * quantity
if position.quantity <= 0:
self.positions.pop(order.symbol, None)
order.filled_quantity += quantity
order.status = "FILLED" if order.remaining == 0 else "PARTIALLY_FILLED"
self.total_fees += fees
self.turnover += amount
self.fills_list.append(fill)
return fill
def advance(self, frame_index, frame):
trading_date = str(frame["datetime"])[:10]
self._settle_trading_day(trading_date)
fills = []
used_volume = {}
for order in self.orders_list:
if order.status not in ACTIVE_ORDER_STATUSES or frame_index <= order.submitted_index:
continue
bar = frame["bars"].get(order.symbol)
if bar is None:
continue
if order.time_in_force == "DAY" and str(order.submitted_at)[:10] != trading_date:
order.status = "EXPIRED"
order.reject_reason = "day_order_expired"
continue
order.last_attempt_index = frame_index
price, reason = self._match_price(order, bar)
if price is None:
if order.time_in_force == "NEXT_BAR":
order.status = "EXPIRED"
order.reject_reason = reason
continue
quantity = self._volume_cap(order, bar, used_volume.get(order.symbol, 0))
if quantity <= 0:
reason = "volume_participation_exhausted"
elif order.side == "BUY":
quantity = self._affordable_buy_quantity(quantity, price)
if quantity <= 0:
reason = "insufficient_cash"
else:
position = self.positions.get(order.symbol)
quantity = min(quantity, 0 if position is None else position.available)
if quantity <= 0:
reason = "t_plus_one_unavailable"
if quantity > 0:
fills.append(self._apply_fill(order, quantity, price, frame_index, frame["datetime"]))
used_volume[order.symbol] = used_volume.get(order.symbol, 0) + quantity
if order.time_in_force == "NEXT_BAR" and order.remaining > 0:
if order.filled_quantity == 0:
order.status = "EXPIRED"
else:
order.status = "PARTIALLY_FILLED_EXPIRED"
order.reject_reason = reason or "next_bar_remaining_expired"
return fills
def expire_open_orders(self, reason="backtest_finished"):
for order in self.orders_list:
if order.status in ACTIVE_ORDER_STATUSES:
order.status = "EXPIRED"
order.reject_reason = reason
def snapshot(self, bars):
positions = {}
market_value = ZERO
for symbol in sorted(self.positions):
position = self.positions[symbol]
bar = bars.get(symbol) or {}
mark = decimal_value(bar.get("close"), position.avg_cost)
market_value += mark * position.quantity
positions[symbol] = position.to_dict(mark)
total_asset = money(self.cash + market_value)
return {
"cash": json_number(self.cash, 2),
"market_value": json_number(money(market_value), 2),
"total_asset": json_number(total_asset, 2),
"positions": positions,
"total_fees": json_number(money(self.total_fees), 2),
"turnover": json_number(money(self.turnover), 2),
}
def orders(self):
return [order.to_dict() for order in self.orders_list]
def fills(self):
return [fill.to_dict() for fill in self.fills_list]
@@ -0,0 +1,134 @@
"""External-strategy client SDK for the ZMQ backtest bridge."""
import json
import uuid
class BacktestRemoteError(RuntimeError):
pass
class BacktestZmqClient(object):
def __init__(
self,
endpoint,
run_id,
client_id="external-strategy",
timeout_seconds=10.0,
):
self.endpoint = str(endpoint)
self.run_id = str(run_id)
self.client_id = str(client_id)
self.timeout_seconds = float(timeout_seconds)
self._context = None
self._socket = None
def _connect(self):
if self._socket is not None:
return self._socket
import zmq
self._context = zmq.Context.instance()
self._socket = self._context.socket(zmq.REQ)
self._socket.setsockopt(zmq.LINGER, 0)
self._socket.connect(self.endpoint)
return self._socket
def _reset_socket(self):
if self._socket is not None:
self._socket.close(linger=0)
self._socket = None
def request(self, method, params=None, request_id=None):
import zmq
request_id = str(request_id or uuid.uuid4().hex)
envelope = {
"schema_version": 1,
"request_id": request_id,
"run_id": self.run_id,
"client_id": self.client_id,
"method": str(method),
"params": dict(params or {}),
}
socket = self._connect()
socket.send(json.dumps(envelope, ensure_ascii=False, separators=(",", ":")).encode("utf-8"))
poller = zmq.Poller()
poller.register(socket, zmq.POLLIN)
events = dict(poller.poll(int(self.timeout_seconds * 1000)))
if socket not in events:
self._reset_socket()
raise TimeoutError("backtest ZMQ request timed out: %s" % method)
response = json.loads(socket.recv().decode("utf-8"))
if str(response.get("request_id") or "") != request_id:
raise BacktestRemoteError("response request_id mismatch")
if not response.get("ok"):
raise BacktestRemoteError(str(response.get("error") or "remote request failed"))
return response.get("data")
def ping(self):
return self.request("ping")
def describe(self):
data = self.request("describe")
if not self.run_id and data.get("run_id"):
self.run_id = str(data["run_id"])
return data
def start(self):
return self.request("start")
def next_bar(self):
return self.request("next_bar")
def state(self):
return self.request("state")
def submit_order(
self,
symbol,
side,
quantity,
order_type="MARKET",
limit_price=None,
client_order_id="",
time_in_force="NEXT_BAR",
):
params = {
"symbol": symbol,
"side": side,
"quantity": int(quantity),
"order_type": order_type,
"client_order_id": client_order_id,
"time_in_force": time_in_force,
}
if limit_price is not None:
params["limit_price"] = limit_price
return self.request("submit_order", params)
def cancel_order(self, order_id):
return self.request("cancel_order", {"order_id": order_id})
def history(self, symbol, count=100, fields=None):
params = {"symbol": symbol, "count": int(count)}
if fields is not None:
params["fields"] = list(fields)
return self.request("history", params)
def orders(self):
return self.request("orders")
def fills(self):
return self.request("fills")
def finish(self):
return self.request("finish")
def close(self):
self._reset_socket()
def __enter__(self):
return self
def __exit__(self, exc_type, exc_value, traceback):
self.close()
@@ -0,0 +1,278 @@
"""Deterministic historical bar feeds for the backtest bridge."""
import csv
import datetime as dt
import hashlib
import io
import json
import os
import threading
from .models import normalize_symbol
DATETIME_FIELDS = ("datetime", "timestamp", "time", "date", "stime")
SYMBOL_FIELDS = ("symbol", "stock_code", "code", "stock")
REQUIRED_PRICE_FIELDS = ("open", "high", "low", "close")
def _first(row, names, default=None):
for name in names:
value = row.get(name)
if value not in (None, ""):
return value
return default
def parse_datetime(value):
if isinstance(value, dt.datetime):
return value.replace(tzinfo=None)
if isinstance(value, dt.date):
return dt.datetime.combine(value, dt.time())
text = str(value or "").strip()
if not text:
raise ValueError("bar datetime is required")
if text.isdigit():
if len(text) == 8:
return dt.datetime.strptime(text, "%Y%m%d")
if len(text) == 14:
return dt.datetime.strptime(text, "%Y%m%d%H%M%S")
numeric = int(text)
if numeric > 10 ** 12:
numeric = numeric / 1000.0
return dt.datetime.fromtimestamp(numeric)
normalized = text.replace("T", " ").replace("Z", "").strip()
from_isoformat = getattr(dt.datetime, "fromisoformat", None)
if from_isoformat is not None:
try:
return from_isoformat(normalized).replace(tzinfo=None)
except ValueError:
pass
for fmt in ("%Y-%m-%d %H:%M:%S", "%Y-%m-%d %H:%M", "%Y/%m/%d %H:%M:%S", "%Y-%m-%d"):
try:
return dt.datetime.strptime(normalized, fmt)
except ValueError:
continue
raise ValueError("unsupported bar datetime: %s" % text)
def _bool_value(value):
if isinstance(value, bool):
return value
return str(value or "").strip().lower() in ("1", "true", "yes", "y", "on")
def _optional_float(value):
return None if value in (None, "") else float(value)
def normalize_bar(row, default_symbol=""):
timestamp = parse_datetime(_first(row, DATETIME_FIELDS))
symbol = normalize_symbol(_first(row, SYMBOL_FIELDS, default_symbol))
bar = {
"datetime": timestamp.strftime("%Y-%m-%d %H:%M:%S"),
"symbol": symbol,
}
for field in REQUIRED_PRICE_FIELDS:
value = row.get(field)
if value in (None, ""):
raise ValueError("%s is required for %s at %s" % (field, symbol, bar["datetime"]))
bar[field] = float(value)
if bar[field] <= 0:
raise ValueError("%s must be positive for %s at %s" % (field, symbol, bar["datetime"]))
if bar["high"] < max(bar["open"], bar["close"], bar["low"]):
raise ValueError("bar high is inconsistent for %s at %s" % (symbol, bar["datetime"]))
if bar["low"] > min(bar["open"], bar["close"], bar["high"]):
raise ValueError("bar low is inconsistent for %s at %s" % (symbol, bar["datetime"]))
bar["volume"] = float(row.get("volume") or 0)
bar["amount"] = float(row.get("amount") or 0)
bar["prev_close"] = _optional_float(row.get("prev_close"))
bar["up_limit"] = _optional_float(row.get("up_limit"))
bar["down_limit"] = _optional_float(row.get("down_limit"))
bar["suspended"] = _bool_value(row.get("suspended"))
if row.get("price_limit_rate") not in (None, ""):
bar["price_limit_rate"] = float(row["price_limit_rate"])
return timestamp, bar
class InMemoryBarFeed(object):
def __init__(self, rows, source="memory", data_hash=None, default_symbol=""):
normalized = []
for row in rows:
timestamp, bar = normalize_bar(dict(row), default_symbol=default_symbol)
normalized.append((timestamp, bar))
normalized.sort(key=lambda item: (item[0], item[1]["symbol"]))
seen = set()
frames = []
current_timestamp = None
current_bars = None
previous_close = {}
for timestamp, bar in normalized:
identity = (timestamp, bar["symbol"])
if identity in seen:
raise ValueError("duplicate bar for %s at %s" % (bar["symbol"], bar["datetime"]))
seen.add(identity)
if bar["prev_close"] is None:
bar["prev_close"] = previous_close.get(bar["symbol"])
previous_close[bar["symbol"]] = bar["close"]
if current_timestamp != timestamp:
current_timestamp = timestamp
current_bars = {}
frames.append(
{
"datetime": timestamp.strftime("%Y-%m-%d %H:%M:%S"),
"bars": current_bars,
}
)
current_bars[bar["symbol"]] = bar
if not frames:
raise ValueError("historical data is empty")
self._frames = frames
self.source = str(source)
if data_hash:
self.data_hash = str(data_hash)
else:
payload = json.dumps(frames, ensure_ascii=False, sort_keys=True, separators=(",", ":"))
self.data_hash = hashlib.sha256(payload.encode("utf-8")).hexdigest()
def __len__(self):
return len(self._frames)
def frame(self, index):
frame = self._frames[int(index)]
return {"datetime": frame["datetime"], "bars": {key: dict(value) for key, value in frame["bars"].items()}}
def history(self, symbol, end_index, count=100, fields=None):
symbol = normalize_symbol(symbol)
end_index = min(int(end_index), len(self._frames) - 1)
count = max(int(count or 0), 0)
result = []
for index in range(0, end_index + 1):
bar = self._frames[index]["bars"].get(symbol)
if bar is None:
continue
if fields:
item = {"datetime": bar["datetime"], "symbol": symbol}
for field in fields:
if field in bar:
item[str(field)] = bar[field]
else:
item = dict(bar)
result.append(item)
return result[-count:] if count else []
class CsvBarFeed(InMemoryBarFeed):
def __init__(self, path, default_symbol="", encoding="utf-8-sig"):
absolute = os.path.abspath(path)
with open(absolute, "rb") as handle:
raw = handle.read()
digest = hashlib.sha256(raw).hexdigest()
text = raw.decode(encoding)
reader = csv.DictReader(io.StringIO(text, newline=""))
rows = list(reader)
super(CsvBarFeed, self).__init__(
rows,
source=absolute,
data_hash=digest,
default_symbol=default_symbol,
)
class StreamingBarFeed(object):
"""Thread-safe feed populated by QMT ``handlebar`` callbacks.
The external strategy can only read through ``frame``/``history`` with an
engine-controlled end index, so bars already captured from QMT but not yet
advanced to remain inaccessible.
"""
def __init__(self, source="qmt_native_backtest"):
self.source = str(source)
self._frames = []
self._seen = set()
self._previous_close = {}
self._condition = threading.Condition()
self.closed = False
@property
def data_hash(self):
with self._condition:
payload = json.dumps(
self._frames,
ensure_ascii=False,
sort_keys=True,
separators=(",", ":"),
)
return hashlib.sha256(payload.encode("utf-8")).hexdigest()
def __len__(self):
with self._condition:
return len(self._frames)
def append(self, row, default_symbol=""):
timestamp, bar = normalize_bar(dict(row), default_symbol=default_symbol)
identity = (timestamp, bar["symbol"])
with self._condition:
if identity in self._seen:
return False
if self.closed:
raise RuntimeError("streaming feed is closed")
self._seen.add(identity)
if bar["prev_close"] is None:
bar["prev_close"] = self._previous_close.get(bar["symbol"])
self._previous_close[bar["symbol"]] = bar["close"]
timestamp_text = timestamp.strftime("%Y-%m-%d %H:%M:%S")
if self._frames and self._frames[-1]["datetime"] == timestamp_text:
self._frames[-1]["bars"][bar["symbol"]] = bar
elif self._frames and self._frames[-1]["datetime"] > timestamp_text:
raise ValueError("streaming bars must be appended chronologically")
else:
self._frames.append({"datetime": timestamp_text, "bars": {bar["symbol"]: bar}})
self._condition.notify_all()
return True
def close(self):
with self._condition:
self.closed = True
self._condition.notify_all()
def wait_for_index(self, index, timeout_seconds=None):
index = int(index)
with self._condition:
if len(self._frames) > index:
return True
self._condition.wait_for(
lambda: len(self._frames) > index or self.closed,
timeout=timeout_seconds,
)
return len(self._frames) > index
def frame(self, index):
with self._condition:
frame = self._frames[int(index)]
return {
"datetime": frame["datetime"],
"bars": {key: dict(value) for key, value in frame["bars"].items()},
}
def history(self, symbol, end_index, count=100, fields=None):
symbol = normalize_symbol(symbol)
count = max(int(count or 0), 0)
with self._condition:
end_index = min(int(end_index), len(self._frames) - 1)
frames = self._frames[: end_index + 1]
result = []
for frame in frames:
bar = frame["bars"].get(symbol)
if bar is None:
continue
if fields:
item = {"datetime": bar["datetime"], "symbol": symbol}
for field in fields:
if field in bar:
item[str(field)] = bar[field]
else:
item = dict(bar)
result.append(item)
return result[-count:] if count else []
@@ -0,0 +1,372 @@
"""Deterministic bar-by-bar backtest engine."""
import csv
import datetime as dt
import hashlib
import json
import math
import os
from .broker import ACTIVE_ORDER_STATUSES, SimulatedBroker
from .models import decimal_value, normalize_symbol
ENGINE_VERSION = "1.0.0"
PROTOCOL_VERSION = 1
class BacktestConfig(object):
def __init__(
self,
run_id,
output_dir,
initial_cash=1000000,
initial_positions=None,
buy_commission_rate=0.0003,
sell_commission_rate=0.0003,
min_commission=5,
stamp_tax_rate=0.0005,
transfer_fee_rate=0.00001,
slippage_bps=0,
max_volume_participation=0.1,
price_limit_rate=0.10,
lot_size=100,
time_in_force="NEXT_BAR",
seed=0,
strategy_name="external_zmq_strategy",
parameters=None,
fee_schedule="a_share_2023_08_28",
market_rules_version="a_share_v1",
):
self.run_id = str(run_id or "").strip()
if not self.run_id:
raise ValueError("run_id is required")
self.output_dir = os.path.abspath(output_dir)
self.initial_cash = decimal_value(initial_cash)
if self.initial_cash < 0:
raise ValueError("initial_cash cannot be negative")
self.initial_positions = dict(initial_positions or {})
self.buy_commission_rate = decimal_value(buy_commission_rate)
self.sell_commission_rate = decimal_value(sell_commission_rate)
self.min_commission = decimal_value(min_commission)
self.stamp_tax_rate = decimal_value(stamp_tax_rate)
self.transfer_fee_rate = decimal_value(transfer_fee_rate)
self.slippage_bps = decimal_value(slippage_bps)
self.max_volume_participation = float(max_volume_participation)
if not 0 < self.max_volume_participation <= 1:
raise ValueError("max_volume_participation must be in (0, 1]")
self.price_limit_rate = decimal_value(price_limit_rate)
self.lot_size = int(lot_size)
if self.lot_size <= 0:
raise ValueError("lot_size must be positive")
self.time_in_force = str(time_in_force or "NEXT_BAR").upper()
if self.time_in_force not in ("NEXT_BAR", "DAY"):
raise ValueError("time_in_force must be NEXT_BAR or DAY")
self.seed = int(seed)
self.strategy_name = str(strategy_name or "external_zmq_strategy")
self.parameters = dict(parameters or {})
self.fee_schedule = str(fee_schedule or "custom")
self.market_rules_version = str(market_rules_version or "custom")
def to_dict(self, include_paths=True, include_identity=True):
payload = {
"initial_cash": float(self.initial_cash),
"initial_positions": self.initial_positions,
"buy_commission_rate": float(self.buy_commission_rate),
"sell_commission_rate": float(self.sell_commission_rate),
"min_commission": float(self.min_commission),
"stamp_tax_rate": float(self.stamp_tax_rate),
"transfer_fee_rate": float(self.transfer_fee_rate),
"slippage_bps": float(self.slippage_bps),
"max_volume_participation": self.max_volume_participation,
"price_limit_rate": float(self.price_limit_rate),
"lot_size": self.lot_size,
"time_in_force": self.time_in_force,
"seed": self.seed,
"strategy_name": self.strategy_name,
"parameters": self.parameters,
"fee_schedule": self.fee_schedule,
"market_rules_version": self.market_rules_version,
}
if include_identity:
payload["run_id"] = self.run_id
if include_paths:
payload["output_dir"] = self.output_dir
return payload
class BacktestEngine(object):
def __init__(self, feed, config):
self.feed = feed
self.config = config
self.created_at = dt.datetime.now().strftime("%Y-%m-%d %H:%M:%S")
self.started = False
self.finished = False
self.current_index = -1
self.current_frame = None
self.last_fills = []
self.equity_curve = []
self.position_rows = []
self._result = None
self.broker = SimulatedBroker(config)
def _record_state(self):
snapshot = self.broker.snapshot(self.current_frame["bars"])
equity_row = {
"frame_index": self.current_index,
"datetime": self.current_frame["datetime"],
"cash": snapshot["cash"],
"market_value": snapshot["market_value"],
"total_asset": snapshot["total_asset"],
}
self.equity_curve.append(equity_row)
for symbol, position in snapshot["positions"].items():
row = {"frame_index": self.current_index, "datetime": self.current_frame["datetime"]}
row.update(position)
self.position_rows.append(row)
def start(self):
if self.started:
return self.state()
self.started = True
self.current_index = 0
self.current_frame = self.feed.frame(0)
self.broker._settle_trading_day(self.current_frame["datetime"][:10])
self._record_state()
return self.state()
def _require_started(self):
if not self.started:
raise RuntimeError("backtest has not started")
if self.finished:
raise RuntimeError("backtest is already finished")
def submit_order(self, payload):
self._require_started()
return self.broker.submit(
payload,
frame_index=self.current_index,
submitted_at=self.current_frame["datetime"],
).to_dict()
def cancel_order(self, order_id):
self._require_started()
return self.broker.cancel(order_id).to_dict()
def next_bar(self):
self._require_started()
if self.current_index >= len(self.feed) - 1:
return self.state()
self.current_index += 1
self.current_frame = self.feed.frame(self.current_index)
self.last_fills = self.broker.advance(self.current_index, self.current_frame)
self._record_state()
return self.state()
def history(self, symbol, count=100, fields=None):
self._require_started()
return self.feed.history(normalize_symbol(symbol), self.current_index, count=count, fields=fields)
def orders(self):
return self.broker.orders()
def fills(self):
return self.broker.fills()
def state(self):
if not self.started:
return {
"run_id": self.config.run_id,
"started": False,
"finished": self.finished,
"done": False,
"frame_index": -1,
"frame_count": len(self.feed),
}
portfolio = self.broker.snapshot(self.current_frame["bars"])
return {
"run_id": self.config.run_id,
"started": True,
"finished": self.finished,
"done": self.current_index >= len(self.feed) - 1,
"frame_index": self.current_index,
"frame_count": len(self.feed),
"datetime": self.current_frame["datetime"],
"bars": {key: dict(value) for key, value in self.current_frame["bars"].items()},
"fills": [fill.to_dict() for fill in self.last_fills],
"cash": portfolio["cash"],
"market_value": portfolio["market_value"],
"total_asset": portfolio["total_asset"],
"positions": portfolio["positions"],
"total_fees": portfolio["total_fees"],
"turnover": portfolio["turnover"],
}
def _metrics(self):
assets = [float(row["total_asset"]) for row in self.equity_curve]
initial = assets[0] if assets else float(self.config.initial_cash)
final = assets[-1] if assets else initial
peak = None
max_drawdown = 0.0
for value in assets:
peak = value if peak is None else max(peak, value)
if peak > 0:
max_drawdown = min(max_drawdown, value / peak - 1.0)
dates = sorted(set(row["datetime"][:10] for row in self.equity_curve))
total_return = 0.0 if initial == 0 else final / initial - 1.0
annualized = None
if len(dates) > 1 and initial > 0 and final > 0:
annualized = math.pow(final / initial, 252.0 / len(dates)) - 1.0
filled_orders = len([order for order in self.broker.orders_list if order.filled_quantity > 0])
rejected_orders = len([order for order in self.broker.orders_list if order.status == "REJECTED"])
return {
"initial_total_asset": round(initial, 2),
"final_total_asset": round(final, 2),
"total_return": round(total_return, 10),
"annualized_return": None if annualized is None else round(annualized, 10),
"max_drawdown": round(-max_drawdown, 10),
"trading_days": len(dates),
"bar_count": len(self.equity_curve),
"order_count": len(self.broker.orders_list),
"filled_order_count": filled_orders,
"rejected_order_count": rejected_orders,
"fill_count": len(self.broker.fills_list),
"total_fees": round(float(self.broker.total_fees), 2),
"turnover": round(float(self.broker.turnover), 2),
}
def _signature_payload(self, metrics):
orders = []
for item in self.orders():
clean = dict(item)
clean.pop("client_order_id", None)
orders.append(clean)
return {
"engine_version": ENGINE_VERSION,
"data_hash": self.feed.data_hash,
"config": self.config.to_dict(include_paths=False, include_identity=False),
"orders": orders,
"fills": self.fills(),
"equity": self.equity_curve,
"metrics": metrics,
}
def finish(self):
if self._result is not None:
return dict(self._result)
if not self.started:
self.start()
self.broker.expire_open_orders()
metrics = self._metrics()
signature_json = json.dumps(
self._signature_payload(metrics), ensure_ascii=False, sort_keys=True, separators=(",", ":")
)
signature = hashlib.sha256(signature_json.encode("utf-8")).hexdigest()
self.finished = True
final_state = self.state()
self._result = {
"schema_version": 1,
"engine_version": ENGINE_VERSION,
"run_id": self.config.run_id,
"strategy_name": self.config.strategy_name,
"data_hash": self.feed.data_hash,
"deterministic_signature": signature,
"metrics": metrics,
"final_state": final_state,
}
self._write_artifacts()
return dict(self._result)
@staticmethod
def _write_json(path, payload):
with open(path, "w", encoding="utf-8", newline="\n") as handle:
json.dump(payload, handle, ensure_ascii=False, sort_keys=True, indent=2)
handle.write("\n")
@staticmethod
def _write_csv(path, rows, fieldnames):
with open(path, "w", encoding="utf-8-sig", newline="") as handle:
writer = csv.DictWriter(handle, fieldnames=fieldnames, extrasaction="ignore")
writer.writeheader()
writer.writerows(rows)
def _write_artifacts(self):
output_dir = self.config.output_dir
os.makedirs(output_dir, exist_ok=True)
meta = {
"schema_version": 1,
"engine_version": ENGINE_VERSION,
"protocol_version": PROTOCOL_VERSION,
"run_id": self.config.run_id,
"created_at": self.created_at,
"data_source": self.feed.source,
"data_hash": self.feed.data_hash,
"frame_count": len(self.feed),
"config": self.config.to_dict(),
"live_ready": False,
"execution_channel": "backtest_zmq_only",
}
self._write_json(os.path.join(output_dir, "meta.json"), meta)
self._write_json(os.path.join(output_dir, "result.json"), self._result)
self._write_csv(
os.path.join(output_dir, "orders.csv"),
self.orders(),
(
"order_id", "client_order_id", "symbol", "side", "quantity", "filled_quantity",
"remaining_quantity", "order_type", "limit_price", "submitted_index", "submitted_at",
"time_in_force", "status", "reject_reason",
),
)
self._write_csv(
os.path.join(output_dir, "fills.csv"),
self.fills(),
(
"fill_id", "order_id", "client_order_id", "symbol", "side", "quantity", "price",
"amount", "commission", "stamp_tax", "transfer_fee", "total_fee", "filled_index", "filled_at",
),
)
self._write_csv(
os.path.join(output_dir, "equity.csv"),
self.equity_curve,
("frame_index", "datetime", "cash", "market_value", "total_asset"),
)
self._write_csv(
os.path.join(output_dir, "positions.csv"),
self.position_rows,
(
"frame_index", "datetime", "symbol", "quantity", "available", "today_buy", "avg_cost",
"realized_pnl", "mark_price", "market_value",
),
)
class StreamingBacktestEngine(BacktestEngine):
"""Backtest engine whose bars arrive from a QMT backtest callback thread."""
def __init__(self, feed, config, bar_wait_timeout_seconds=60.0):
super(StreamingBacktestEngine, self).__init__(feed, config)
self.bar_wait_timeout_seconds = float(bar_wait_timeout_seconds)
def start(self):
if not self.started and not self.feed.wait_for_index(0, self.bar_wait_timeout_seconds):
raise TimeoutError("timed out waiting for the first QMT backtest bar")
return super(StreamingBacktestEngine, self).start()
def next_bar(self):
self._require_started()
target = self.current_index + 1
if not self.feed.wait_for_index(target, self.bar_wait_timeout_seconds):
if self.feed.closed:
return self.state()
raise TimeoutError("timed out waiting for QMT backtest bar index %d" % target)
self.current_index = target
self.current_frame = self.feed.frame(target)
self.last_fills = self.broker.advance(self.current_index, self.current_frame)
self._record_state()
return self.state()
def state(self):
state = super(StreamingBacktestEngine, self).state()
if state.get("started"):
state["done"] = bool(self.feed.closed and self.current_index >= len(self.feed) - 1)
return state
@@ -0,0 +1,179 @@
"""Backtest-only domain models with JSON-safe serialization."""
from decimal import Decimal, ROUND_HALF_UP
ZERO = Decimal("0")
MONEY_QUANT = Decimal("0.01")
def decimal_value(value, default="0"):
if value in (None, ""):
value = default
if isinstance(value, Decimal):
return value
return Decimal(str(value))
def money(value):
return decimal_value(value).quantize(MONEY_QUANT, rounding=ROUND_HALF_UP)
def json_number(value, digits=None):
if value is None:
return None
number = float(value)
return round(number, digits) if digits is not None else number
def normalize_symbol(value):
text = str(value or "").strip().upper()
if not text:
raise ValueError("symbol is required")
if "." in text:
pure, market = text.rsplit(".", 1)
if pure and market in ("SH", "SZ", "BJ"):
return "%s.%s" % (pure, market)
return text
if text.isdigit() and len(text) == 6:
if text.startswith(("4", "8")):
return text + ".BJ"
if text.startswith(("5", "6", "9")):
return text + ".SH"
return text + ".SZ"
return text
def price_precision(symbol):
pure = normalize_symbol(symbol).split(".", 1)[0]
return 3 if pure.startswith(("15", "16", "50", "51", "52", "56", "58")) else 2
def price_quant(symbol):
return Decimal("0.001") if price_precision(symbol) == 3 else Decimal("0.01")
def round_price(symbol, value):
return decimal_value(value).quantize(price_quant(symbol), rounding=ROUND_HALF_UP)
class Position(object):
def __init__(self, symbol, quantity=0, available=None, today_buy=0, avg_cost=0, realized_pnl=0):
self.symbol = normalize_symbol(symbol)
self.quantity = int(quantity or 0)
self.available = self.quantity if available is None else int(available or 0)
self.today_buy = int(today_buy or 0)
self.avg_cost = decimal_value(avg_cost)
self.realized_pnl = decimal_value(realized_pnl)
def to_dict(self, mark_price=None):
market_value = None if mark_price is None else money(decimal_value(mark_price) * self.quantity)
return {
"symbol": self.symbol,
"quantity": self.quantity,
"available": self.available,
"today_buy": self.today_buy,
"avg_cost": json_number(self.avg_cost, 6),
"realized_pnl": json_number(self.realized_pnl, 2),
"mark_price": json_number(mark_price, 6),
"market_value": json_number(market_value, 2),
}
class BacktestOrder(object):
def __init__(
self,
order_id,
client_order_id,
symbol,
side,
quantity,
order_type,
limit_price,
submitted_index,
submitted_at,
time_in_force="NEXT_BAR",
):
self.order_id = str(order_id)
self.client_order_id = str(client_order_id or "")
self.symbol = normalize_symbol(symbol)
self.side = str(side).upper()
self.quantity = int(quantity)
self.filled_quantity = 0
self.order_type = str(order_type).upper()
self.limit_price = decimal_value(limit_price) if limit_price not in (None, "") else None
self.submitted_index = int(submitted_index)
self.submitted_at = str(submitted_at)
self.time_in_force = str(time_in_force or "NEXT_BAR").upper()
self.status = "PENDING"
self.reject_reason = ""
self.last_attempt_index = None
@property
def remaining(self):
return max(self.quantity - self.filled_quantity, 0)
def to_dict(self):
return {
"order_id": self.order_id,
"client_order_id": self.client_order_id,
"symbol": self.symbol,
"side": self.side,
"quantity": self.quantity,
"filled_quantity": self.filled_quantity,
"remaining_quantity": self.remaining,
"order_type": self.order_type,
"limit_price": json_number(self.limit_price, 6),
"submitted_index": self.submitted_index,
"submitted_at": self.submitted_at,
"time_in_force": self.time_in_force,
"status": self.status,
"reject_reason": self.reject_reason,
}
class BacktestFill(object):
def __init__(
self,
fill_id,
order,
quantity,
price,
commission,
stamp_tax,
transfer_fee,
filled_index,
filled_at,
):
self.fill_id = str(fill_id)
self.order_id = order.order_id
self.client_order_id = order.client_order_id
self.symbol = order.symbol
self.side = order.side
self.quantity = int(quantity)
self.price = decimal_value(price)
self.amount = money(self.price * self.quantity)
self.commission = money(commission)
self.stamp_tax = money(stamp_tax)
self.transfer_fee = money(transfer_fee)
self.total_fee = money(self.commission + self.stamp_tax + self.transfer_fee)
self.filled_index = int(filled_index)
self.filled_at = str(filled_at)
def to_dict(self):
return {
"fill_id": self.fill_id,
"order_id": self.order_id,
"client_order_id": self.client_order_id,
"symbol": self.symbol,
"side": self.side,
"quantity": self.quantity,
"price": json_number(self.price, 6),
"amount": json_number(self.amount, 2),
"commission": json_number(self.commission, 2),
"stamp_tax": json_number(self.stamp_tax, 2),
"transfer_fee": json_number(self.transfer_fee, 2),
"total_fee": json_number(self.total_fee, 2),
"filled_index": self.filled_index,
"filled_at": self.filled_at,
}
@@ -0,0 +1,137 @@
"""Versioned request/response protocol for the backtest-only ZMQ bridge."""
import datetime as dt
import json
SCHEMA_VERSION = 1
class BacktestBridgeProtocol(object):
def __init__(self, engine, request_cache_size=10000):
self.engine = engine
self.request_cache_size = int(request_cache_size)
self.client_id = None
self._responses = {}
self._request_fingerprints = {}
self._response_order = []
def _response(self, request, ok, data=None, error=""):
execution_backend = str(getattr(self.engine, "execution_backend", "LOCAL_SIM"))
return {
"schema_version": SCHEMA_VERSION,
"request_id": str(request.get("request_id") or ""),
"run_id": self.engine.config.run_id,
"client_id": str(request.get("client_id") or ""),
"method": str(request.get("method") or ""),
"ok": bool(ok),
"data": data,
"error": str(error or ""),
"execution_mode": "QMT_BACKTEST" if execution_backend == "QMT_NATIVE" else "BACKTEST",
"execution_backend": execution_backend,
"live_ready": False,
"handled_at": dt.datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
}
@staticmethod
def _fingerprint(request):
return json.dumps(request, ensure_ascii=False, sort_keys=True, separators=(",", ":"))
def _remember(self, request_id, request, response):
self._responses[request_id] = response
self._request_fingerprints[request_id] = self._fingerprint(request)
self._response_order.append(request_id)
while len(self._response_order) > self.request_cache_size:
oldest = self._response_order.pop(0)
self._responses.pop(oldest, None)
self._request_fingerprints.pop(oldest, None)
def _validate(self, request):
if not isinstance(request, dict):
raise ValueError("request must be a JSON object")
if int(request.get("schema_version") or 0) != SCHEMA_VERSION:
raise ValueError("unsupported schema_version")
if not str(request.get("request_id") or ""):
raise ValueError("request_id is required")
method = str(request.get("method") or "").lower()
requested_run_id = str(request.get("run_id") or "")
discovery = method in ("ping", "describe") and not requested_run_id
if not discovery and requested_run_id != self.engine.config.run_id:
raise ValueError("run_id mismatch")
if not str(request.get("client_id") or ""):
raise ValueError("client_id is required")
if not str(request.get("method") or ""):
raise ValueError("method is required")
def _claim_or_check_client(self, request):
client_id = str(request["client_id"])
method = str(request["method"]).lower()
if self.client_id is None and method == "start":
self.client_id = client_id
if method not in ("ping", "describe") and self.client_id != client_id:
raise PermissionError("run is owned by another client_id")
def _dispatch(self, request):
method = str(request["method"]).lower()
params = dict(request.get("params") or {})
if method == "ping":
return {
"status": "ok",
"started": self.engine.started,
"finished": self.engine.finished,
}
if method == "describe":
execution_backend = str(getattr(self.engine, "execution_backend", "LOCAL_SIM"))
return {
"schema_version": SCHEMA_VERSION,
"run_id": self.engine.config.run_id,
"engine_version": str(getattr(self.engine, "engine_version", "1.0.0")),
"execution_backend": execution_backend,
"engine_owner": "QMT" if execution_backend == "QMT_NATIVE" else "LOCAL",
"matching_owner": "QMT" if execution_backend == "QMT_NATIVE" else "LOCAL",
"methods": [
"ping", "describe", "start", "next_bar", "submit_order", "cancel_order",
"state", "history", "orders", "fills", "finish",
],
"fill_timing": str(getattr(self.engine, "fill_timing", "next_symbol_bar")),
"live_ready": False,
}
if method == "start":
return self.engine.start()
if method == "next_bar":
return self.engine.next_bar()
if method == "submit_order":
return self.engine.submit_order(params)
if method == "cancel_order":
return self.engine.cancel_order(params.get("order_id"))
if method == "state":
return self.engine.state()
if method == "history":
return self.engine.history(
params.get("symbol"),
count=params.get("count", 100),
fields=params.get("fields"),
)
if method == "orders":
return self.engine.orders()
if method == "fills":
return self.engine.fills()
if method == "finish":
return self.engine.finish()
raise ValueError("unsupported method: %s" % method)
def handle(self, request):
request_id = str((request or {}).get("request_id") or "")
if request_id and request_id in self._responses:
if self._request_fingerprints.get(request_id) != self._fingerprint(request):
return self._response(request, False, None, "request_id reused with different payload")
return self._responses[request_id]
try:
self._validate(request)
self._claim_or_check_client(request)
response = self._response(request, True, self._dispatch(request))
except Exception as exc:
response = self._response(request or {}, False, None, "%s: %s" % (exc.__class__.__name__, exc))
if request_id:
self._remember(request_id, request, response)
return response
@@ -0,0 +1,718 @@
"""QMT-native backtest service exposed to external strategies over ZMQ.
QMT remains the only backtest engine and matching system. The ZMQ listener
thread only queues commands; every QMT API call is executed by ``handlebar`` on
QMT's callback thread.
"""
import datetime as dt
import threading
import uuid
from .data_feed import StreamingBarFeed, parse_datetime
from .models import normalize_symbol
from .protocol import BacktestBridgeProtocol
from .zmq_server import ZmqBacktestServer
_CONFIG = {}
_QMT_API = {}
_RUNTIME = None
def configure(**kwargs):
_CONFIG.update(kwargs)
def bind_qmt_api(passorder_func=None, cancel_func=None, get_trade_detail_data_func=None):
if passorder_func is not None:
_QMT_API["passorder"] = passorder_func
if cancel_func is not None:
_QMT_API["cancel"] = cancel_func
if get_trade_detail_data_func is not None:
_QMT_API["get_trade_detail_data"] = get_trade_detail_data_func
def _sequence(value):
if value is None:
return []
if isinstance(value, dict):
for item in value.values():
result = _sequence(item)
if result:
return result
return []
if isinstance(value, (list, tuple)):
return list(value)
if hasattr(value, "tolist"):
try:
result = value.tolist()
return result if isinstance(result, list) else [result]
except Exception:
pass
if hasattr(value, "values"):
try:
return list(value.values)
except Exception:
pass
return [value]
def _last_value(value):
values = _sequence(value)
return values[-1] if values else None
def _attr(value, names, default=None):
for name in names:
if isinstance(value, dict) and name in value:
result = value.get(name)
else:
result = getattr(value, name, None)
if result is not None:
return result
return default
def _json_number(value):
if value in (None, ""):
return None
try:
return float(value)
except (TypeError, ValueError):
return None
def _full_symbol(row):
code = str(_attr(row, ("m_strInstrumentID", "instrument_id", "stock_code", "symbol"), "") or "")
market = str(_attr(row, ("m_strExchangeID", "exchange_id", "market"), "") or "").upper()
if "." not in code and market in ("SH", "SZ", "BJ"):
code = code + "." + market
return normalize_symbol(code) if code else ""
def _side_from_offset(value):
try:
return "BUY" if int(value or 0) == 48 else "SELL"
except (TypeError, ValueError):
return str(value or "")
def _is_qmt_backtest(context):
value = getattr(context, "do_back_test", None)
if callable(value):
try:
value = value()
except Exception:
value = None
if bool(value):
return True
for name in ("is_backtest", "is_back_test", "backtest"):
value = getattr(context, name, None)
if callable(value):
try:
value = value()
except Exception:
value = None
if bool(value):
return True
return False
class QmtBarExtractor(object):
def __init__(self):
self.previous_close = {}
@staticmethod
def _symbol(context):
raw = ""
for name in ("stock", "symbol", "stockcode"):
value = getattr(context, name, None)
if value:
raw = str(value)
break
if not raw:
raise ValueError("QMT ContextInfo has no stock symbol")
if "." not in raw:
market = str(getattr(context, "market", "") or "").upper()
if market in ("SH", "SZ", "BJ"):
raw = raw + "." + market
return normalize_symbol(raw)
@staticmethod
def _timestamp(context):
barpos = getattr(context, "barpos", getattr(context, "bar_index", None))
getter = getattr(context, "get_bar_timetag", None)
if callable(getter) and barpos is not None:
return parse_datetime(getter(barpos)).strftime("%Y-%m-%d %H:%M:%S")
for name in ("bar_time", "datetime", "timestamp"):
value = getattr(context, name, None)
if value not in (None, ""):
return parse_datetime(value).strftime("%Y-%m-%d %H:%M:%S")
raise ValueError("QMT ContextInfo has no deterministic bar timestamp")
@staticmethod
def _periods(context):
result = []
for value in (getattr(context, "period", None), "1m", "1d"):
text = str(value or "").strip()
if text and text not in result:
result.append(text)
return result
def _history_value(self, context, field):
getter = getattr(context, "get_history_data", None)
if not callable(getter):
return None
for period in self._periods(context):
for call in (
lambda period=period: getter(1, period, field),
lambda period=period: getter(field, 1, period),
lambda: getter(field, 1),
):
try:
value = _last_value(call())
if value not in (None, ""):
return value
except Exception:
continue
return None
def _field(self, context, field, aliases=()):
for name in (field,) + tuple(aliases):
value = _last_value(getattr(context, name, None))
if value not in (None, ""):
return value
for name in (field,) + tuple(aliases):
value = self._history_value(context, name)
if value not in (None, ""):
return value
return None
def extract(self, context):
symbol = self._symbol(context)
close = self._field(context, "close")
row = {
"datetime": self._timestamp(context),
"symbol": symbol,
"open": self._field(context, "open"),
"high": self._field(context, "high"),
"low": self._field(context, "low"),
"close": close,
"volume": self._field(context, "volume", ("vol",)) or 0,
"amount": self._field(context, "amount") or 0,
"prev_close": self._field(context, "prev_close", ("preClose", "lastClose")),
}
if row["prev_close"] in (None, ""):
row["prev_close"] = self.previous_close.get(symbol)
self.previous_close[symbol] = close
return row
class NativeSessionConfig(object):
def __init__(self, run_id, strategy_name, account_id):
self.run_id = str(run_id)
self.strategy_name = str(strategy_name)
self.account_id = str(account_id)
class QmtNativeBacktestSession(object):
"""Engine-shaped adapter whose actual engine and broker are both QMT."""
engine_version = "qmt-native-1.0.0"
execution_backend = "QMT_NATIVE"
fill_timing = "qmt_native_matching"
def __init__(self, config=None, qmt_api=None):
options = dict(config or {})
run_id = str(options.get("run_id") or ("qmt-native-" + dt.datetime.now().strftime("%Y%m%d-%H%M%S")))
self.config = NativeSessionConfig(
run_id=run_id,
strategy_name=options.get("strategy_name") or "ZMQ_BACKTEST",
account_id=options.get("account_id") or "",
)
self.account_type = str(options.get("account_type") or "STOCK")
self.combo_type = int(options.get("combo_type") or 1101)
self.quick_trade = int(options.get("quick_trade") if options.get("quick_trade") is not None else 2)
self.market_price_type = int(options.get("market_price_type") or 5)
self.limit_price_type = int(options.get("limit_price_type") or 11)
self.bar_wait_timeout = float(options.get("bar_wait_timeout_seconds") or 60.0)
self.require_backtest = bool(options.get("require_qmt_backtest", True))
self.qmt_api = dict(qmt_api or {})
self.feed = StreamingBarFeed(source="qmt_native_backtest")
self.extractor = QmtBarExtractor()
self.created_at = dt.datetime.now().strftime("%Y-%m-%d %H:%M:%S")
self.started = False
self.finished = False
self.qmt_completed = False
self.current_index = -1
self.current_frame = None
self._released_index = -1
self._condition = threading.Condition()
self._pending_commands = []
self._orders = {}
self._fills = {}
self._current_frame_fills = []
self._published_fill_keys = set()
self._positions = {}
self._asset = {"cash": None, "total_asset": None}
self._context = None
self._failure = ""
def bind_context(self, context):
if self.require_backtest and not _is_qmt_backtest(context):
raise RuntimeError("QMT native bridge refused to run outside QMT backtest mode")
if not self.config.account_id:
raise RuntimeError("account_id is required for the QMT native backtest bridge")
self._context = context
if self.config.account_id and hasattr(context, "set_account"):
context.set_account(self.config.account_id)
def _require_api(self, name):
func = self.qmt_api.get(name)
if func is None:
raise RuntimeError("QMT runtime API is unavailable: %s" % name)
return func
def _require_started(self):
if not self.started:
raise RuntimeError("external strategy has not attached")
if self.finished:
raise RuntimeError("external strategy session is already finished")
def start(self):
with self._condition:
if not self.started:
self.started = True
self._condition.notify_all()
if self.current_index < 0 and not self.qmt_completed:
ready = self._condition.wait_for(
lambda: self.current_index >= 0 or self.qmt_completed or bool(self._failure),
timeout=self.bar_wait_timeout,
)
if not ready:
raise TimeoutError("timed out waiting for QMT's first backtest bar")
if self._failure:
raise RuntimeError(self._failure)
return self._state_unlocked()
def _queue_command(self, command):
with self._condition:
self._require_started()
if self.qmt_completed or self.current_index < 0:
raise RuntimeError("QMT backtest has no active bar")
if self._released_index >= self.current_index:
raise RuntimeError("current QMT bar has already been released")
command = dict(command)
command["frame_index"] = self.current_index
self._pending_commands.append(command)
return command
def submit_order(self, payload):
payload = dict(payload or {})
side = str(payload.get("side") or "").upper()
if side not in ("BUY", "SELL"):
raise ValueError("side must be BUY or SELL")
quantity = int(payload.get("quantity") or 0)
if quantity <= 0:
raise ValueError("quantity must be positive")
symbol = normalize_symbol(payload.get("symbol"))
order_type = str(payload.get("order_type") or "MARKET").upper()
if order_type not in ("MARKET", "LIMIT"):
raise ValueError("order_type must be MARKET or LIMIT")
limit_price = payload.get("limit_price")
if order_type == "LIMIT" and limit_price in (None, ""):
raise ValueError("limit_price is required for LIMIT order")
client_order_id = str(payload.get("client_order_id") or ("zmq:" + uuid.uuid4().hex[:20]))
record = {
"order_id": client_order_id,
"client_order_id": client_order_id,
"symbol": symbol,
"side": side,
"quantity": quantity,
"filled_quantity": 0,
"order_type": order_type,
"limit_price": None if limit_price in (None, "") else float(limit_price),
"status": "QUEUED",
"reject_reason": "",
"submitted_index": self.current_index,
"submitted_at": (self.current_frame or {}).get("datetime", ""),
"execution_backend": self.execution_backend,
}
with self._condition:
self._orders[client_order_id] = record
self._queue_command({"kind": "submit", "client_order_id": client_order_id})
return dict(record)
def cancel_order(self, order_id):
order_id = str(order_id or "").strip()
if not order_id:
raise ValueError("order_id is required")
command = self._queue_command({"kind": "cancel", "order_id": order_id})
return {"order_id": order_id, "status": "CANCEL_QUEUED", "frame_index": command["frame_index"]}
def _execute_submit(self, command, context):
client_order_id = command["client_order_id"]
with self._condition:
record = dict(self._orders[client_order_id])
if not self.config.account_id:
raise RuntimeError("account_id is required for QMT native passorder")
passorder = self._require_api("passorder")
side = record["side"]
order_type = record["order_type"]
price_type = self.limit_price_type if order_type == "LIMIT" else self.market_price_type
price = float(record["limit_price"] or 0)
result = passorder(
23 if side == "BUY" else 24,
self.combo_type,
self.config.account_id,
record["symbol"],
price_type,
price,
int(record["quantity"]),
self.config.strategy_name,
self.quick_trade,
client_order_id,
context,
)
with self._condition:
target = self._orders[client_order_id]
target["status"] = "SUBMITTED"
if result not in (None, ""):
target["qmt_order_id"] = str(result)
def _execute_cancel(self, command, context):
cancel = self._require_api("cancel")
order_id = command["order_id"]
with self._condition:
record = self._orders.get(order_id)
qmt_order_id = (
(record or {}).get("qmt_order_id")
or (record or {}).get("order_id")
or order_id
)
result = cancel(qmt_order_id, self.config.account_id, self.account_type, context)
with self._condition:
record = self._orders.get(order_id)
if record is not None:
record["status"] = "CANCEL_SUBMITTED" if result is not False else "CANCEL_REJECTED"
def _execute_commands(self, commands, context):
for command in commands:
try:
if command["kind"] == "submit":
self._execute_submit(command, context)
elif command["kind"] == "cancel":
self._execute_cancel(command, context)
except Exception as exc:
key = command.get("client_order_id") or command.get("order_id")
with self._condition:
record = self._orders.get(key)
if record is not None:
record["status"] = "REJECTED"
record["reject_reason"] = "%s: %s" % (exc.__class__.__name__, exc)
print("[bigqmt_backtest] QMT command failed kind=%s error=%s" % (command.get("kind"), exc))
def on_bar(self, context):
self.bind_context(context)
self._refresh_qmt_state()
row = self.extractor.extract(context)
appended = self.feed.append(row)
if not appended:
return False
with self._condition:
self.current_index = len(self.feed) - 1
self.current_frame = self.feed.frame(self.current_index)
new_fill_keys = [key for key in self._fills if key not in self._published_fill_keys]
self._current_frame_fills = [dict(self._fills[key]) for key in new_fill_keys]
self._published_fill_keys.update(new_fill_keys)
index = self.current_index
self._condition.notify_all()
released = self._condition.wait_for(
lambda: self._released_index >= index or self.finished or bool(self._failure),
timeout=self.bar_wait_timeout,
)
if not released:
self._failure = "external strategy timed out on QMT bar index %d" % index
self._condition.notify_all()
raise TimeoutError(self._failure)
commands = [item for item in self._pending_commands if item.get("frame_index") == index]
self._pending_commands = [item for item in self._pending_commands if item.get("frame_index") != index]
self._execute_commands(commands, context)
self._refresh_qmt_state()
print(
"[bigqmt_backtest] QMT native bar released index=%d datetime=%s symbol=%s commands=%d"
% (index, row["datetime"], row["symbol"], len(commands))
)
return True
def next_bar(self):
with self._condition:
self._require_started()
previous = self.current_index
if self.qmt_completed:
return self._state_unlocked()
self._released_index = max(self._released_index, previous)
self._condition.notify_all()
ready = self._condition.wait_for(
lambda: self.current_index > previous or self.qmt_completed or bool(self._failure),
timeout=self.bar_wait_timeout,
)
if not ready:
raise TimeoutError("timed out waiting for QMT backtest bar after index %d" % previous)
if self._failure:
raise RuntimeError(self._failure)
return self._state_unlocked()
def history(self, symbol, count=100, fields=None):
with self._condition:
self._require_started()
end_index = self.current_index
return self.feed.history(symbol, end_index, count=count, fields=fields)
def orders(self):
with self._condition:
return [dict(value) for value in self._orders.values()]
def fills(self):
with self._condition:
return [dict(value) for value in self._fills.values()]
def _state_unlocked(self):
if self.current_frame is None:
return {
"run_id": self.config.run_id,
"started": self.started,
"finished": self.finished,
"done": self.qmt_completed,
"frame_index": -1,
"frame_count": len(self.feed),
"execution_backend": self.execution_backend,
}
return {
"run_id": self.config.run_id,
"started": self.started,
"finished": self.finished,
"done": self.qmt_completed,
"frame_index": self.current_index,
"frame_count": len(self.feed),
"datetime": self.current_frame["datetime"],
"bars": {key: dict(value) for key, value in self.current_frame["bars"].items()},
"fills": [dict(value) for value in self._current_frame_fills],
"cash": self._asset.get("cash"),
"total_asset": self._asset.get("total_asset"),
"positions": {key: dict(value) for key, value in self._positions.items()},
"execution_backend": self.execution_backend,
"qmt_completed": self.qmt_completed,
"failure": self._failure,
}
def state(self):
with self._condition:
return self._state_unlocked()
def finish(self):
with self._condition:
if self.finished:
return self._result_unlocked()
self._released_index = max(self._released_index, self.current_index)
self.finished = True
self._condition.notify_all()
return self._result_unlocked()
def _result_unlocked(self):
return {
"schema_version": 1,
"engine_version": self.engine_version,
"run_id": self.config.run_id,
"strategy_name": self.config.strategy_name,
"execution_backend": self.execution_backend,
"qmt_completed": self.qmt_completed,
"order_count": len(self._orders),
"fill_count": len(self._fills),
"final_state": self._state_unlocked(),
"result_owner": "QMT",
}
def on_qmt_stop(self):
self.feed.close()
with self._condition:
new_fill_keys = [key for key in self._fills if key not in self._published_fill_keys]
self._current_frame_fills = [dict(self._fills[key]) for key in new_fill_keys]
self._published_fill_keys.update(new_fill_keys)
self.qmt_completed = True
self._condition.notify_all()
print("[bigqmt_backtest] QMT native backtest completed bars=%d" % len(self.feed))
def on_order(self, order):
item = {
"order_id": str(_attr(order, ("m_strOrderSysID", "order_sys_id", "order_id"), "") or ""),
"client_order_id": str(_attr(order, ("m_strRemark", "remark", "user_order_id"), "") or ""),
"symbol": _full_symbol(order),
"side": _side_from_offset(_attr(order, ("m_nOffsetFlag", "offset_flag"), 0)),
"quantity": int(_attr(order, ("m_nVolumeTotalOriginal", "volume", "quantity"), 0) or 0),
"filled_quantity": int(_attr(order, ("m_nVolumeTraded", "traded_volume", "filled_quantity"), 0) or 0),
"price": _json_number(_attr(order, ("m_dLimitPrice", "m_dPrice", "price"))),
"status": str(_attr(order, ("m_nOrderStatus", "status"), "") or ""),
}
key = item["client_order_id"] or item["order_id"] or ("order:" + uuid.uuid4().hex)
with self._condition:
existing = self._orders.get(key, {})
existing.update(item)
self._orders[key] = existing
self._condition.notify_all()
return dict(existing)
def on_trade(self, trade):
item = {
"fill_id": str(_attr(trade, ("m_strTradeID", "trade_id", "fill_id"), "") or ""),
"order_id": str(_attr(trade, ("m_strOrderSysID", "order_sys_id", "order_id"), "") or ""),
"client_order_id": str(_attr(trade, ("m_strRemark", "remark", "user_order_id"), "") or ""),
"symbol": _full_symbol(trade),
"side": _side_from_offset(_attr(trade, ("m_nOffsetFlag", "offset_flag"), 0)),
"quantity": int(_attr(trade, ("m_nVolume", "volume", "quantity"), 0) or 0),
"price": _json_number(_attr(trade, ("m_dPrice", "m_dTradePrice", "price"))),
"filled_at": str(_attr(trade, ("m_strTradeTime", "trade_time", "filled_at"), "") or ""),
}
key = item["fill_id"] or "%s:%s:%s" % (item["order_id"], item["quantity"], item["price"])
with self._condition:
self._fills[key] = item
self._condition.notify_all()
return dict(item)
def _query(self, detail_type):
query = self.qmt_api.get("get_trade_detail_data")
if query is None or not self.config.account_id:
return []
calls = []
if detail_type in ("ORDER", "DEAL", "TRADE"):
calls.append(lambda: query(
self.config.account_id, self.account_type, detail_type, self.config.strategy_name
))
calls.append(lambda: query(self.config.account_id, self.account_type, detail_type))
last_error = None
for call in calls:
try:
return list(call() or [])
except Exception as exc:
last_error = exc
print("[bigqmt_backtest] QMT query failed type=%s error=%s" % (detail_type, last_error))
return []
def _refresh_qmt_state(self):
positions = {}
for row in self._query("POSITION"):
symbol = _full_symbol(row)
if not symbol:
continue
positions[symbol] = {
"symbol": symbol,
"quantity": int(_attr(row, ("m_nVolume", "volume", "quantity"), 0) or 0),
"available": int(_attr(row, ("m_nCanUseVolume", "available", "can_use_volume"), 0) or 0),
"avg_cost": _json_number(_attr(row, ("m_dOpenPrice", "m_dCostPrice", "cost", "avg_cost"))),
}
asset_rows = self._query("ACCOUNT") or self._query("ASSET")
asset = {"cash": None, "total_asset": None}
if asset_rows:
row = asset_rows[0]
asset = {
"cash": _json_number(_attr(row, ("m_dAvailable", "m_dAvailableCash", "available_cash", "cash"))),
"total_asset": _json_number(_attr(row, ("m_dBalance", "m_dAsset", "total_asset", "asset"))),
}
order_rows = self._query("ORDER")
trade_rows = self._query("DEAL") or self._query("TRADE")
with self._condition:
self._positions = positions
self._asset = asset
for row in order_rows:
self.on_order(row)
for row in trade_rows:
self.on_trade(row)
class QmtBacktestBridgeRuntime(object):
def __init__(self, config=None, qmt_api=None):
config = dict(config or {})
bind_endpoint = str(config.pop("bind_endpoint", "tcp://127.0.0.1:16662"))
self.engine = QmtNativeBacktestSession(config=config, qmt_api=qmt_api)
self.protocol = BacktestBridgeProtocol(self.engine)
self.server = ZmqBacktestServer(self.protocol, endpoint=bind_endpoint, exit_on_finish=True)
self.server_thread = None
def start(self, context):
if self.server_thread is not None:
return
self.engine.bind_context(context)
self.server_thread = threading.Thread(
target=self.server.serve_forever,
name="bigqmt-native-backtest-zmq",
daemon=True,
)
self.server_thread.start()
if not self.server.wait_until_ready(5.0) or not self.server.actual_endpoint:
raise RuntimeError("QMT native backtest ZMQ service failed to bind")
print(
"[bigqmt_backtest] QMT native service started run_id=%s endpoint=%s account=%s live_ready=False"
% (self.engine.config.run_id, self.server.actual_endpoint, self.engine.config.account_id)
)
def on_bar(self, context):
return self.engine.on_bar(context)
def on_order(self, order):
return self.engine.on_order(order)
def on_trade(self, trade):
return self.engine.on_trade(trade)
def on_qmt_stop(self):
self.engine.on_qmt_stop()
def stop_server(self):
self.server.stop()
def reset_runtime():
global _RUNTIME
if _RUNTIME is not None:
_RUNTIME.stop_server()
_RUNTIME = None
def get_runtime():
return _RUNTIME
def init(ContextInfo):
global _RUNTIME
reset_runtime()
_RUNTIME = QmtBacktestBridgeRuntime(_CONFIG, _QMT_API)
_RUNTIME.start(ContextInfo)
return _RUNTIME
def handlebar(ContextInfo):
if _RUNTIME is None:
init(ContextInfo)
return _RUNTIME.on_bar(ContextInfo)
def order_callback(ContextInfo, orderInfo):
if _RUNTIME is not None:
return _RUNTIME.on_order(orderInfo)
return None
def deal_callback(ContextInfo, dealInfo):
if _RUNTIME is not None:
return _RUNTIME.on_trade(dealInfo)
return None
def stop(ContextInfo=None):
if _RUNTIME is not None:
_RUNTIME.on_qmt_stop()
def after_backtest(ContextInfo=None):
return stop(ContextInfo)
@@ -0,0 +1,80 @@
"""Command-line entry for the standalone ZMQ backtest bridge."""
import argparse
import json
import os
import sys
import uuid
from .data_feed import CsvBarFeed
from .engine import BacktestConfig, BacktestEngine
from .protocol import BacktestBridgeProtocol
from .zmq_server import ZmqBacktestServer
def _load_config(path):
if not path:
return {}
with open(os.path.abspath(path), encoding="utf-8") as handle:
payload = json.load(handle)
if not isinstance(payload, dict):
raise ValueError("config JSON must be an object")
return payload
def _parser():
parser = argparse.ArgumentParser(description="Standalone ZMQ backtest bridge")
parser.add_argument("--data", required=True, help="UTF-8 CSV historical bar file")
parser.add_argument("--config", default="", help="Optional UTF-8 JSON backtest config")
parser.add_argument("--run-id", default="", help="Unique run identity")
parser.add_argument("--output-dir", default="", help="Artifact directory")
parser.add_argument("--bind", default="tcp://127.0.0.1:16661", help="ZMQ REP bind endpoint")
parser.add_argument("--default-symbol", default="", help="Used when CSV has no symbol column")
parser.add_argument("--initial-cash", type=float, default=None)
parser.add_argument("--slippage-bps", type=float, default=None)
parser.add_argument("--max-volume-participation", type=float, default=None)
parser.add_argument("--keep-running", action="store_true", help="Do not stop server after finish")
return parser
def build_engine(args):
payload = _load_config(args.config)
run_id = str(args.run_id or payload.pop("run_id", "") or ("bt-" + uuid.uuid4().hex[:12]))
output_dir = args.output_dir or payload.pop("output_dir", "") or os.path.join("backtest_runs", run_id)
if args.initial_cash is not None:
payload["initial_cash"] = args.initial_cash
if args.slippage_bps is not None:
payload["slippage_bps"] = args.slippage_bps
if args.max_volume_participation is not None:
payload["max_volume_participation"] = args.max_volume_participation
config = BacktestConfig(run_id=run_id, output_dir=output_dir, **payload)
feed = CsvBarFeed(args.data, default_symbol=args.default_symbol)
return BacktestEngine(feed, config)
def main(argv=None):
args = _parser().parse_args(argv)
engine = build_engine(args)
server = ZmqBacktestServer(
BacktestBridgeProtocol(engine),
endpoint=args.bind,
exit_on_finish=not args.keep_running,
)
startup = {
"event": "backtest_bridge_starting",
"run_id": engine.config.run_id,
"bind": args.bind,
"data_hash": engine.feed.data_hash,
"output_dir": engine.config.output_dir,
"live_ready": False,
}
print(json.dumps(startup, ensure_ascii=False, sort_keys=True), flush=True)
try:
server.serve_forever()
except KeyboardInterrupt:
server.stop()
return 0
if __name__ == "__main__":
sys.exit(main())
@@ -0,0 +1,75 @@
"""Small external-strategy contract and a synchronous ZMQ runner."""
class StrategyContext(object):
def __init__(self, client):
self.client = client
self.state = None
@property
def now(self):
return None if self.state is None else self.state.get("datetime")
@property
def cash(self):
return 0 if self.state is None else self.state.get("cash", 0)
@property
def positions(self):
return {} if self.state is None else self.state.get("positions", {})
def history(self, symbol, count=100, fields=None):
return self.client.history(symbol, count=count, fields=fields)
class ExternalStrategyRunner(object):
"""Drive a user strategy without exposing future bars.
Strategy methods are optional:
* ``on_start(context)``
* ``on_bar(context, bars) -> iterable[order dict]``
* ``on_fill(context, fill)``
* ``on_finish(context, result)``
"""
def __init__(self, client, strategy):
self.client = client
self.strategy = strategy
self.context = StrategyContext(client)
def _call(self, name, *args):
callback = getattr(self.strategy, name, None)
return callback(*args) if callback is not None else None
def _apply_orders(self, orders):
for order in list(orders or []):
payload = dict(order)
self.client.submit_order(
symbol=payload["symbol"],
side=payload["side"],
quantity=payload["quantity"],
order_type=payload.get("order_type", "MARKET"),
limit_price=payload.get("limit_price"),
client_order_id=payload.get("client_order_id", ""),
time_in_force=payload.get("time_in_force", "NEXT_BAR"),
)
def run(self):
if not self.client.run_id:
self.client.describe()
state = self.client.start()
self.context.state = state
self._call("on_start", self.context)
while True:
for fill in state.get("fills", []):
self._call("on_fill", self.context, fill)
orders = self._call("on_bar", self.context, state.get("bars", {}))
self._apply_orders(orders)
if state.get("done"):
break
state = self.client.next_bar()
self.context.state = state
result = self.client.finish()
self._call("on_finish", self.context, result)
return result
@@ -0,0 +1,65 @@
"""Minimal REQ/REP ZMQ server for one isolated backtest run."""
import json
import threading
class ZmqBacktestServer(object):
def __init__(self, protocol, endpoint="tcp://127.0.0.1:16661", exit_on_finish=False, poll_ms=100):
self.protocol = protocol
self.endpoint = str(endpoint)
self.exit_on_finish = bool(exit_on_finish)
self.poll_ms = int(poll_ms)
self._stop_event = threading.Event()
self._ready_event = threading.Event()
self.actual_endpoint = None
def wait_until_ready(self, timeout_seconds=None):
return self._ready_event.wait(timeout_seconds)
def stop(self):
self._stop_event.set()
def serve_forever(self):
import zmq
context = zmq.Context.instance()
socket = context.socket(zmq.REP)
socket.setsockopt(zmq.LINGER, 0)
socket.setsockopt(zmq.RCVHWM, 1000)
socket.setsockopt(zmq.SNDHWM, 1000)
try:
if self.endpoint.endswith(":0"):
base = self.endpoint.rsplit(":", 1)[0]
port = socket.bind_to_random_port(base)
self.actual_endpoint = "%s:%d" % (base, port)
else:
socket.bind(self.endpoint)
self.actual_endpoint = self.endpoint
self._ready_event.set()
poller = zmq.Poller()
poller.register(socket, zmq.POLLIN)
while not self._stop_event.is_set():
events = dict(poller.poll(self.poll_ms))
if socket not in events:
continue
try:
request = json.loads(socket.recv().decode("utf-8"))
response = self.protocol.handle(request)
except Exception as exc:
response = {
"schema_version": 1,
"request_id": "",
"run_id": self.protocol.engine.config.run_id,
"client_id": "",
"method": "",
"ok": False,
"data": None,
"error": "%s: %s" % (exc.__class__.__name__, exc),
}
socket.send(json.dumps(response, ensure_ascii=False, separators=(",", ":")).encode("utf-8"))
if self.exit_on_finish and self.protocol.engine.finished:
break
finally:
self._ready_event.set()
socket.close(linger=0)
@@ -0,0 +1,50 @@
# bigqmt_signal_trader
大 QMT 信号交易包的核心骨架。当前版本只完成可替换包边界和 dry-run 运行入口,不会发送真实委托。
## 已完成
- `TradeSignal``OrderRequest``PositionSnapshot``AccountSnapshot` 等核心数据模型。
- `SignalSource``MarketDataProvider``PositionProvider``OrderGateway``PositionSyncSink``StateStore` 等替换接口。
- `SignalTradingApp.tick()` 编排流程:
1. 读取信号。
2. 原子 claim。
3. 读取持仓。
4. 计算买卖数量。
5. 生成价格。
6. 调用可替换 `OrderGateway`
7. 写回状态。
8. 同步持仓快照。
- `DryRunOrderGateway`:记录委托请求,不调用真实 `passorder`
- `bigqmt_signal_trader_strategy.py`:大 QMT 运行文件骨架,响应 `init``adjust``order_callback``deal_callback``sync_positions`
## 当前安全状态
默认 `adapter_factory.build_app()` 使用:
- 空信号源。
- 空行情源。
- 空持仓源。
- dry-run 下单 gateway。
- no-op 状态存储。
- 内存持仓同步 sink。
因此即使大 QMT 加载该运行文件,也不会真实下单。
## 后续接入顺序
1. 实现 `BigQmtMarketDataProvider``BigQmtPositionProvider`
2. 实现 `BigQmtOrderGateway(passorder/cancel/get_trade_detail_data)`
3. 实现 Redis Stream / MySQL outbox 信号源。
4. 实现 Redis / MySQL 状态写回。
5. 实现 Redis / MySQL 持仓同步 sink。
6. dry-run 跑通后,再按账户灰度切换真实下单。
## 测试
```powershell
cd <REPO_ROOT>
python -m unittest discover -s tests\bigqmt_signal_trader
```
@@ -0,0 +1,32 @@
"""可替换的大 QMT 信号下单包核心模块。"""
__version__ = "0.2.0"
from .app import SignalTradingApp
from .models import (
AccountSnapshot,
AssetSnapshot,
OrderRequest,
OrderSubmitResult,
PositionSnapshot,
SignalAction,
SignalStatus,
TradeSignal,
)
from .xtquant_compat import BigQmtRpcClient, BigQmtXtData, BigQmtXtTrader
__all__ = [
"AccountSnapshot",
"AssetSnapshot",
"BigQmtRpcClient",
"BigQmtXtData",
"BigQmtXtTrader",
"OrderRequest",
"OrderSubmitResult",
"PositionSnapshot",
"SignalAction",
"SignalStatus",
"SignalTradingApp",
"TradeSignal",
"__version__",
]
@@ -0,0 +1,159 @@
"""根据配置装配 SignalTradingApp。
当前第一版只提供安全的空信号 + dry-run 默认实现,后续再接 Redis/MySQL/大 QMT
真实 adapter。这样大 QMT 运行文件可以先加载和响应调度,不会误发真实委托。
"""
from .adapters.order_dryrun import DryRunOrderGateway
from .app import SignalTradingApp
from .models import AssetSnapshot
class EmptySignalSource:
def fetch(self, account_id, limit):
return []
def ack(self, signal):
return None
class EmptyMarketDataProvider:
def get_ticks(self, codes):
return {}
def get_instrument(self, code):
return {}
class EmptyPositionProvider:
def get_positions(self, account_id):
return {}
def get_asset(self, account_id):
return AssetSnapshot(account_id=account_id, cash=None, total_asset=None)
class NoopPositionSyncSink:
def __init__(self):
self.snapshots = []
def publish(self, snapshot):
self.snapshots.append(snapshot)
class NoopStateStore:
def claim(self, signal, consumer_id):
return False
def mark_submitted(self, signal_id, result):
return None
def mark_finished(self, signal_id, status, message=""):
return None
def _config_bool(value, default=False):
if value is None:
return default
if isinstance(value, bool):
return value
return str(value).strip().lower() in ("1", "true", "yes", "y", "on")
def build_app(context_info=None, config=None):
config = config or {}
mode = str(config.get("mode") or "dryrun").lower()
account_id = config.get("account_id", "default")
source_type = str(config.get("signal_source_type") or config.get("source_type") or "").lower()
state_type = str(config.get("state_store_type") or "").lower()
position_sync_type = str(config.get("position_sync_type") or "").lower()
signal_source = config.get("signal_source")
market_data = config.get("market_data")
position_provider = config.get("position_provider")
order_gateway = config.get("order_gateway")
position_sync_sink = config.get("position_sync_sink")
state_store = config.get("state_store")
redis_client = config.get("redis_client")
if source_type == "redis" or state_type == "redis" or position_sync_type == "redis":
from .adapters.redis_common import build_redis_client
redis_client = redis_client or build_redis_client(config.get("redis") or {})
if source_type == "redis":
from .adapters.signal_redis import RedisStreamSignalSource
redis_cfg = config.get("redis") or {}
signal_source = signal_source or RedisStreamSignalSource(
redis_client=redis_client,
stream_key_template=redis_cfg.get("stream_key_template", "bigqmt:signals:{account_id}"),
group_name=redis_cfg.get("group_name", "bigqmt-signal-trader"),
consumer_name=redis_cfg.get("consumer_name", "bigqmt-consumer"),
block_ms=int(redis_cfg.get("block_ms", 0)),
)
if state_type == "redis" or (source_type == "redis" and state_store is None):
from .adapters.state_redis import RedisStateStore
redis_cfg = config.get("redis") or {}
state_store = state_store or RedisStateStore(
redis_client=redis_client,
account_id=account_id,
claim_key_template=redis_cfg.get("claim_key_template", "bigqmt:signal_claim:{account_id}:{signal_id}"),
status_key_template=redis_cfg.get("status_key_template", "bigqmt:signal_status:{account_id}:{signal_id}"),
claim_ttl_seconds=int(redis_cfg.get("claim_ttl_seconds", 3600)),
status_ttl_seconds=int(redis_cfg.get("status_ttl_seconds", 86400)),
)
if position_sync_type == "redis":
from .adapters.position_sync_redis import RedisPositionSyncSink
redis_cfg = config.get("redis") or {}
position_sync_sink = position_sync_sink or RedisPositionSyncSink(
redis_client=redis_client,
key_template=redis_cfg.get("position_key_template", "bigqmt:positions:{account_id}"),
event_stream_template=redis_cfg.get("position_event_stream_template", "bigqmt:position_events:{account_id}"),
ttl_seconds=int(redis_cfg.get("position_ttl_seconds", 120)),
publish_events=_config_bool(redis_cfg.get("position_publish_events"), True),
)
if mode == "bigqmt":
from .adapters.market_bigqmt import BigQmtMarketDataProvider
from .adapters.order_bigqmt import BigQmtOrderGateway
from .adapters.position_bigqmt import BigQmtPositionProvider
qmt_api = config.get("qmt_api") or {}
get_trade_detail_data_func = qmt_api.get("get_trade_detail_data")
market_data = market_data or BigQmtMarketDataProvider(context_info, qmt_api=qmt_api)
position_provider = position_provider or BigQmtPositionProvider(
get_trade_detail_data_func=get_trade_detail_data_func,
account_type=config.get("account_type", "STOCK"),
)
# passorder / cancel need the RAW QMT ContextInfo as their last arg -- QMT's
# injected passorder reads internals off it (e.g. .request_id). Our runtime
# wrapper (BigQmtRuntimeAdapter) doesn't have those, so unwrap it here.
raw_context_info = getattr(context_info, "context_info", context_info)
order_gateway = order_gateway or BigQmtOrderGateway(
context_info=raw_context_info,
account_id=account_id,
passorder_func=qmt_api.get("passorder"),
cancel_func=qmt_api.get("cancel"),
get_trade_detail_data_func=get_trade_detail_data_func,
account_type=config.get("account_type", "STOCK"),
combo_type=int(config.get("combo_type", 1101)),
price_type=int(config.get("order_price_type", 11)),
quick_trade=int(config.get("quick_trade", 2)),
)
return SignalTradingApp(
account_id=account_id,
signal_source=signal_source or EmptySignalSource(),
market_data=market_data or EmptyMarketDataProvider(),
position_provider=position_provider or EmptyPositionProvider(),
order_gateway=order_gateway or DryRunOrderGateway(),
position_sync_sink=position_sync_sink or NoopPositionSyncSink(),
state_store=state_store or NoopStateStore(),
consumer_id=config.get("consumer_id", "bigqmt-signal-trader"),
fetch_limit=config.get("fetch_limit", 20),
)
@@ -0,0 +1 @@
"""具体外部系统 adapter。"""
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,254 @@
"""Big QMT order gateway.
The passorder signature follows src/api/qmt_jq_trade.
"""
import hashlib
from ..code_utils import normalize_stock_code
from ..models import CancelResult, OrderSnapshot, OrderSubmitResult, SignalAction, TradeSnapshot
from .position_bigqmt import _attr, _full_code
PRICE_TYPE_ALIASES = {
"LIMIT": 11,
"FIX_PRICE": 11,
"LATEST_PRICE": 5,
"MARKET_PEER_PRICE_FIRST": 44,
"MARKET_SH_CONVERT_5_LIMIT": 43,
"MARKET_SZ_CONVERT_5_CANCEL": 47,
}
def _action_from_offset_flag(offset_flag):
return SignalAction.BUY.value if int(offset_flag or 0) == 48 else SignalAction.SELL.value
# 报单时间。大 QMT 的 ORDER 行把日期和时间分成两个字段, MiniQMT 的
# XtOrder.order_time 是 Unix 秒, 所以要拼接后转换。成交那条路径早就读了
# m_strTradeTime, 委托这边一直漏掉 (issue #48)。
_ORDER_DATE_FIELDS = ("m_strInsertDate", "m_strOrderDate", "insert_date", "order_date")
_ORDER_TIME_FIELDS = ("m_strInsertTime", "m_strOrderTime", "insert_time", "order_time")
# 取不到时打印该行实际有哪些 m_*, 每进程一次。字段名无法离线核实,
# 猜一个然后静默返回 0 正是订单方向那个 bug 的成因。
_missing_order_time_reported = []
def _report_missing_order_time(row):
if _missing_order_time_reported:
return
_missing_order_time_reported.append(True)
try:
available = sorted(n for n in dir(row) if n.startswith("m_"))
except Exception:
available = []
print(
"[bigqmt_order] order_time not found (tried %s / %s); ORDER row exposes: %s"
% (", ".join(_ORDER_DATE_FIELDS), ", ".join(_ORDER_TIME_FIELDS),
", ".join(available) or "<none>")
)
def _order_time_seconds(row):
"""把 ORDER 行的报单日期+时间转成 Unix 秒, 拿不到返回 0。
容忍几种实际会遇到的写法: 日期 '20260819''2026-08-19',
时间 '093015''09:30:15''09:30:15.123'。已经是数字时间戳的直接用
(毫秒会被归一到秒)。
"""
raw_time = _attr(row, _ORDER_TIME_FIELDS)
raw_date = _attr(row, _ORDER_DATE_FIELDS)
if raw_time is None and raw_date is None:
_report_missing_order_time(row)
return 0
# 已是数字: 当成时间戳 (>1e11 视为毫秒)。
if isinstance(raw_time, (int, float)) and not isinstance(raw_time, bool):
value = float(raw_time)
if value > 1e11:
value /= 1000.0
if value > 1e8: # 像时间戳而不是 093015 这种时分秒
return int(value)
date_text = "".join(ch for ch in str(raw_date or "") if ch.isdigit())
time_text = "".join(ch for ch in str(raw_time or "") if ch.isdigit())
if not date_text or len(date_text) < 8:
return 0
time_text = (time_text + "000000")[:6] # 补齐到 HHMMSS, 丢掉毫秒
try:
import time as _time
parsed = _time.strptime(date_text[:8] + time_text, "%Y%m%d%H%M%S")
return int(_time.mktime(parsed))
except Exception:
return 0
def _price_type_value(value, default):
if value is None or value == "":
return int(default)
try:
return int(value)
except (TypeError, ValueError):
text = str(value).strip().upper()
return int(PRICE_TYPE_ALIASES.get(text, default))
class BigQmtOrderGateway:
def __init__(
self,
context_info,
account_id="",
passorder_func=None,
cancel_func=None,
get_trade_detail_data_func=None,
account_type="STOCK",
combo_type=1101,
price_type=11,
quick_trade=2,
):
self.context_info = context_info
self.account_id = account_id
self.passorder = passorder_func
self.cancel_func = cancel_func
self.get_trade_detail_data = get_trade_detail_data_func
self.account_type = account_type
self.combo_type = combo_type
self.price_type = price_type
self.quick_trade = quick_trade
def _require_passorder(self):
if self.passorder is None:
raise RuntimeError("passorder is not available in Big QMT runtime")
return self.passorder
def _require_cancel(self):
if self.cancel_func is None:
raise RuntimeError("cancel is not available in Big QMT runtime")
return self.cancel_func
def _require_query_func(self):
if self.get_trade_detail_data is None:
raise RuntimeError("get_trade_detail_data is not available in Big QMT runtime")
return self.get_trade_detail_data
@staticmethod
def build_user_order_id(signal_id):
text = str(signal_id or "")
digest = hashlib.sha1(text.encode("utf-8")).hexdigest()[:10]
return "bq:%s:%s" % (digest, text[:30])
def submit(self, request):
passorder = self._require_passorder()
action = str(request.action).upper()
if action == SignalAction.BUY.value:
op_type = 23
elif action == SignalAction.SELL.value:
op_type = 24
else:
raise ValueError("unsupported order action: %s" % request.action)
user_order_id = str(request.remark or "").strip() or self.build_user_order_id(request.signal_id)
account_id = request.account_id or self.account_id
passorder(
op_type,
self.combo_type,
account_id,
normalize_stock_code(request.stock_code),
_price_type_value(request.price_type, self.price_type),
float(request.price),
int(request.volume),
request.strategy_name,
self.quick_trade,
user_order_id,
self.context_info,
)
return OrderSubmitResult(
status="SUBMITTED",
user_order_id=user_order_id,
order_sys_id=None,
message="passorder submitted",
)
def cancel(self, order_ref):
cancel_func = self._require_cancel()
ok = cancel_func(order_ref.order_sys_id, self.account_id, self.account_type, self.context_info)
return CancelResult(success=bool(ok), message="" if ok else "cancel returned false")
def query_orders(self, account_id, strategy_name):
try:
return self.query_orders_strict(account_id, strategy_name)
except Exception:
return []
def query_orders_strict(self, account_id, strategy_name):
query = self._require_query_func()
rows = query(account_id, self.account_type, "ORDER", strategy_name) or []
result = []
for row in rows:
result.append(
OrderSnapshot(
order_sys_id=str(_attr(row, ("m_strOrderSysID", "order_sys_id"), "") or ""),
user_order_id=str(_attr(row, ("m_strRemark", "user_order_id", "remark"), "") or ""),
stock_code=_full_code(
_attr(row, ("m_strInstrumentID", "instrument_id", "stock_code")),
_attr(row, ("m_strExchangeID", "exchange_id", "market")),
),
action=_action_from_offset_flag(_attr(row, ("m_nOffsetFlag", "offset_flag"), 0)),
volume=int(_attr(row, ("m_nVolumeTotalOriginal", "volume"), 0) or 0),
traded_volume=int(_attr(row, ("m_nVolumeTraded", "traded_volume"), 0) or 0),
status=str(_attr(row, ("m_nOrderStatus", "status"), "") or ""),
price=float(_attr(row, ("m_dLimitPrice", "m_dPrice", "price"), 0.0) or 0.0),
strategy_name=str(_attr(row, ("m_strStrategyName", "strategy_name"), "") or ""),
remark=str(_attr(row, ("m_strRemark", "remark"), "") or ""),
order_time=_order_time_seconds(row),
)
)
return result
def query_trades(self, account_id, strategy_name):
try:
return self.query_trades_strict(account_id, strategy_name)
except Exception:
return []
def query_trades_strict(self, account_id, strategy_name):
query = self._require_query_func()
rows = []
last_error = None
for detail_type in ("DEAL", "TRADE"):
try:
if str(strategy_name or "").strip():
rows = query(account_id, self.account_type, detail_type, strategy_name) or []
else:
rows = query(account_id, self.account_type, detail_type) or []
if rows:
break
except Exception as exc:
last_error = exc
if not rows and last_error is not None:
raise last_error
result = []
for row in rows:
result.append(
TradeSnapshot(
trade_id=str(_attr(row, ("m_strTradeID", "trade_id"), "") or ""),
order_sys_id=str(_attr(row, ("m_strOrderSysID", "order_sys_id"), "") or ""),
stock_code=_full_code(
_attr(row, ("m_strInstrumentID", "instrument_id", "stock_code")),
_attr(row, ("m_strExchangeID", "exchange_id", "market")),
),
action=_action_from_offset_flag(_attr(row, ("m_nOffsetFlag", "offset_flag"), 0)),
volume=int(_attr(row, ("m_nVolume", "volume"), 0) or 0),
price=float(_attr(row, ("m_dPrice", "m_dTradePrice", "price"), 0.0) or 0.0),
traded_at=str(_attr(row, ("m_strTradeTime", "trade_time", "traded_at"), "") or ""),
user_order_id=str(_attr(row, ("m_strRemark", "user_order_id", "remark"), "") or ""),
)
)
return result
def query_submission_identities_strict(self, account_id, strategy_name):
orders = self.query_orders_strict(account_id, strategy_name)
trades = self.query_trades_strict(account_id, strategy_name)
return orders, trades
@@ -0,0 +1,30 @@
"""不发真实委托的下单 gateway,用于联调和回放。"""
import hashlib
from ..models import OrderSubmitResult
class DryRunOrderGateway:
def __init__(self):
self.submitted = []
self.cancelled = []
def submit(self, request):
self.submitted.append(request)
digest = hashlib.sha1(request.signal_id.encode("utf-8")).hexdigest()[:10]
return OrderSubmitResult(
status="DRY_RUN",
user_order_id=f"dryrun:bq:{digest}:{request.signal_id}",
order_sys_id=None,
)
def cancel(self, order_ref):
self.cancelled.append(order_ref)
return None
def query_orders(self, account_id, strategy_name):
return []
def query_trades(self, account_id, strategy_name):
return []
@@ -0,0 +1,146 @@
"""Big QMT position and asset adapters."""
from ..code_utils import normalize_stock_code
from ..models import AssetSnapshot, PositionSnapshot
def _attr(obj, names, default=None):
for name in names:
if hasattr(obj, name):
value = getattr(obj, name)
if value is not None:
return value
return default
def _float_or_none(value):
if value is None:
return None
try:
return float(value)
except (TypeError, ValueError):
return None
# Candidate ThinkTrader field names on the ACCOUNT row of get_trade_detail_data.
# The MiniQMT SDK only documents the normalized name (XtAsset.frozen_cash); the
# big QMT ACCOUNT struct is a different surface and brokers vary, so probe the
# plausible spellings the way cash/total_asset already do.
_FROZEN_CASH_FIELDS = (
"m_dFrozenCash",
"m_dFrozen",
"m_dFrozenBalance",
"m_dFrozenMargin",
"frozen_cash",
"frozen",
)
_MARKET_VALUE_FIELDS = (
"m_dInstrumentValue",
"m_dStockValue",
"m_dMarketValue",
"market_value",
)
# Printed once per process when the frozen field is not found, listing what the
# row actually carries. Guessing a field name and shipping it unverified is how
# the order-direction bug happened; this makes the real name self-reporting.
_missing_field_reported = set()
def _report_missing_field(label, row, candidates):
if label in _missing_field_reported:
return
_missing_field_reported.add(label)
try:
available = sorted(name for name in dir(row) if name.startswith("m_"))
except Exception:
available = []
print(
"[bigqmt_asset] %s not found (tried %s); ACCOUNT row exposes: %s"
% (label, ", ".join(candidates), ", ".join(available) or "<none>")
)
def _full_code(instrument_id, exchange_id):
code = str(instrument_id or "").strip().upper()
market = str(exchange_id or "").strip().upper()
if "." in code:
return normalize_stock_code(code)
if market in ("SH", "SZ"):
return normalize_stock_code("%s.%s" % (code, market))
return normalize_stock_code(code)
class BigQmtPositionProvider:
def __init__(self, get_trade_detail_data_func, account_type="STOCK"):
self.get_trade_detail_data = get_trade_detail_data_func
self.account_type = account_type
def _require_query_func(self):
if self.get_trade_detail_data is None:
raise RuntimeError("get_trade_detail_data is not available in Big QMT runtime")
return self.get_trade_detail_data
def get_positions(self, account_id):
query = self._require_query_func()
# QMT's get_trade_detail_data can raise on POSITION queries in some
# states (e.g. context not bound). Degrade to empty like get_asset does.
try:
rows = query(account_id, self.account_type, "POSITION") or []
except Exception:
return {}
positions = {}
for row in rows:
code = _full_code(
_attr(row, ("m_strInstrumentID", "instrument_id", "stock_code")),
_attr(row, ("m_strExchangeID", "exchange_id", "market")),
)
positions[code] = PositionSnapshot(
stock_code=code,
volume=int(_attr(row, ("m_nVolume", "volume"), 0) or 0),
available=int(_attr(row, ("m_nCanUseVolume", "available", "can_use_volume"), 0) or 0),
cost=float(_attr(row, ("m_dOpenPrice", "m_dCostPrice", "cost"), 0.0) or 0.0),
stock_name=str(_attr(row, ("m_strInstrumentName", "stock_name"), "") or ""),
market_value=_float_or_none(_attr(row, ("m_dMarketValue", "m_dInstrumentValue", "market_value"))),
price=_float_or_none(_attr(row, ("m_dLastPrice", "m_dSettlementPrice", "price", "last_price"))),
open_price=_float_or_none(_attr(row, ("m_dOpenPrice", "m_dCostPrice", "open_price", "cost"))),
frozen_volume=int(_attr(row, ("m_nFrozenVolume", "frozen_volume"), 0) or 0),
on_road_volume=int(_attr(row, ("m_nOnRoadVolume", "on_road_volume"), 0) or 0),
yesterday_volume=int(_attr(row, ("m_nYesterdayVolume", "yesterday_volume"), 0) or 0),
direction=int(_attr(row, ("m_nDirection", "direction"), 48) or 48),
)
return positions
def get_asset(self, account_id):
query = self._require_query_func()
rows = []
for detail_type in ("ACCOUNT", "ASSET"):
try:
rows = query(account_id, self.account_type, detail_type) or []
if rows:
break
except Exception:
rows = []
if not rows:
return AssetSnapshot(account_id=account_id, cash=None, total_asset=None)
row = rows[0]
cash = _attr(row, ("m_dAvailable", "m_dAvailableCash", "available_cash", "cash"))
total_asset = _attr(row, ("m_dBalance", "m_dAsset", "total_asset", "asset"))
frozen_cash = _attr(row, _FROZEN_CASH_FIELDS)
market_value = _attr(row, _MARKET_VALUE_FIELDS)
if frozen_cash is None:
_report_missing_field("frozen_cash", row, _FROZEN_CASH_FIELDS)
if market_value is None and cash is not None and total_asset is not None:
# Derive only as a last resort. Without frozen_cash this overstates
# market value by the frozen amount, so subtract it when known.
market_value = float(total_asset) - float(cash)
if frozen_cash is not None:
market_value -= float(frozen_cash)
return AssetSnapshot(
account_id=account_id,
cash=float(cash) if cash is not None else None,
total_asset=float(total_asset) if total_asset is not None else None,
frozen_cash=float(frozen_cash) if frozen_cash is not None else None,
market_value=float(market_value) if market_value is not None else None,
)

Some files were not shown because too many files have changed in this diff Show More