diff --git a/examples/VnTrader/run.py b/examples/VnTrader/run.py index e174bd53..22c5a54a 100644 --- a/examples/VnTrader/run.py +++ b/examples/VnTrader/run.py @@ -18,7 +18,7 @@ from vnpy.trader.gateway import (ctpGateway, femasGateway, xspeedGateway, # 加载上层应用 from vnpy.trader.app import (riskManager, dataRecorder, - ctaStrategy) + ctaStrategy, spreadTrading) #---------------------------------------------------------------------- @@ -45,6 +45,7 @@ def main(): me.addApp(riskManager) me.addApp(dataRecorder) me.addApp(ctaStrategy) + me.addApp(spreadTrading) # 创建主窗口 mw = MainWindow(me, ee) diff --git a/vnpy/trader/app/ctaStrategy/ctaBase.py b/vnpy/trader/app/ctaStrategy/ctaBase.py index 1bc92b79..10e257ad 100644 --- a/vnpy/trader/app/ctaStrategy/ctaBase.py +++ b/vnpy/trader/app/ctaStrategy/ctaBase.py @@ -33,6 +33,10 @@ MINUTE_DB_NAME = 'VnTrader_1Min_Db' ENGINETYPE_BACKTESTING = 'backtesting' # 回测 ENGINETYPE_TRADING = 'trading' # 实盘 +# CTA模块事件 +EVENT_CTA_LOG = 'eCtaLog' # CTA相关的日志事件 +EVENT_CTA_STRATEGY = 'eCtaStrategy.' # CTA策略状态变化事件 + # CTA引擎中涉及的数据类定义 from vnpy.trader.vtConstant import EMPTY_UNICODE, EMPTY_STRING, EMPTY_FLOAT, EMPTY_INT diff --git a/vnpy/trader/app/dataRecorder/drBase.py b/vnpy/trader/app/dataRecorder/drBase.py index cddfd90a..43c4f05d 100644 --- a/vnpy/trader/app/dataRecorder/drBase.py +++ b/vnpy/trader/app/dataRecorder/drBase.py @@ -12,6 +12,8 @@ TICK_DB_NAME = 'VnTrader_Tick_Db' DAILY_DB_NAME = 'VnTrader_Daily_Db' MINUTE_DB_NAME = 'VnTrader_1Min_Db' +# 行情记录模块事件 +EVENT_DATARECORDER_LOG = 'eDataRecorderLog' # 行情记录日志更新事件 # CTA引擎中涉及的数据类定义 from vnpy.trader.vtConstant import EMPTY_UNICODE, EMPTY_STRING, EMPTY_FLOAT, EMPTY_INT diff --git a/vnpy/trader/app/spreadTrading/ST_setting.json b/vnpy/trader/app/spreadTrading/ST_setting.json new file mode 100644 index 00000000..b2edf2c1 --- /dev/null +++ b/vnpy/trader/app/spreadTrading/ST_setting.json @@ -0,0 +1,64 @@ +[ + { + "name": "IF.07-09", + + "activeLeg": + { + "vtSymbol": "IF1707", + "ratio": 1, + "multiplier": 1.0, + "payup": 2 + }, + + "passiveLegs": [ + { + "vtSymbol": "IF1709", + "ratio": -1, + "multiplier": -1.0, + "payup": 2 + } + ] + }, + + { + "name": "IH.07-09", + + "activeLeg": + { + "vtSymbol": "IH1707", + "ratio": 1, + "multiplier": 1.0, + "payup": 2 + }, + + "passiveLegs": [ + { + "vtSymbol": "IH1709", + "ratio": -1, + "multiplier": -1.0, + "payup": 2 + } + ] + }, + + { + "name": "IC.07-09", + + "activeLeg": + { + "vtSymbol": "IC1707", + "ratio": 1, + "multiplier": 1.0, + "payup": 2 + }, + + "passiveLegs": [ + { + "vtSymbol": "IC1709", + "ratio": -1, + "multiplier": -1.0, + "payup": 2 + } + ] + } +] \ No newline at end of file diff --git a/vnpy/trader/app/spreadTrading/__init__.py b/vnpy/trader/app/spreadTrading/__init__.py new file mode 100644 index 00000000..722ecaf7 --- /dev/null +++ b/vnpy/trader/app/spreadTrading/__init__.py @@ -0,0 +1,10 @@ +# encoding: UTF-8 + +from .stEngine import StEngine +from .uiStWidget import StManager + +appName = 'SpreadTrading' +appDisplayName = u'价差交易' +appEngine = StEngine +appWidget = StManager +appIco = 'st.ico' \ No newline at end of file diff --git a/vnpy/trader/app/spreadTrading/st.ico b/vnpy/trader/app/spreadTrading/st.ico new file mode 100644 index 00000000..a66c245e Binary files /dev/null and b/vnpy/trader/app/spreadTrading/st.ico differ diff --git a/vnpy/trader/app/spreadTrading/stBase.py b/vnpy/trader/app/spreadTrading/stBase.py new file mode 100644 index 00000000..9297b764 --- /dev/null +++ b/vnpy/trader/app/spreadTrading/stBase.py @@ -0,0 +1,162 @@ +# encoding: UTF-8 + +from __future__ import division + +from math import floor +from datetime import datetime + +from vnpy.trader.vtConstant import (EMPTY_INT, EMPTY_FLOAT, + EMPTY_STRING, EMPTY_UNICODE) + + + +EVENT_SPREADTRADING_TICK = 'eSpreadTradingTick.' + + + +######################################################################## +class StLeg(object): + """""" + + #---------------------------------------------------------------------- + def __init__(self): + """Constructor""" + self.vtSymbol = EMPTY_STRING # 代码 + self.ratio = EMPTY_INT # 实际交易时的比例 + self.multiplier = EMPTY_FLOAT # 计算价差时的乘数 + self.payup = EMPTY_INT # 对冲时的超价tick + + self.bidPrice = EMPTY_FLOAT + self.askPrice = EMPTY_FLOAT + self.bidVolume = EMPTY_INT + self.askVolume = EMPTY_INT + + self.longPos = EMPTY_INT + self.shortPos = EMPTY_INT + self.netPos = EMPTY_INT + + +######################################################################## +class StSpread(object): + """""" + + #---------------------------------------------------------------------- + def __init__(self): + """Constructor""" + self.name = EMPTY_UNICODE # 名称 + self.symbol = EMPTY_STRING # 代码(基于组成腿计算) + + self.activeLeg = None # 主动腿 + self.passiveLegs = [] # 被动腿(支持多条) + self.allLegs = [] # 所有腿 + + self.bidPrice = EMPTY_FLOAT + self.askPrice = EMPTY_FLOAT + self.bidVolume = EMPTY_INT + self.askVolume = EMPTY_INT + self.time = EMPTY_STRING + + self.longPos = EMPTY_INT + self.shortPos = EMPTY_INT + self.netPos = EMPTY_INT + + #---------------------------------------------------------------------- + def initSpread(self): + """初始化价差""" + # 价差最少要有一条主动腿 + if not self.activeLeg: + return + + # 生成所有腿列表 + self.allLegs.append(self.activeLeg) + self.allLegs.extend(self.passiveLegs) + + # 生成价差代码 + legSymbolList = [] + + for leg in self.allLegs: + if leg.multiplier >= 0: + legSymbol = '+%s*%s' %(leg.multiplier, leg.vtSymbol) + else: + legSymbol = '%s*%s' %(leg.multiplier, leg.vtSymbol) + legSymbolList.append(legSymbol) + + self.symbol = ''.join(legSymbolList) + + #---------------------------------------------------------------------- + def calculatePrice(self): + """计算价格""" + # 清空价格和委托量数据 + self.bidPrice = EMPTY_FLOAT + self.askPrice = EMPTY_FLOAT + self.askVolume = EMPTY_INT + self.bidVolume = EMPTY_INT + + # 遍历价差腿列表 + for n, leg in enumerate(self.allLegs): + # 计算价格 + if leg.multiplier > 0: + self.bidPrice += leg.bidPrice * leg.multiplier + self.askPrice += leg.askPrice * leg.multiplier + else: + self.bidPrice += leg.askPrice * leg.multiplier + self.askPrice += leg.bidPrice * leg.multiplier + + # 计算报单量 + if leg.ratio > 0: + legAdjustedBidVolume = floor(leg.bidVolume / leg.ratio) + legAdjustedAskVolume = floor(leg.askVolume / leg.ratio) + else: + legAdjustedBidVolume = floor(leg.askVolume / abs(leg.ratio)) + legAdjustedAskVolume = floor(leg.bidVolume / abs(leg.ratio)) + + if n == 0: + self.bidVolume = legAdjustedBidVolume # 对于第一条腿,直接初始化 + self.askVolume = legAdjustedAskVolume + else: + self.bidVolume = min(self.bidVolume, legAdjustedBidVolume) # 对于后续的腿,价差可交易报单量取较小值 + self.askVolume = min(self.askVolume, legAdjustedAskVolume) + + # 更新时间 + self.time = datetime.now().strftime('%H:%M:%S.%f')[:-3] + + #---------------------------------------------------------------------- + def calculatePos(self): + """计算持仓""" + # 清空持仓数据 + self.longPos = EMPTY_INT + self.shortPos = EMPTY_INT + self.netPos = EMPTY_INT + + # 遍历价差腿列表 + for n, leg in enumerate(self.allLegs): + if leg.ratio > 0: + legAdjustedLongPos = floor(leg.longPos / leg.ratio) + legAdjustedShortPos = floor(leg.shortPos / leg.ratio) + else: + legAdjustedLongPos = floor(leg.shortPos / abs(leg.ratio)) + legAdjustedShortPos = floor(leg.longPos / abs(leg.ratio)) + + if n == 0: + self.longPos = legAdjustedLongPos + self.shortPos = legAdjustedShortPos + else: + self.longPos = min(self.longPos, legAdjustedLongPos) + self.shortPos = min(self.shortPos, legAdjustedShortPos) + + # 计算净仓位 + self.netPos = self.longPos - self.shortPos + + #---------------------------------------------------------------------- + def addActiveLeg(self, leg): + """添加主动腿""" + self.activeLeg = leg + + #---------------------------------------------------------------------- + def addPassiveLeg(self, leg): + """添加被动腿""" + self.passiveLegs.append(leg) + + + + \ No newline at end of file diff --git a/vnpy/trader/app/spreadTrading/stEngine.py b/vnpy/trader/app/spreadTrading/stEngine.py new file mode 100644 index 00000000..d5f38a9b --- /dev/null +++ b/vnpy/trader/app/spreadTrading/stEngine.py @@ -0,0 +1,176 @@ +# encoding: UTF-8 + +import json +from copy import copy + +from vnpy.event import Event +from vnpy.trader.vtFunction import getJsonPath +from vnpy.trader.vtEvent import EVENT_TICK, EVENT_TRADE, EVENT_POSITION +from vnpy.trader.vtObject import VtSubscribeReq + +from .stBase import StLeg, StSpread, EVENT_SPREADTRADING_TICK + + +######################################################################## +class StEngine(object): + """""" + settingFileName = 'ST_setting.json' + settingFilePath = getJsonPath(settingFileName, __file__) + + #---------------------------------------------------------------------- + def __init__(self, mainEngine, eventEngine): + """Constructor""" + self.mainEngine = mainEngine + self.eventEngine = eventEngine + + # 腿、价差相关字典 + self.legDict = {} # vtSymbol:StLeg + self.spreadDict = {} # name:StSpread + self.vtSymbolSpreadDict = {} # vtSymbol:StSpread + + self.registerEvent() + + #---------------------------------------------------------------------- + def loadSetting(self): + """加载配置""" + with open(self.settingFilePath) as f: + l = json.load(f) + + for setting in l: + self.createSpread(setting) + + #---------------------------------------------------------------------- + def saveSetting(self): + """保存配置""" + with open(self.settingFilePath) as f: + pass + + #---------------------------------------------------------------------- + def createSpread(self, setting): + """创建价差""" + result = False + msg = '' + + # 检查价差重名 + if setting['name'] in self.spreadDict: + msg = u'%s价差重名' %setting['name'] + return result, msg + + # 检查腿是否已使用 + l = [] + l.append(setting['activeLeg']['vtSymbol']) + for d in setting['passiveLegs']: + l.append(d['vtSymbol']) + + for vtSymbol in l: + if vtSymbol in self.vtSymbolSpreadDict: + existingSpread = self.vtSymbolSpreadDict[vtSymbol] + msg = u'%s合约已经存在于%s价差中' %(vtSymbol, existingSpread.name) + return result, msg + + # 创建价差 + spread = StSpread() + spread.name = setting['name'] + self.spreadDict[spread.name] = spread + + # 创建主动腿 + activeSetting = setting['activeLeg'] + + activeLeg = StLeg() + activeLeg.vtSymbol = activeSetting['vtSymbol'] + activeLeg.ratio = activeSetting['ratio'] + activeLeg.multiplier = activeSetting['multiplier'] + activeLeg.payup = activeSetting['payup'] + + spread.addActiveLeg(activeLeg) + self.legDict[activeLeg.vtSymbol] = activeLeg + self.vtSymbolSpreadDict[activeLeg.vtSymbol] = spread + + self.subscribeMarketData(activeLeg.vtSymbol) + + # 创建被动腿 + passiveSettingList = setting['passiveLegs'] + passiveLegList = [] + + for d in passiveSettingList: + passiveLeg = StLeg() + passiveLeg.vtSymbol = d['vtSymbol'] + passiveLeg.ratio = d['ratio'] + passiveLeg.multiplier = d['multiplier'] + passiveLeg.payup = d['payup'] + + spread.addPassiveLeg(passiveLeg) + self.legDict[passiveLeg.vtSymbol] = passiveLeg + self.vtSymbolSpreadDict[passiveLeg.vtSymbol] = spread + + self.subscribeMarketData(passiveLeg.vtSymbol) + + # 初始化价差 + spread.initSpread() + + # 返回结果 + result = True + msg = u'%s价差创建成功' %spread.name + return result, msg + + #---------------------------------------------------------------------- + def processTickEvent(self, event): + """处理行情推送""" + # 检查行情是否需要处理 + tick = event.dict_['data'] + if tick.vtSymbol not in self.legDict: + return + + # 更新腿价格 + leg = self.legDict[tick.vtSymbol] + leg.bidPrice = tick.bidPrice1 + leg.askPrice = tick.askPrice1 + leg.bidVolume = tick.bidVolume1 + leg.askVolume = tick.askVolume1 + + # 更新价差价格 + spread = self.vtSymbolSpreadDict[tick.vtSymbol] + spread.calculatePrice() + + # 推送价差更新 + newSpread = copy(spread) + event = Event(EVENT_SPREADTRADING_TICK) + event.dict_['data'] = newSpread + self.eventEngine.put(event) + + #---------------------------------------------------------------------- + def processTradeEvent(self, event): + """""" + pass + + #---------------------------------------------------------------------- + def processPositionEvent(self, event): + """""" + pass + + #---------------------------------------------------------------------- + def registerEvent(self): + """""" + self.eventEngine.register(EVENT_TICK, self.processTickEvent) + self.eventEngine.register(EVENT_TRADE, self.processTradeEvent) + self.eventEngine.register(EVENT_POSITION, self.processPositionEvent) + + #---------------------------------------------------------------------- + def subscribeMarketData(self, vtSymbol): + """订阅行情""" + contract = self.mainEngine.getContract(vtSymbol) + if not contract: + return + + req = VtSubscribeReq() + req.symbol = contract.symbol + req.exchange = contract.exchange + + self.mainEngine.subscribe(req, contract.gatewayName) + + #---------------------------------------------------------------------- + def stop(self): + """停止""" + pass + + \ No newline at end of file diff --git a/vnpy/trader/app/spreadTrading/uiStWidget.py b/vnpy/trader/app/spreadTrading/uiStWidget.py new file mode 100644 index 00000000..c0cd5b21 --- /dev/null +++ b/vnpy/trader/app/spreadTrading/uiStWidget.py @@ -0,0 +1,97 @@ +# encoding: UTF-8 + +from collections import OrderedDict + +from vnpy.trader.uiQt import QtWidgets +from vnpy.trader.uiBasicWidget import (BasicMonitor, BasicCell, + AskCell, BidCell, BASIC_FONT) + +from .stBase import EVENT_SPREADTRADING_TICK + + + +######################################################################## +class StTickMonitor(BasicMonitor): + """""" + + #---------------------------------------------------------------------- + def __init__(self, mainEngine, eventEngine, parent=None): + """Constructor""" + super(StTickMonitor, self).__init__(mainEngine, eventEngine, parent) + + # 设置表头有序字典 + d = OrderedDict() + d['name'] = {'chinese':u'价差名称', 'cellType':BasicCell} + d['bidPrice'] = {'chinese':u'买价', 'cellType':BidCell} + d['bidVolume'] = {'chinese':u'买量', 'cellType':BidCell} + d['askPrice'] = {'chinese':u'卖价', 'cellType':AskCell} + d['askVolume'] = {'chinese':u'卖量', 'cellType':AskCell} + d['time'] = {'chinese':u'时间', 'cellType':BasicCell} + d['symbol'] = {'chinese':u'代码', 'cellType':BasicCell} + self.setHeaderDict(d) + + # 设置数据键 + self.setDataKey('name') + + # 设置监控事件类型 + self.setEventType(EVENT_SPREADTRADING_TICK) + + # 设置字体 + self.setFont(BASIC_FONT) + + # 初始化表格 + self.initTable() + + # 注册事件监听 + self.registerEvent() + + +######################################################################## +class StManager(QtWidgets.QWidget): + """""" + + #---------------------------------------------------------------------- + def __init__(self, stEngine, eventEngine, parent=None): + """Constructor""" + super(StManager, self).__init__(parent) + + self.stEngine = stEngine + self.mainEngine = stEngine.mainEngine + self.eventEngine = eventEngine + + self.initUi() + + #---------------------------------------------------------------------- + def initUi(self): + """初始化界面""" + self.setWindowTitle(u'价差交易') + + # 创建按钮 + buttonLoadSetting = QtWidgets.QPushButton(u'加载配置') + + buttonLoadSetting.clicked.connect(self.stEngine.loadSetting) + + # 创建组件 + tickMonitor = StTickMonitor(self.mainEngine, self.eventEngine) + + # 设置布局 + hbox = QtWidgets.QHBoxLayout() + hbox.addWidget(buttonLoadSetting) + hbox.addStretch() + + vbox = QtWidgets.QVBoxLayout() + vbox.addLayout(hbox) + vbox.addWidget(tickMonitor) + + self.setLayout(vbox) + + #---------------------------------------------------------------------- + def show(self): + """重载显示""" + self.showMaximized() + + + + + + \ No newline at end of file diff --git a/vnpy/trader/gateway/ctpGateway/ctpGateway.py b/vnpy/trader/gateway/ctpGateway/ctpGateway.py index 619eacea..2294cd21 100644 --- a/vnpy/trader/gateway/ctpGateway/ctpGateway.py +++ b/vnpy/trader/gateway/ctpGateway/ctpGateway.py @@ -91,8 +91,6 @@ class CtpGateway(VtGateway): self.tdConnected = False # 交易API连接状态 self.qryEnabled = False # 循环查询 - - self.requireAuthentication = False #---------------------------------------------------------------------- def connect(self): diff --git a/vnpy/trader/gateway/windGateway/windGateway.py b/vnpy/trader/gateway/windGateway/windGateway.py index 108643d1..d1307841 100644 --- a/vnpy/trader/gateway/windGateway/windGateway.py +++ b/vnpy/trader/gateway/windGateway/windGateway.py @@ -26,6 +26,9 @@ exchangeMap[EXCHANGE_CZCE] = 'CZC' exchangeMap[EXCHANGE_UNKNOWN] = '' exchangeMapReverse = {v:k for k,v in exchangeMap.items()} +# Wind接口相关事件 +EVENT_WIND_CONNECTREQ = 'eWindConnectReq' # Wind接口请求连接事件 + ######################################################################## class WindGateway(VtGateway): diff --git a/vnpy/trader/vtEvent.py b/vnpy/trader/vtEvent.py index b26175fb..4c5c67ef 100644 --- a/vnpy/trader/vtEvent.py +++ b/vnpy/trader/vtEvent.py @@ -17,14 +17,4 @@ EVENT_ORDER = 'eOrder.' # 报单回报事件 EVENT_POSITION = 'ePosition.' # 持仓回报事件 EVENT_ACCOUNT = 'eAccount.' # 账户回报事件 EVENT_CONTRACT = 'eContract.' # 合约基础信息回报事件 -EVENT_ERROR = 'eError.' # 错误回报事件 - -# CTA模块相关 -EVENT_CTA_LOG = 'eCtaLog' # CTA相关的日志事件 -EVENT_CTA_STRATEGY = 'eCtaStrategy.' # CTA策略状态变化事件 - -# 行情记录模块相关 -EVENT_DATARECORDER_LOG = 'eDataRecorderLog' # 行情记录日志更新事件 - -# Wind接口相关 -EVENT_WIND_CONNECTREQ = 'eWindConnectReq' # Wind接口请求连接事件 \ No newline at end of file +EVENT_ERROR = 'eError.' # 错误回报事件 \ No newline at end of file diff --git a/vnpy/trader/vtFunction.py b/vnpy/trader/vtFunction.py index 189d1d82..32bc99a4 100644 --- a/vnpy/trader/vtFunction.py +++ b/vnpy/trader/vtFunction.py @@ -64,5 +64,20 @@ def getTempPath(name): path = os.path.join(tempPath, name) return path +#---------------------------------------------------------------------- +def getJsonPath(name, moduleFile): + """ + 获取JSON配置文件的路径: + 1. 优先从当前工作目录查找JSON文件 + 2. 若无法找到则前往模块所在目录查找 + """ + currentFolder = os.getcwd() + currentJsonPath = os.path.join(currentFolder, name) + if os.path.isfile(currentJsonPath): + return currentJsonPath + + moduleFolder = os.path.abspath(os.path.dirname(moduleFile)) + moduleJsonPath = os.path.join(moduleFolder, '.', name) + return moduleJsonPath