├── .gitignore ├── LICENSE ├── README.md ├── py3_ctp_demo_on_vnpy ├── api │ ├── 6.3.13 │ │ ├── thostmduserapi_se.dll │ │ └── thosttraderapi_se.dll │ ├── 6.3.15 │ │ ├── thostmduserapi_se.dll │ │ └── thosttraderapi_se.dll │ └── README.md ├── demoMain.py ├── log │ ├── EventLog │ │ └── deleteme │ └── deleteme ├── modules │ ├── RM_setting.json │ ├── __init__.py │ ├── baseSetting.py │ ├── ctaEngine.py │ ├── ctpApi.py │ ├── ctpDataType.py │ ├── eventEngine.py │ ├── eventType.py │ ├── functions.py │ ├── objects.py │ ├── rmEngine.py │ ├── thostmduserapi_se.dll │ ├── thosttraderapi_se.dll │ ├── uiWidgets.py │ ├── vnctpmd.pyd │ └── vnctptd.pyd ├── resource │ └── fs.ico ├── setting │ ├── CTA_setting.json │ ├── syncdata │ │ └── deleteme │ └── user.json ├── strategy │ ├── __init__.py │ ├── __pycache__ │ │ ├── __init__.cpython-36.pyc │ │ ├── strategyFlashStalker.cpython-36.pyc │ │ └── strategyRandom.cpython-36.pyc │ └── strategyRandom.py └── temp │ ├── contracts │ └── deleteme └── screenshots └── screenshot20180308.PNG /.gitignore: -------------------------------------------------------------------------------- 1 | # Windows: 2 | Thumbs.db 3 | ehthumbs.db 4 | Desktop.ini 5 | 6 | # Python: 7 | *.pyc 8 | *.so 9 | *.egg 10 | *.egg-info 11 | dist 12 | build 13 | 14 | # My configurations: -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Zhang Li 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # py3_demo_on_vnpy_ctp 2 | #### 更新: 3 | 4 | - 20190603 更新穿透式监管ctpapi 5 | 6 | #### 特点: 7 | 8 | - 基于vnpy的ctp接口的简化版本,实现查询功能的基础上增加了ctaEngine。适用于认为vnpy功能太多无法驾驭,只需要ctp和策略功能的新人(像我这样)。 9 | 10 | - python3版本,无数据库,无talib,适合开发不依赖历史数据的日内策略。策略载入历史数据需要自行实现。 11 | 12 | - 因为是自用的系统,很多处理是个性化和不完善的,请自行修改。 13 | 14 | #### 运行环境: 15 | 16 | - 64位版本windows7或windows10,python3.7(Anaconda3-2019.03-Windows-x86_64测试可用) 17 | 18 | - 项目中的ctpapi是从vnpy开源项目搬运过来的,选择上述运行环境可以不需要自行编译ctpapi。 19 | 20 | - linux环境需要自行编译ctpapi,推荐方法如下: 21 | https://www.wepin.online/blog/0015640409375992273687df432443499ca0d69de612df9000 22 | 23 | #### 使用方法: 24 | 25 | - 修改user.json中的模拟账号。 26 | 27 | - 修改basesetting中的工作目录WORKING_DIR为你的工作目录。 28 | 29 | - 启动demoMain.py。 30 | 31 | 32 | 33 | 34 | ![](https://github.com/vvipi/py3_demo_on_vnpy_ctp/raw/master/screenshots/screenshot20180308.PNG) 35 | 36 | -------------------------------------------------------------------------------- /py3_ctp_demo_on_vnpy/api/6.3.13/thostmduserapi_se.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vvipi/py3_demo_on_vnpy_ctp/491f6394ce049a7dd93a34415cd2ffe499bf9ffa/py3_ctp_demo_on_vnpy/api/6.3.13/thostmduserapi_se.dll -------------------------------------------------------------------------------- /py3_ctp_demo_on_vnpy/api/6.3.13/thosttraderapi_se.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vvipi/py3_demo_on_vnpy_ctp/491f6394ce049a7dd93a34415cd2ffe499bf9ffa/py3_ctp_demo_on_vnpy/api/6.3.13/thosttraderapi_se.dll -------------------------------------------------------------------------------- /py3_ctp_demo_on_vnpy/api/6.3.15/thostmduserapi_se.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vvipi/py3_demo_on_vnpy_ctp/491f6394ce049a7dd93a34415cd2ffe499bf9ffa/py3_ctp_demo_on_vnpy/api/6.3.15/thostmduserapi_se.dll -------------------------------------------------------------------------------- /py3_ctp_demo_on_vnpy/api/6.3.15/thosttraderapi_se.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vvipi/py3_demo_on_vnpy_ctp/491f6394ce049a7dd93a34415cd2ffe499bf9ffa/py3_ctp_demo_on_vnpy/api/6.3.15/thosttraderapi_se.dll -------------------------------------------------------------------------------- /py3_ctp_demo_on_vnpy/api/README.md: -------------------------------------------------------------------------------- 1 | #### 说明: 2 | 3 | - 6.3.13是测评版本的api,6.3.15是实盘版本。切换版本只需要把相应的dll文件覆盖到modules文件夹下。预编译的pyd文件不需要动,代码也不需要动。 4 | 5 | - user.json中的服务器地址要相应修改,测评版本只能连测评版本服务器,实盘同理。 6 | 7 | - 有的期货公司估计服务架设不完整,测评的交易可以连,行情连不上。这种情况行情服务器直接用实盘的,反正不验证登录。 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /py3_ctp_demo_on_vnpy/demoMain.py: -------------------------------------------------------------------------------- 1 | # encoding: UTF-8 2 | ''' 3 | 主程序 4 | 导入CTP API、事件引擎、策略引擎、风控引擎、GUI 5 | ''' 6 | # 系统模块 7 | from datetime import datetime, time, timedelta 8 | import functools 9 | # 自己开发的模块 10 | from modules.ctpApi import * 11 | from modules.eventEngine import * 12 | from modules.eventType import * 13 | from modules.ctaEngine import CtaEngine 14 | from modules.rmEngine import * 15 | from modules.uiWidgets import * 16 | from modules.objects import * 17 | from modules.functions import load_json, save_json 18 | from modules.baseSetting import WORKING_DIR, USER_FILE, globalSetting 19 | 20 | 21 | 22 | def stand_alone(func): 23 | ''' 24 | 装饰器 25 | 如果已经有实例在跑则退出 26 | ''' 27 | @functools.wraps(func) 28 | def f(*args,**kwargs): 29 | import socket 30 | try: 31 | # 全局属性,否则变量会在方法退出后被销毁 32 | global soket_bind 33 | soket_bind = socket.socket() 34 | host = socket.gethostname() 35 | soket_bind.bind((host, 9527)) 36 | except: 37 | print('已经运行一个实例,不能重复打开') 38 | return 39 | return func(*args,**kwargs) 40 | return f 41 | 42 | @stand_alone 43 | class MainEngine: 44 | """主引擎,负责对API的调度""" 45 | 46 | FINISHED_STATUS = [STATUS_ALLTRADED, STATUS_REJECTED, STATUS_CANCELLED] 47 | 48 | #---------------------------------------------------------------------- 49 | def __init__(self): 50 | """Constructor""" 51 | self.ee = EventEngine() # 创建事件驱动引擎 52 | self.re = RmEngine(self.ee) # 创建风控引擎 53 | self.md = CtpMdApi(self.ee) # 创建行情API接口 54 | self.td = CtpTdApi(self.re, self.ee) # 创建交易API接口 55 | self.ce = CtaEngine(self, self.ee) # 创建CTA引擎 56 | self.ee.start() # 启动事件驱动引擎 57 | self.list_instrument = [] # 保存合约资料 58 | self.ee.register(EVENT_INSTRUMENT, self.insertInstrument) 59 | self.list_marketdata = [] # 保存行情截面资料 60 | self.ee.register(EVENT_MARKETDATA, self.insertMarketData) 61 | self.contractDict = {} # 合约信息字典 62 | self.loadContracts() 63 | self.ee.register(EVENT_CONTRACT, self.updateContract) 64 | 65 | self.userID = '' # 账号 66 | self.password = '' # 密码 67 | self.brokerID = '' # 经纪商代码 68 | self.MdIp = '' # 行情服务器地址 69 | self.TdIp = '' # 交易服务器地址 70 | self.authCode = '' # 授权码 71 | self.appID = '' # 软件代号 72 | self.userProductInfo = '' # 产品信息 73 | 74 | # vn格式的委托数据 75 | self.orderDict = {} 76 | self.workingOrderDict = {} 77 | # 持仓细节相关 78 | self.detailDict = {} # vtSymbol:PositionDetail 79 | self.tdPenaltyList = globalSetting['tdPenalty'] # 平今手续费惩罚的产品代码列表 80 | # 载入设置 81 | self.loadSetting() 82 | # 循环查询持仓和账户相关 83 | self.countGet = 0 # 查询延时计数 84 | self.lastGet = 'Position' # 上次查询的性质,先查询账户 85 | # 注册持仓和账户、委托事件 86 | self.ee.register(EVENT_VNORDER, self.processOrderEvent) 87 | self.ee.register(EVENT_VNTRADE, self.processTradeEvent) 88 | self.ee.register(EVENT_VNPOSITION, self.processPositionEvent) 89 | 90 | # 策略已启动 91 | self.ctaActive = False 92 | 93 | # 主窗体 94 | self.mw = MainWindow(mainEngine=self,eventEngine=self.ee) 95 | self.mw.showMaximized() 96 | 97 | #---------------------------------------------------------------------- 98 | def login(self): 99 | """登陆""" 100 | self.md.connect(self.userID, self.password, self.brokerID, self.MdIp) 101 | self.td.connect(self.userID, self.password, self.brokerID, self.TdIp, self.appID, self.authCode, self.userProductInfo) 102 | # ---------------------------------------------------------------------- 103 | def loadSetting(self): 104 | """载入设置""" 105 | # 载入用户信息 106 | d = load_json(USER_FILE) 107 | self.userID = d['userID'] # 账号 108 | self.password = d['password'] # 密码 109 | self.brokerID = d['brokerID'] # 经纪商代码 110 | self.MdIp = d['MdIp'] # 行情服务器地址 111 | self.TdIp = d['TdIp'] # 交易服务器地址 112 | self.authCode = d['authCode'] # 授权码 113 | self.appID = d['appID'] # 软件代号 114 | self.userProductInfo = d['userProductInfo'] # 产品信息 115 | 116 | # ---------------------------------------------------------------------- 117 | def insertInstrument(self, event): 118 | """插入合约对象""" 119 | data = event.dict_['data'] 120 | last = event.dict_['last'] 121 | self.list_instrument.append(data) 122 | 123 | # 创建合约信息实例 124 | contract = CtaContractData() 125 | 126 | 127 | contract.vtSymbol = contract.symbol = data['InstrumentID'] 128 | contract.name = data['InstrumentName'] 129 | contract.exchange = data['ExchangeID'] 130 | 131 | # 合约数值 132 | contract.size = data['VolumeMultiple'] 133 | contract.priceTick = data['PriceTick'] 134 | contract.strikePrice = data['StrikePrice'] 135 | contract.underlyingSymbol = data['UnderlyingInstrID'] 136 | 137 | # 期权类型 138 | if data['OptionsType'] == '1': 139 | contract.optionType = OPTION_CALL 140 | elif data['OptionsType'] == '2': 141 | contract.optionType = OPTION_PUT 142 | 143 | # 推送合约信息 144 | self.onContract(contract) 145 | 146 | if last: # 最后一条数据 147 | # 将查询完成的合约信息保存到本地文件 148 | event = Event(type_=EVENT_LOG) 149 | log = '合约信息查询完成' 150 | event.dict_['log'] = log 151 | self.ee.put(event) 152 | # 保存合约信息到本地 153 | self.saveContracts() 154 | # 字典格式的合约数据也保存一份 155 | instrumentFile = WORKING_DIR + 'temp/InstrumentID.json' 156 | save_json(self.list_instrument, instrumentFile) 157 | # 推送日志 158 | event = Event(type_=EVENT_LOG) 159 | log = '合约信息已经保存到本地' 160 | event.dict_['log'] = log 161 | self.ee.put(event) 162 | # ---------------------------------------------------------------------- 163 | def insertMarketData(self, event): 164 | """插入合约截面数据""" 165 | data = event.dict_['data'] 166 | last = event.dict_['last'] 167 | self.list_marketdata.append(data) 168 | if last: 169 | # 更新交易日 170 | self.md.TradingDay = data['TradingDay'] 171 | # 将查询完成的合约截面数据保存到本地文件 172 | event = Event(type_=EVENT_LOG) 173 | log = '合约截面数据查询完成' 174 | event.dict_['log'] = log 175 | self.ee.put(event) 176 | # 字典格式的合约截面数据也保存一份 177 | makertdataFile = WORKING_DIR + 'temp/Marketdata.json' 178 | save_json(self.list_marketdata, makertdataFile) 179 | # 推送日志 180 | event = Event(type_=EVENT_LOG) 181 | log = '合约截面数据已经保存到本地' 182 | event.dict_['log'] = log 183 | self.ee.put(event) 184 | if not self.ctaActive: 185 | self.runStrategy() 186 | self.ctaActive = True 187 | # ---------------------------------------------------------------------- 188 | def runStrategy(self): 189 | """启动策略""" 190 | # 定时器事件,循环查询 191 | self.ee.register(EVENT_TIMER, self.getAccountPosition) 192 | 193 | # 发送信号到策略界面,自动载入、初始化、启动全部策略 194 | event = Event(EVENT_CTA_ROBOT) 195 | self.ee.put(event) 196 | 197 | #---------------------------------------------------------------------- 198 | def onContract(self, contract): 199 | """合约基础信息推送""" 200 | event = Event(type_=EVENT_CONTRACT) 201 | event.dict_['data'] = contract 202 | self.ee.put(event) 203 | #---------------------------------------------------------------------- 204 | def updateContract(self, event): 205 | """更新合约数据""" 206 | contract = event.dict_['data'] 207 | self.contractDict[contract.vtSymbol] = contract 208 | #---------------------------------------------------------------------- 209 | def getContract(self, vtSymbol): 210 | """查询合约对象""" 211 | try: 212 | return self.contractDict[vtSymbol] 213 | except KeyError: 214 | return None 215 | #---------------------------------------------------------------------- 216 | def saveContracts(self): 217 | """保存所有合约对象到硬盘""" 218 | contractFilePath = WORKING_DIR + 'temp/contracts' 219 | data = {key: value.__dict__ for key, value in self.contractDict.items()} 220 | save_json(data, contractFilePath) 221 | 222 | #---------------------------------------------------------------------- 223 | def loadContracts(self): 224 | """从硬盘读取合约对象""" 225 | 226 | contractFilePath = WORKING_DIR + 'temp/contracts' 227 | contractDict = load_json(contractFilePath) 228 | 229 | for k, v in contractDict.items(): 230 | # 创建合约信息实例 231 | contract = CtaContractData() 232 | contract.vtSymbol = contract.symbol = k 233 | contract.name = v.get('name') 234 | contract.exchange = v.get('exchange') 235 | contract.size = v.get('size') 236 | contract.priceTick = v.get('priceTick') 237 | contract.strikePrice = v.get('strikePrice') 238 | contract.underlyingSymbol = v.get('underlyingSymbol') 239 | contract.optionType = v.get('optionType') 240 | self.contractDict[k] = contract 241 | 242 | #---------------------------------------------------------------------- 243 | def getOrder(self, vtOrderID): 244 | """查询委托""" 245 | try: 246 | return self.orderDict[vtOrderID] 247 | except KeyError: 248 | return None 249 | #---------------------------------------------------------------------- 250 | def processOrderEvent(self, event): 251 | """处理委托事件""" 252 | order = event.dict_['data'] 253 | self.orderDict[order.vtOrderID] = order 254 | 255 | # 如果订单的状态是全部成交或者撤销,则需要从workingOrderDict中移除 256 | if order.status in self.FINISHED_STATUS: 257 | if order.vtOrderID in self.workingOrderDict: 258 | del self.workingOrderDict[order.vtOrderID] 259 | # 否则则更新字典中的数据 260 | else: 261 | self.workingOrderDict[order.vtOrderID] = order 262 | 263 | # 更新到持仓细节中 264 | detail = self.getPositionDetail(order.vtSymbol) 265 | detail.updateOrder(order) 266 | #---------------------------------------------------------------------- 267 | def processTradeEvent(self, event): 268 | """处理成交事件""" 269 | trade = event.dict_['data'] 270 | 271 | # 更新到持仓细节中 272 | detail = self.getPositionDetail(trade.vtSymbol) 273 | detail.updateTrade(trade) 274 | #---------------------------------------------------------------------- 275 | def processPositionEvent(self, event): 276 | """处理持仓事件""" 277 | pos = event.dict_['data'] 278 | last = event.dict_['last'] 279 | 280 | # 更新到持仓细节中 281 | detail = self.getPositionDetail(pos.vtSymbol) 282 | detail.updatePosition(pos) 283 | 284 | #---------------------------------------------------------------------- 285 | def getPositionDetail(self, vtSymbol): 286 | """查询持仓细节""" 287 | if vtSymbol in self.detailDict: 288 | detail = self.detailDict[vtSymbol] 289 | else: 290 | contract = self.getContract(vtSymbol) 291 | detail = PositionDetail(vtSymbol, contract) 292 | self.detailDict[vtSymbol] = detail 293 | 294 | # 设置持仓细节的委托转换模式 295 | contract = self.getContract(vtSymbol) 296 | 297 | if contract: 298 | detail.exchange = contract.exchange 299 | 300 | # 上期所合约 301 | if contract.exchange == EXCHANGE_SHFE: 302 | detail.mode = detail.MODE_SHFE 303 | 304 | # 检查是否有平今惩罚 305 | for productID in self.tdPenaltyList: 306 | if str(productID) in contract.symbol: 307 | detail.mode = detail.MODE_TDPENALTY 308 | 309 | return detail 310 | #---------------------------------------------------------------------- 311 | def convertOrderReq(self, req): 312 | """根据规则转换委托请求""" 313 | detail = self.detailDict.get(req.vtSymbol, None) 314 | if not detail: 315 | return [req] 316 | else: 317 | return detail.convertOrderReq(req) 318 | # ---------------------------------------------------------------------- 319 | def getAccountPosition(self, event): 320 | """循环查询账户和持仓""" 321 | self.countGet += 1 322 | # 每n秒发一次查询 323 | if self.countGet > 5: 324 | self.countGet = 0 # 清空计数 325 | 326 | if self.lastGet == 'Account': 327 | self.qryPosition() 328 | self.lastGet = 'Position' 329 | else: 330 | self.qryAccount() 331 | self.lastGet = 'Account' 332 | # ---------------------------------------------------------------------- 333 | def qryAccount(self): 334 | """查询账户""" 335 | self.td.qryAccount() 336 | # ---------------------------------------------------------------------- 337 | def qryPosition(self): 338 | """查询持仓""" 339 | self.td.qryPosition() 340 | # ---------------------------------------------------------------------- 341 | def sendOrder(self, req): 342 | '''发单''' 343 | return self.td.sendOrder(req) 344 | # ---------------------------------------------------------------------- 345 | def cancelOrder(self, req): 346 | '''撤单''' 347 | self.td.cancelOrder(req) 348 | # ---------------------------------------------------------------------- 349 | def subscribe(self, req): 350 | '''订阅行情''' 351 | self.md.subscribe(req.symbol) 352 | # ---------------------------------------------------------------------- 353 | def exit(self): 354 | """退出程序前调用,保证正常退出""" 355 | # 停止事件引擎 356 | self.ee.stop() 357 | # ---------------------------------------------------------------------- 358 | 359 | ######################################################################## 360 | class PositionDetail(object): 361 | """本地维护的持仓信息""" 362 | WORKING_STATUS = [STATUS_UNKNOWN, STATUS_NOTTRADED, STATUS_PARTTRADED] 363 | 364 | MODE_NORMAL = 'normal' # 普通模式 365 | MODE_SHFE = 'shfe' # 上期所今昨分别平仓 366 | MODE_TDPENALTY = 'tdpenalty' # 平今惩罚 367 | 368 | #---------------------------------------------------------------------- 369 | def __init__(self, vtSymbol, contract=None): 370 | """Constructor""" 371 | self.vtSymbol = vtSymbol 372 | self.symbol = EMPTY_STRING 373 | self.exchange = EMPTY_STRING 374 | self.name = EMPTY_UNICODE 375 | self.size = 1 376 | 377 | if contract: 378 | self.symbol = contract.symbol 379 | self.exchange = contract.exchange 380 | self.name = contract.name 381 | self.size = contract.size 382 | 383 | self.longPos = EMPTY_INT 384 | self.longYd = EMPTY_INT 385 | self.longTd = EMPTY_INT 386 | self.longPosFrozen = EMPTY_INT 387 | self.longYdFrozen = EMPTY_INT 388 | self.longTdFrozen = EMPTY_INT 389 | self.longPnl = EMPTY_FLOAT 390 | self.longPrice = EMPTY_FLOAT 391 | 392 | self.shortPos = EMPTY_INT 393 | self.shortYd = EMPTY_INT 394 | self.shortTd = EMPTY_INT 395 | self.shortPosFrozen = EMPTY_INT 396 | self.shortYdFrozen = EMPTY_INT 397 | self.shortTdFrozen = EMPTY_INT 398 | self.shortPnl = EMPTY_FLOAT 399 | self.shortPrice = EMPTY_FLOAT 400 | 401 | self.lastPrice = EMPTY_FLOAT 402 | 403 | self.mode = self.MODE_NORMAL 404 | self.exchange = EMPTY_STRING 405 | 406 | self.workingOrderDict = {} 407 | 408 | #---------------------------------------------------------------------- 409 | def updateTrade(self, trade): 410 | """成交更新""" 411 | # 多头 412 | if trade.direction is DIRECTION_LONG: 413 | # 开仓 414 | if trade.offset is OFFSET_OPEN: 415 | self.longTd += trade.volume 416 | # 平今 417 | elif trade.offset is OFFSET_CLOSETODAY: 418 | self.shortTd -= trade.volume 419 | # 平昨 420 | elif trade.offset is OFFSET_CLOSEYESTERDAY: 421 | self.shortYd -= trade.volume 422 | # 平仓 423 | elif trade.offset is OFFSET_CLOSE: 424 | # 上期所等同于平昨 425 | if self.exchange is EXCHANGE_SHFE: 426 | self.shortYd -= trade.volume 427 | # 非上期所,优先平今(原注释) 428 | # 应该是优先平昨,否则可能导致平今惩罚模式计算错误(存疑) 429 | else: 430 | # self.shortTd -= trade.volume 431 | 432 | # if self.shortTd < 0: 433 | # self.shortYd += self.shortTd 434 | # self.shortTd = 0 435 | self.shortYd -= trade.volume 436 | 437 | if self.shortYd < 0: 438 | self.shortTd += self.shortYd 439 | self.shortYd = 0 440 | # 空头 441 | elif trade.direction is DIRECTION_SHORT: 442 | # 开仓 443 | if trade.offset is OFFSET_OPEN: 444 | self.shortTd += trade.volume 445 | # 平今 446 | elif trade.offset is OFFSET_CLOSETODAY: 447 | self.longTd -= trade.volume 448 | # 平昨 449 | elif trade.offset is OFFSET_CLOSEYESTERDAY: 450 | self.longYd -= trade.volume 451 | # 平仓 452 | elif trade.offset is OFFSET_CLOSE: 453 | # 上期所等同于平昨 454 | if self.exchange is EXCHANGE_SHFE: 455 | self.longYd -= trade.volume 456 | # 非上期所,优先平今(原注释) 457 | # 应该是优先平昨,否则可能导致平今惩罚模式计算错误(存疑) 458 | else: 459 | # self.longTd -= trade.volume 460 | 461 | # if self.longTd < 0: 462 | # self.longYd += self.longTd 463 | # self.longTd = 0 464 | self.longYd -= trade.volume 465 | 466 | if self.longYd < 0: 467 | self.longTd += self.longYd 468 | self.longYd = 0 469 | 470 | # 汇总 471 | self.calculatePrice(trade) 472 | self.calculatePosition() 473 | self.calculatePnl() 474 | 475 | #---------------------------------------------------------------------- 476 | def updateOrder(self, order): 477 | """委托更新""" 478 | # 将活动委托缓存下来 479 | if order.status in self.WORKING_STATUS: 480 | self.workingOrderDict[order.vtOrderID] = order 481 | 482 | # 移除缓存中已经完成的委托 483 | else: 484 | if order.vtOrderID in self.workingOrderDict: 485 | del self.workingOrderDict[order.vtOrderID] 486 | 487 | # 计算冻结 488 | self.calculateFrozen() 489 | 490 | #---------------------------------------------------------------------- 491 | def updatePosition(self, pos): 492 | """持仓更新""" 493 | if pos.direction is DIRECTION_LONG: 494 | self.longPos = pos.position 495 | self.longYd = pos.ydPosition 496 | self.longTd = self.longPos - self.longYd 497 | self.longPnl = pos.positionProfit 498 | self.longPrice = pos.price 499 | elif pos.direction is DIRECTION_SHORT: 500 | self.shortPos = pos.position 501 | self.shortYd = pos.ydPosition 502 | self.shortTd = self.shortPos - self.shortYd 503 | self.shortPnl = pos.positionProfit 504 | self.shortPrice = pos.price 505 | 506 | # self.output() 507 | 508 | #---------------------------------------------------------------------- 509 | def updateOrderReq(self, req, vtOrderID): 510 | """发单更新""" 511 | vtSymbol = req.vtSymbol 512 | 513 | # 基于请求生成委托对象 514 | order = VtOrderData() 515 | order.vtSymbol = vtSymbol 516 | order.symbol = req.symbol 517 | order.exchange = req.exchange 518 | order.offset = req.offset 519 | order.direction = req.direction 520 | order.totalVolume = req.volume 521 | order.status = STATUS_UNKNOWN 522 | 523 | # 缓存到字典中 524 | self.workingOrderDict[vtOrderID] = order 525 | 526 | # 计算冻结量 527 | self.calculateFrozen() 528 | 529 | #---------------------------------------------------------------------- 530 | def updateTick(self, tick): 531 | """行情更新""" 532 | self.lastPrice = tick.lastPrice 533 | self.calculatePnl() 534 | 535 | #---------------------------------------------------------------------- 536 | def calculatePnl(self): 537 | """计算持仓盈亏""" 538 | self.longPnl = self.longPos * (self.lastPrice - self.longPrice) * self.size 539 | self.shortPnl = self.shortPos * (self.shortPrice - self.lastPrice) * self.size 540 | 541 | #---------------------------------------------------------------------- 542 | def calculatePrice(self, trade): 543 | """计算持仓均价(基于成交数据)""" 544 | # 只有开仓会影响持仓均价 545 | if trade.offset == OFFSET_OPEN: 546 | if trade.direction == DIRECTION_LONG: 547 | cost = self.longPrice * self.longPos 548 | cost += trade.volume * trade.price 549 | newPos = self.longPos + trade.volume 550 | if newPos: 551 | self.longPrice = cost / newPos 552 | else: 553 | self.longPrice = 0 554 | else: 555 | cost = self.shortPrice * self.shortPos 556 | cost += trade.volume * trade.price 557 | newPos = self.shortPos + trade.volume 558 | if newPos: 559 | self.shortPrice = cost / newPos 560 | else: 561 | self.shortPrice = 0 562 | 563 | #---------------------------------------------------------------------- 564 | def calculatePosition(self): 565 | """计算持仓情况""" 566 | self.longPos = self.longTd + self.longYd 567 | self.shortPos = self.shortTd + self.shortYd 568 | 569 | #---------------------------------------------------------------------- 570 | def calculateFrozen(self): 571 | """计算冻结情况""" 572 | # 清空冻结数据 573 | self.longPosFrozen = EMPTY_INT 574 | self.longYdFrozen = EMPTY_INT 575 | self.longTdFrozen = EMPTY_INT 576 | self.shortPosFrozen = EMPTY_INT 577 | self.shortYdFrozen = EMPTY_INT 578 | self.shortTdFrozen = EMPTY_INT 579 | 580 | # 遍历统计 581 | for order in self.workingOrderDict.values(): 582 | # 计算剩余冻结量 583 | frozenVolume = order.totalVolume - order.tradedVolume 584 | 585 | # 多头委托 586 | if order.direction is DIRECTION_LONG: 587 | # 平今 588 | if order.offset is OFFSET_CLOSETODAY: 589 | self.shortTdFrozen += frozenVolume 590 | # 平昨 591 | elif order.offset is OFFSET_CLOSEYESTERDAY: 592 | self.shortYdFrozen += frozenVolume 593 | # 平仓 594 | elif order.offset is OFFSET_CLOSE: 595 | self.shortTdFrozen += frozenVolume 596 | 597 | if self.shortTdFrozen > self.shortTd: 598 | self.shortYdFrozen += (self.shortTdFrozen - self.shortTd) 599 | self.shortTdFrozen = self.shortTd 600 | # 空头委托 601 | elif order.direction is DIRECTION_SHORT: 602 | # 平今 603 | if order.offset is OFFSET_CLOSETODAY: 604 | self.longTdFrozen += frozenVolume 605 | # 平昨 606 | elif order.offset is OFFSET_CLOSEYESTERDAY: 607 | self.longYdFrozen += frozenVolume 608 | # 平仓 609 | elif order.offset is OFFSET_CLOSE: 610 | self.longTdFrozen += frozenVolume 611 | 612 | if self.longTdFrozen > self.longTd: 613 | self.longYdFrozen += (self.longTdFrozen - self.longTd) 614 | self.longTdFrozen = self.longTd 615 | 616 | # 汇总今昨冻结 617 | self.longPosFrozen = self.longYdFrozen + self.longTdFrozen 618 | self.shortPosFrozen = self.shortYdFrozen + self.shortTdFrozen 619 | 620 | #---------------------------------------------------------------------- 621 | def output(self): 622 | """""" 623 | print(self.vtSymbol, '-'*30) 624 | print('long, total:%s, td:%s, yd:%s' %(self.longPos, self.longTd, self.longYd)) 625 | print('long frozen, total:%s, td:%s, yd:%s' %(self.longPosFrozen, self.longTdFrozen, self.longYdFrozen)) 626 | print('short, total:%s, td:%s, yd:%s' %(self.shortPos, self.shortTd, self.shortYd)) 627 | print('short frozen, total:%s, td:%s, yd:%s' %(self.shortPosFrozen, self.shortTdFrozen, self.shortYdFrozen)) 628 | 629 | #---------------------------------------------------------------------- 630 | def convertOrderReq(self, req): 631 | """转换委托请求""" 632 | # 普通模式无需转换 633 | if self.mode is self.MODE_NORMAL: 634 | return [req] 635 | 636 | # 上期所模式拆分今昨,优先平今 637 | elif self.mode is self.MODE_SHFE: 638 | # 开仓无需转换 639 | if req.offset is OFFSET_OPEN: 640 | return [req] 641 | 642 | # 多头 643 | if req.direction is DIRECTION_LONG: 644 | posAvailable = self.shortPos - self.shortPosFrozen 645 | tdAvailable = self.shortTd- self.shortTdFrozen 646 | ydAvailable = self.shortYd - self.shortYdFrozen 647 | # 空头 648 | else: 649 | posAvailable = self.longPos - self.longPosFrozen 650 | tdAvailable = self.longTd - self.longTdFrozen 651 | ydAvailable = self.longYd - self.longYdFrozen 652 | 653 | # 平仓量超过总可用,拒绝,返回空列表 654 | if req.volume > posAvailable: 655 | return [] 656 | # 平仓量小于今可用,全部平今 657 | elif req.volume <= tdAvailable: 658 | req.offset = OFFSET_CLOSETODAY 659 | return [req] 660 | # 平仓量大于今可用,平今再平昨 661 | else: 662 | l = [] 663 | 664 | if tdAvailable > 0: 665 | reqTd = copy(req) 666 | reqTd.offset = OFFSET_CLOSETODAY 667 | reqTd.volume = tdAvailable 668 | l.append(reqTd) 669 | 670 | reqYd = copy(req) 671 | reqYd.offset = OFFSET_CLOSEYESTERDAY 672 | reqYd.volume = req.volume - tdAvailable 673 | l.append(reqYd) 674 | 675 | return l 676 | 677 | # 平今惩罚模式,没有今仓则平昨,否则锁仓 678 | elif self.mode is self.MODE_TDPENALTY: 679 | # 多头 680 | if req.direction is DIRECTION_LONG: 681 | td = self.shortTd 682 | ydAvailable = self.shortYd - self.shortYdFrozen 683 | # 空头 684 | else: 685 | td = self.longTd 686 | ydAvailable = self.longYd - self.longYdFrozen 687 | 688 | # 这里针对开仓和平仓委托均使用一套逻辑 689 | 690 | # 如果有今仓,则只能开仓(或锁仓) 691 | if td: 692 | req.offset = OFFSET_OPEN 693 | return [req] 694 | # 如果平仓量小于昨可用,全部平昨 695 | elif req.volume <= ydAvailable: 696 | if self.exchange is EXCHANGE_SHFE: 697 | req.offset = OFFSET_CLOSEYESTERDAY 698 | else: 699 | req.offset = OFFSET_CLOSE 700 | return [req] 701 | # 平仓量大于昨可用,平仓再反向开仓 702 | else: 703 | l = [] 704 | 705 | if ydAvailable > 0: 706 | reqClose = copy(req) 707 | if self.exchange is EXCHANGE_SHFE: 708 | req.offset = OFFSET_CLOSEYESTERDAY 709 | else: 710 | req.offset = OFFSET_CLOSE 711 | reqClose.volume = ydAvailable 712 | 713 | l.append(reqClose) 714 | 715 | reqOpen = copy(req) 716 | reqOpen.offset = OFFSET_OPEN 717 | reqOpen.volume = req.volume - ydAvailable 718 | l.append(reqOpen) 719 | 720 | return l 721 | 722 | # 其他情况则直接返回空 723 | return [] 724 | 725 | 726 | # 直接运行脚本可以进行测试 727 | if __name__ == '__main__': 728 | # 显示自定义图标 729 | import ctypes 730 | ctypes.windll.shell32.SetCurrentProcessExplicitAppUserModelID("myappid") 731 | import sys 732 | app = QApplication(sys.argv) 733 | try: 734 | import qdarkstyle 735 | app.setStyleSheet(qdarkstyle.load_stylesheet_pyqt5()) # 黑色主题 736 | app.setFont(QFont("Microsoft YaHei", 11)) # 微软雅黑字体 737 | except: 738 | pass 739 | 740 | main = MainEngine() 741 | main.login() 742 | app.exec_() 743 | -------------------------------------------------------------------------------- /py3_ctp_demo_on_vnpy/log/EventLog/deleteme: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vvipi/py3_demo_on_vnpy_ctp/491f6394ce049a7dd93a34415cd2ffe499bf9ffa/py3_ctp_demo_on_vnpy/log/EventLog/deleteme -------------------------------------------------------------------------------- /py3_ctp_demo_on_vnpy/log/deleteme: -------------------------------------------------------------------------------- 1 | # -------------------------------------------------------------------------------- /py3_ctp_demo_on_vnpy/modules/RM_setting.json: -------------------------------------------------------------------------------- 1 | { 2 | "active": true, 3 | "orderFlowLimit": 20, 4 | "orderFlowClear": 1, 5 | "orderSizeLimit": 10, 6 | "tradeLimit": 40 7 | } -------------------------------------------------------------------------------- /py3_ctp_demo_on_vnpy/modules/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | __title__ = '' 4 | __author__ = 'vvipi' 5 | __mtime__ = '2017/9/17' 6 | """ 7 | -------------------------------------------------------------------------------- /py3_ctp_demo_on_vnpy/modules/baseSetting.py: -------------------------------------------------------------------------------- 1 | # encoding: UTF-8 2 | ''' 3 | 各种常量 4 | ''' 5 | 6 | WORKING_DIR = 'D:/git_ctp_demo/py3_ctp_demo_on_vnpy/' # 工作目录 7 | 8 | USER_FILE = 'setting/user.json' # 账号配置文件 9 | 10 | 11 | 12 | globalSetting = { 13 | 'tdPenalty': ['jm','j',"IF", "IH", "IC"], # 平今惩罚 14 | 15 | } 16 | -------------------------------------------------------------------------------- /py3_ctp_demo_on_vnpy/modules/ctaEngine.py: -------------------------------------------------------------------------------- 1 | # encoding: UTF-8 2 | 3 | ''' 4 | 本文件中实现了CTA策略引擎,针对CTA类型的策略,抽象简化了部分底层接口的功能。 5 | 6 | ''' 7 | import os 8 | import traceback 9 | from collections import OrderedDict 10 | from datetime import datetime, timedelta 11 | from copy import copy 12 | 13 | from modules.eventEngine import Event 14 | from modules.eventType import * 15 | from modules.objects import * 16 | from modules.functions import load_json, save_json 17 | from modules.baseSetting import WORKING_DIR 18 | from strategy import STRATEGY_CLASS 19 | 20 | 21 | ######################################################################## 22 | class CtaEngine(object): 23 | """CTA策略引擎""" 24 | settingFileName = 'CTA_setting.json' 25 | # settingfilePath = getJsonPath(settingFileName, __file__) 26 | settingfilePath = WORKING_DIR + 'setting/CTA_setting.json' 27 | 28 | STATUS_FINISHED = set([STATUS_REJECTED, STATUS_CANCELLED, STATUS_ALLTRADED]) 29 | 30 | #---------------------------------------------------------------------- 31 | def __init__(self, mainEngine, eventEngine): 32 | """Constructor""" 33 | self.mainEngine = mainEngine 34 | self.eventEngine = eventEngine 35 | 36 | # 当前日期 37 | self.today = datetime.now().replace(hour=0, minute=0, second=0, microsecond=0) 38 | 39 | # 保存策略实例的字典 40 | # key为策略名称,value为策略实例,注意策略名称不允许重复 41 | self.strategyDict = {} 42 | 43 | # 保存vtSymbol和策略实例映射的字典(用于推送tick数据) 44 | # 由于可能多个strategy交易同一个vtSymbol,因此key为vtSymbol 45 | # value为包含所有相关strategy对象的list 46 | self.tickStrategyDict = {} 47 | 48 | # 保存vtOrderID和strategy对象映射的字典(用于推送order和trade数据) 49 | # key为vtOrderID,value为strategy对象 50 | self.orderStrategyDict = {} 51 | 52 | # 本地停止单编号计数 53 | self.stopOrderCount = 0 54 | # stopOrderID = STOPORDERPREFIX + str(stopOrderCount) 55 | 56 | # 本地停止单字典 57 | # key为stopOrderID,value为stopOrder对象 58 | self.stopOrderDict = {} # 停止单撤销后不会从本字典中删除 59 | self.workingStopOrderDict = {} # 停止单撤销后会从本字典中删除 60 | 61 | # 保存策略名称和委托号列表的字典 62 | # key为name,value为保存orderID(限价+本地停止)的集合 63 | self.strategyOrderDict = {} 64 | 65 | # 成交号集合,用来过滤已经收到过的成交推送 66 | self.tradeSet = set() 67 | 68 | # 引擎类型为实盘 69 | self.engineType = ENGINETYPE_TRADING 70 | 71 | # 注册日志事件类型 72 | # self.mainEngine.registerLogEvent(EVENT_CTA_LOG) 73 | 74 | # 注册事件监听 75 | self.registerEvent() 76 | 77 | #---------------------------------------------------------------------- 78 | def sendOrder(self, vtSymbol, orderType, price, volume, strategy): 79 | """发单""" 80 | contract = self.mainEngine.getContract(vtSymbol) 81 | 82 | req = CtaOrderReq() 83 | req.symbol = contract.symbol 84 | req.exchange = contract.exchange 85 | req.vtSymbol = contract.vtSymbol 86 | req.price = self.roundToPriceTick(contract.priceTick, price) 87 | req.volume = volume 88 | 89 | # req.productClass = strategy.productClass 90 | # req.currency = strategy.currency 91 | 92 | # 设计为CTA引擎发出的委托只允许使用限价单 93 | req.priceType = PRICETYPE_LIMITPRICE 94 | 95 | # CTA委托类型映射 96 | if orderType == CTAORDER_BUY: 97 | req.direction = DIRECTION_LONG 98 | req.offset = OFFSET_OPEN 99 | 100 | elif orderType == CTAORDER_SELL: 101 | req.direction = DIRECTION_SHORT 102 | req.offset = OFFSET_CLOSE 103 | 104 | elif orderType == CTAORDER_SHORT: 105 | req.direction = DIRECTION_SHORT 106 | req.offset = OFFSET_OPEN 107 | 108 | elif orderType == CTAORDER_COVER: 109 | req.direction = DIRECTION_LONG 110 | req.offset = OFFSET_CLOSE 111 | 112 | # 委托转换 113 | reqList = self.mainEngine.convertOrderReq(req) 114 | vtOrderIDList = [] 115 | 116 | if not reqList: 117 | return vtOrderIDList 118 | 119 | for convertedReq in reqList: 120 | # vtOrderID = self.mainEngine.sendOrder(convertedReq, contract.gatewayName) # 发单 121 | vtOrderID = self.mainEngine.sendOrder(convertedReq) # 发单,只有ctpgateway,不传gatewayname 122 | self.orderStrategyDict[vtOrderID] = strategy # 保存vtOrderID和策略的映射关系 123 | self.strategyOrderDict[strategy.name].add(vtOrderID) # 添加到策略委托号集合中 124 | vtOrderIDList.append(vtOrderID) 125 | 126 | self.writeCtaLog(u'策略%s发送委托,%s,%s,%s@%s' 127 | %(strategy.name, vtSymbol, req.direction, volume, price)) 128 | 129 | return vtOrderIDList 130 | 131 | #---------------------------------------------------------------------- 132 | def cancelOrder(self, vtOrderID): 133 | """撤单""" 134 | # 查询报单对象 135 | order = self.mainEngine.getOrder(vtOrderID) 136 | 137 | # 如果查询成功 138 | if order: 139 | # 检查是否报单还有效,只有有效时才发出撤单指令 140 | orderFinished = (order.status==STATUS_ALLTRADED or order.status==STATUS_CANCELLED) 141 | if not orderFinished: 142 | req = CtaCancelOrderReq() 143 | req.symbol = order.symbol 144 | req.exchange = order.exchange 145 | req.frontID = order.frontID 146 | req.sessionID = order.sessionID 147 | req.orderID = order.orderID 148 | req.OrderSysID = order.OrderSysID 149 | # self.mainEngine.cancelOrder(req, order.gatewayName) 150 | self.mainEngine.cancelOrder(req) 151 | 152 | #---------------------------------------------------------------------- 153 | def sendStopOrder(self, vtSymbol, orderType, price, volume, strategy): 154 | """发停止单(本地实现)""" 155 | self.stopOrderCount += 1 156 | stopOrderID = STOPORDERPREFIX + str(self.stopOrderCount) 157 | 158 | so = StopOrder() 159 | so.vtSymbol = vtSymbol 160 | so.orderType = orderType 161 | so.price = price 162 | so.volume = volume 163 | so.strategy = strategy 164 | so.stopOrderID = stopOrderID 165 | so.status = STOPORDER_WAITING 166 | 167 | if orderType == CTAORDER_BUY: 168 | so.direction = DIRECTION_LONG 169 | so.offset = OFFSET_OPEN 170 | elif orderType == CTAORDER_SELL: 171 | so.direction = DIRECTION_SHORT 172 | so.offset = OFFSET_CLOSE 173 | elif orderType == CTAORDER_SHORT: 174 | so.direction = DIRECTION_SHORT 175 | so.offset = OFFSET_OPEN 176 | elif orderType == CTAORDER_COVER: 177 | so.direction = DIRECTION_LONG 178 | so.offset = OFFSET_CLOSE 179 | 180 | # 保存stopOrder对象到字典中 181 | self.stopOrderDict[stopOrderID] = so 182 | self.workingStopOrderDict[stopOrderID] = so 183 | 184 | # 保存stopOrderID到策略委托号集合中 185 | self.strategyOrderDict[strategy.name].add(stopOrderID) 186 | 187 | # 推送停止单状态 188 | strategy.onStopOrder(so) 189 | 190 | return [stopOrderID] 191 | 192 | #---------------------------------------------------------------------- 193 | def cancelStopOrder(self, stopOrderID): 194 | """撤销停止单""" 195 | # 检查停止单是否存在 196 | if stopOrderID in self.workingStopOrderDict: 197 | so = self.workingStopOrderDict[stopOrderID] 198 | strategy = so.strategy 199 | 200 | # 更改停止单状态为已撤销 201 | so.status = STOPORDER_CANCELLED 202 | 203 | # 从活动停止单字典中移除 204 | del self.workingStopOrderDict[stopOrderID] 205 | 206 | # 从策略委托号集合中移除 207 | s = self.strategyOrderDict[strategy.name] 208 | if stopOrderID in s: 209 | s.remove(stopOrderID) 210 | 211 | # 通知策略 212 | strategy.onStopOrder(so) 213 | 214 | #---------------------------------------------------------------------- 215 | def processStopOrder(self, tick): 216 | """收到行情后处理本地停止单(检查是否要立即发出)""" 217 | vtSymbol = tick.vtSymbol 218 | 219 | # 首先检查是否有策略交易该合约 220 | if vtSymbol in self.tickStrategyDict: 221 | # 遍历等待中的停止单,检查是否会被触发 222 | for so in self.workingStopOrderDict.values(): 223 | if so.vtSymbol == vtSymbol: 224 | longTriggered = so.direction==DIRECTION_LONG and tick.lastPrice>=so.price # 多头停止单被触发 225 | shortTriggered = so.direction==DIRECTION_SHORT and tick.lastPrice<=so.price # 空头停止单被触发 226 | 227 | if longTriggered or shortTriggered: 228 | # 买入和卖出分别以涨停跌停价发单(模拟市价单) 229 | if so.direction==DIRECTION_LONG: 230 | price = tick.upperLimit 231 | else: 232 | price = tick.lowerLimit 233 | 234 | # 发出市价委托 235 | self.sendOrder(so.vtSymbol, so.orderType, price, so.volume, so.strategy) 236 | 237 | # 从活动停止单字典中移除该停止单 238 | del self.workingStopOrderDict[so.stopOrderID] 239 | 240 | # 从策略委托号集合中移除 241 | s = self.strategyOrderDict[so.strategy.name] 242 | if so.stopOrderID in s: 243 | s.remove(so.stopOrderID) 244 | 245 | # 更新停止单状态,并通知策略 246 | so.status = STOPORDER_TRIGGERED 247 | so.strategy.onStopOrder(so) 248 | 249 | #---------------------------------------------------------------------- 250 | def processTickEvent(self, event): 251 | """处理行情推送""" 252 | tick = event.dict_['data'] 253 | # 收到tick行情后,先处理本地停止单(检查是否要立即发出) 254 | self.processStopOrder(tick) 255 | 256 | # 推送tick到对应的策略实例进行处理 257 | if tick.vtSymbol in self.tickStrategyDict: 258 | # tick时间可能出现异常数据,使用try...except实现捕捉和过滤 259 | try: 260 | # 添加datetime字段 261 | if not tick.datetime: 262 | tick.datetime = datetime.strptime(' '.join([tick.date, tick.time]), '%Y%m%d %H:%M:%S.%f') 263 | except ValueError: 264 | self.writeCtaLog(traceback.format_exc()) 265 | return 266 | 267 | # 逐个推送到策略实例中 268 | l = self.tickStrategyDict[tick.vtSymbol] 269 | for strategy in l: 270 | self.callStrategyFunc(strategy, strategy.onTick, tick) 271 | 272 | #---------------------------------------------------------------------- 273 | def processOrderEvent(self, event): 274 | """处理委托推送""" 275 | order = event.dict_['data'] 276 | 277 | vtOrderID = order.vtOrderID 278 | 279 | if vtOrderID in self.orderStrategyDict: 280 | strategy = self.orderStrategyDict[vtOrderID] 281 | 282 | # 如果委托已经完成(拒单、撤销、全成),则从活动委托集合中移除 283 | if order.status in self.STATUS_FINISHED: 284 | s = self.strategyOrderDict[strategy.name] 285 | if vtOrderID in s: 286 | s.remove(vtOrderID) 287 | 288 | self.callStrategyFunc(strategy, strategy.onOrder, order) 289 | 290 | #---------------------------------------------------------------------- 291 | def processTradeEvent(self, event): 292 | """处理成交推送""" 293 | trade = event.dict_['data'] 294 | 295 | # 过滤已经收到过的成交回报 296 | if trade.vtOrderID in self.tradeSet: 297 | return 298 | self.tradeSet.add(trade.vtOrderID) 299 | 300 | # 将成交推送到策略对象中 301 | if trade.vtOrderID in self.orderStrategyDict: 302 | strategy = self.orderStrategyDict[trade.vtOrderID] 303 | 304 | # 计算策略持仓 305 | if trade.direction == DIRECTION_LONG: 306 | strategy.pos += trade.volume 307 | else: 308 | strategy.pos -= trade.volume 309 | 310 | self.callStrategyFunc(strategy, strategy.onTrade, trade) 311 | 312 | # 保存策略持仓到数据库 313 | self.saveSyncData(strategy) 314 | 315 | #---------------------------------------------------------------------- 316 | def registerEvent(self): 317 | """注册事件监听""" 318 | self.eventEngine.register(EVENT_TICK, self.processTickEvent) 319 | self.eventEngine.register(EVENT_VNORDER, self.processOrderEvent) 320 | self.eventEngine.register(EVENT_VNTRADE, self.processTradeEvent) 321 | 322 | #---------------------------------------------------------------------- 323 | def insertData(self, dbName, collectionName, data): 324 | """插入数据到数据库(这里的data可以是VtTickData或者VtBarData)""" 325 | self.mainEngine.dbInsert(dbName, collectionName, data.__dict__) 326 | 327 | #---------------------------------------------------------------------- 328 | def loadBar(self, dbName, collectionName, days): 329 | """从数据库中读取Bar数据,startDate是datetime对象""" 330 | pass 331 | # 有需要的话可以自行修改成从csv文件或json文件读取历史数据 332 | 333 | # startDate = self.today - timedelta(days) 334 | 335 | # d = {'datetime':{'$gte':startDate}} 336 | # barData = self.mainEngine.dbQuery(dbName, collectionName, d, 'datetime') 337 | 338 | # l = [] 339 | # for d in barData: 340 | # bar = VtBarData() 341 | # bar.__dict__ = d 342 | # l.append(bar) 343 | # return l 344 | 345 | #---------------------------------------------------------------------- 346 | def loadTick(self, dbName, collectionName, days): 347 | """从数据库中读取Tick数据,startDate是datetime对象""" 348 | pass 349 | # 有需要的话可以自行修改成从csv文件或json文件读取历史数据 350 | 351 | # startDate = self.today - timedelta(days) 352 | 353 | # d = {'datetime':{'$gte':startDate}} 354 | # tickData = self.mainEngine.dbQuery(dbName, collectionName, d, 'datetime') 355 | 356 | # l = [] 357 | # for d in tickData: 358 | # tick = VtTickData() 359 | # tick.__dict__ = d 360 | # l.append(tick) 361 | # return l 362 | 363 | #---------------------------------------------------------------------- 364 | def writeCtaLog(self, content): 365 | """发出CTA模块日志事件""" 366 | 367 | event = Event(type_=EVENT_LOG) 368 | event.dict_['log'] = content 369 | self.eventEngine.put(event) 370 | 371 | #---------------------------------------------------------------------- 372 | def loadStrategy(self, setting): 373 | """载入策略""" 374 | try: 375 | name = setting['name'] 376 | className = setting['className'] 377 | except Exception: 378 | msg = traceback.format_exc() 379 | self.writeCtaLog(u'载入策略出错:%s' %msg) 380 | return 381 | 382 | # 获取策略类 383 | strategyClass = STRATEGY_CLASS.get(className, None) 384 | if not strategyClass: 385 | self.writeCtaLog(u'找不到策略类:%s' %className) 386 | return 387 | 388 | # 防止策略重名 389 | if name in self.strategyDict: 390 | self.writeCtaLog(u'策略实例重名:%s' %name) 391 | else: 392 | # 创建策略实例 393 | strategy = strategyClass(self, setting) 394 | self.strategyDict[name] = strategy 395 | 396 | # 创建委托号列表 397 | self.strategyOrderDict[name] = set() 398 | 399 | # 保存Tick映射关系 400 | if strategy.vtSymbol in self.tickStrategyDict: 401 | l = self.tickStrategyDict[strategy.vtSymbol] 402 | else: 403 | l = [] 404 | self.tickStrategyDict[strategy.vtSymbol] = l 405 | l.append(strategy) 406 | 407 | #---------------------------------------------------------------------- 408 | def subscribeMarketData(self, strategy): 409 | """订阅行情""" 410 | # 订阅合约 411 | contract = self.mainEngine.getContract(strategy.vtSymbol) 412 | if contract: 413 | req = CtaSubscribeReq() 414 | req.symbol = contract.symbol 415 | req.exchange = contract.exchange 416 | 417 | # 对于IB接口订阅行情时所需的货币和产品类型,从策略属性中获取 418 | # req.currency = strategy.currency 419 | # req.productClass = strategy.productClass 420 | 421 | # self.mainEngine.subscribe(req, contract.gatewayName) 422 | self.mainEngine.subscribe(req) 423 | else: 424 | self.writeCtaLog(u'%s的交易合约%s无法找到' %(strategy.name, strategy.vtSymbol)) 425 | 426 | #---------------------------------------------------------------------- 427 | def initStrategy(self, name): 428 | """初始化策略""" 429 | if name in self.strategyDict: 430 | strategy = self.strategyDict[name] 431 | 432 | if not strategy.inited: 433 | strategy.inited = True 434 | self.callStrategyFunc(strategy, strategy.onInit) 435 | self.loadSyncData(strategy) # 初始化完成后加载同步数据 436 | self.subscribeMarketData(strategy) # 加载同步数据后再订阅行情 437 | else: 438 | self.writeCtaLog(u'请勿重复初始化策略实例:%s' %name) 439 | else: 440 | self.writeCtaLog(u'策略实例不存在:%s' %name) 441 | 442 | #--------------------------------------------------------------------- 443 | def startStrategy(self, name): 444 | """启动策略""" 445 | if name in self.strategyDict: 446 | strategy = self.strategyDict[name] 447 | 448 | if strategy.inited and not strategy.trading: 449 | strategy.trading = True 450 | self.callStrategyFunc(strategy, strategy.onStart) 451 | else: 452 | self.writeCtaLog(u'策略实例不存在:%s' %name) 453 | 454 | #---------------------------------------------------------------------- 455 | def stopStrategy(self, name): 456 | """停止策略""" 457 | if name in self.strategyDict: 458 | strategy = self.strategyDict[name] 459 | 460 | if strategy.trading: 461 | strategy.trading = False 462 | self.callStrategyFunc(strategy, strategy.onStop) 463 | 464 | # 对该策略发出的所有限价单进行撤单 465 | for vtOrderID, s in self.orderStrategyDict.items(): 466 | if s is strategy: 467 | self.cancelOrder(vtOrderID) 468 | 469 | # 对该策略发出的所有本地停止单撤单 470 | for stopOrderID, so in self.workingStopOrderDict.items(): 471 | if so.strategy is strategy: 472 | self.cancelStopOrder(stopOrderID) 473 | else: 474 | self.writeCtaLog(u'策略实例不存在:%s' %name) 475 | 476 | #---------------------------------------------------------------------- 477 | def initAll(self): 478 | """全部初始化""" 479 | for name in self.strategyDict.keys(): 480 | self.initStrategy(name) 481 | 482 | #---------------------------------------------------------------------- 483 | def startAll(self): 484 | """全部启动""" 485 | for name in self.strategyDict.keys(): 486 | self.startStrategy(name) 487 | 488 | #---------------------------------------------------------------------- 489 | def stopAll(self): 490 | """全部停止""" 491 | for name in self.strategyDict.keys(): 492 | self.stopStrategy(name) 493 | 494 | #---------------------------------------------------------------------- 495 | def saveSetting(self): 496 | """保存策略配置""" 497 | l = [] 498 | for strategy in self.strategyDict.values(): 499 | setting = {} 500 | for param in strategy.paramList: 501 | setting[param] = strategy.__getattribute__(param) 502 | l.append(setting) 503 | 504 | save_json(l, self.settingfilePath) 505 | #---------------------------------------------------------------------- 506 | def loadSetting(self): 507 | """读取策略配置""" 508 | l = load_json(self.settingfilePath) 509 | for setting in l: 510 | self.loadStrategy(setting) 511 | 512 | #---------------------------------------------------------------------- 513 | def getStrategyVar(self, name): 514 | """获取策略当前的变量字典""" 515 | if name in self.strategyDict: 516 | strategy = self.strategyDict[name] 517 | varDict = OrderedDict() 518 | 519 | for key in strategy.varList: 520 | varDict[key] = strategy.__getattribute__(key) 521 | 522 | return varDict 523 | else: 524 | self.writeCtaLog(u'策略实例不存在:' + name) 525 | return None 526 | 527 | #---------------------------------------------------------------------- 528 | def getStrategyParam(self, name): 529 | """获取策略的参数字典""" 530 | if name in self.strategyDict: 531 | strategy = self.strategyDict[name] 532 | paramDict = OrderedDict() 533 | 534 | for key in strategy.paramList: 535 | paramDict[key] = strategy.__getattribute__(key) 536 | 537 | return paramDict 538 | else: 539 | self.writeCtaLog(u'策略实例不存在:' + name) 540 | return None 541 | 542 | #---------------------------------------------------------------------- 543 | def putStrategyEvent(self, name): 544 | """触发策略状态变化事件(通常用于通知GUI更新)""" 545 | event = Event(EVENT_CTA_STRATEGY+name) 546 | self.eventEngine.put(event) 547 | 548 | #---------------------------------------------------------------------- 549 | def callStrategyFunc(self, strategy, func, params=None): 550 | """调用策略的函数,若触发异常则捕捉""" 551 | try: 552 | if params: 553 | func(params) 554 | else: 555 | func() 556 | except Exception: 557 | # 停止策略,修改状态为未初始化 558 | strategy.trading = False 559 | strategy.inited = False 560 | 561 | # 发出日志 562 | content = '\n'.join([u'策略%s触发异常已停止' %strategy.name, 563 | traceback.format_exc()]) 564 | self.writeCtaLog(content) 565 | 566 | #---------------------------------------------------------------------- 567 | def saveSyncData(self, strategy): 568 | """保存策略的持仓情况到本地""" 569 | 570 | flt = {'name': strategy.name, 571 | 'vtSymbol': strategy.vtSymbol} 572 | 573 | d = copy(flt) 574 | for key in strategy.syncList: 575 | d[key] = strategy.__getattribute__(key) 576 | 577 | syncFile = WORKING_DIR + 'setting/syncdata/' + strategy.name + '-' + strategy.vtSymbol 578 | save_json(d, syncFile) 579 | # self.mainEngine.dbUpdate(POSITION_DB_NAME, strategy.className, 580 | # d, flt, True) 581 | 582 | content = '策略{name}持仓同步数据保存成功,当前持仓{pos}'.format(name=strategy.name, pos=strategy.pos) 583 | self.writeCtaLog(content) 584 | 585 | #---------------------------------------------------------------------- 586 | def loadSyncData(self, strategy): 587 | """从本地载入策略的持仓情况""" 588 | flt = {'name': strategy.name, 589 | 'vtSymbol': strategy.vtSymbol} 590 | # syncData = self.mainEngine.dbQuery(POSITION_DB_NAME, strategy.className, flt) 591 | syncData = {} 592 | syncFile = WORKING_DIR + 'setting/syncdata/' + strategy.name + '-' + strategy.vtSymbol 593 | try: 594 | syncData = load_json(syncFile) 595 | except IOError: 596 | content = '策略{name}持仓同步数据读取失败,未找到文件'.format(name=strategy.name) 597 | self.writeCtaLog(content) 598 | 599 | if not syncData: 600 | return 601 | 602 | # d = syncData[0] 603 | d = syncData 604 | 605 | for key in strategy.syncList: 606 | if key in d: 607 | strategy.__setattr__(key, d[key]) 608 | 609 | #---------------------------------------------------------------------- 610 | def roundToPriceTick(self, priceTick, price): 611 | """取整价格到合约最小价格变动""" 612 | if not priceTick: 613 | return price 614 | 615 | newPrice = round(price/priceTick, 0) * priceTick 616 | return newPrice 617 | 618 | #---------------------------------------------------------------------- 619 | def stop(self): 620 | """停止""" 621 | pass 622 | 623 | #---------------------------------------------------------------------- 624 | def cancelAll(self, name): 625 | """全部撤单""" 626 | s = self.strategyOrderDict[name] 627 | 628 | # 遍历列表,全部撤单 629 | # 这里不能直接遍历集合s,因为撤单时会修改s中的内容,导致出错 630 | for orderID in list(s): 631 | if STOPORDERPREFIX in orderID: 632 | self.cancelStopOrder(orderID) 633 | else: 634 | self.cancelOrder(orderID) 635 | -------------------------------------------------------------------------------- /py3_ctp_demo_on_vnpy/modules/ctpApi.py: -------------------------------------------------------------------------------- 1 | # encoding: UTF-8 2 | """ 3 | 从VNPY项目搬运新版穿透式监管ctpapi,验证登录部分代码做相应修改 4 | """ 5 | from modules.vnctpmd import MdApi 6 | from modules.vnctptd import TdApi 7 | from modules.eventEngine import * 8 | from modules.eventType import * 9 | from modules.ctpDataType import * 10 | from modules.objects import * 11 | 12 | from time import sleep 13 | from datetime import datetime, time 14 | import random 15 | import os 16 | from copy import copy 17 | 18 | # 以下为一些VT类型和CTP类型的映射字典 19 | # 价格类型映射 20 | priceTypeMap = {} 21 | priceTypeMap[PRICETYPE_LIMITPRICE] = defineDict["THOST_FTDC_OPT_LimitPrice"] 22 | priceTypeMap[PRICETYPE_MARKETPRICE] = defineDict["THOST_FTDC_OPT_AnyPrice"] 23 | priceTypeMapReverse = {v: k for k, v in priceTypeMap.items()} 24 | 25 | # 方向类型映射 26 | directionMap = {} 27 | directionMap[DIRECTION_LONG] = defineDict['THOST_FTDC_D_Buy'] 28 | directionMap[DIRECTION_SHORT] = defineDict['THOST_FTDC_D_Sell'] 29 | directionMapReverse = {v: k for k, v in directionMap.items()} 30 | 31 | # 开平类型映射 32 | offsetMap = {} 33 | offsetMap[OFFSET_OPEN] = defineDict['THOST_FTDC_OF_Open'] 34 | offsetMap[OFFSET_CLOSE] = defineDict['THOST_FTDC_OF_Close'] 35 | offsetMap[OFFSET_CLOSETODAY] = defineDict['THOST_FTDC_OF_CloseToday'] 36 | offsetMap[OFFSET_CLOSEYESTERDAY] = defineDict['THOST_FTDC_OF_CloseYesterday'] 37 | offsetMapReverse = {v:k for k,v in offsetMap.items()} 38 | 39 | # 交易所类型映射 40 | exchangeMap = {} 41 | exchangeMap[EXCHANGE_CFFEX] = 'CFFEX' 42 | exchangeMap[EXCHANGE_SHFE] = 'SHFE' 43 | exchangeMap[EXCHANGE_CZCE] = 'CZCE' 44 | exchangeMap[EXCHANGE_DCE] = 'DCE' 45 | exchangeMap[EXCHANGE_SSE] = 'SSE' 46 | exchangeMap[EXCHANGE_UNKNOWN] = '' 47 | exchangeMapReverse = {v:k for k,v in exchangeMap.items()} 48 | 49 | # 持仓类型映射 50 | posiDirectionMap = {} 51 | posiDirectionMap[DIRECTION_NET] = defineDict["THOST_FTDC_PD_Net"] 52 | posiDirectionMap[DIRECTION_LONG] = defineDict["THOST_FTDC_PD_Long"] 53 | posiDirectionMap[DIRECTION_SHORT] = defineDict["THOST_FTDC_PD_Short"] 54 | posiDirectionMapReverse = {v:k for k,v in posiDirectionMap.items()} 55 | 56 | # 产品类型映射 57 | productClassMap = {} 58 | productClassMap[PRODUCT_FUTURES] = defineDict["THOST_FTDC_PC_Futures"] 59 | productClassMap[PRODUCT_OPTION] = defineDict["THOST_FTDC_PC_Options"] 60 | productClassMap[PRODUCT_COMBINATION] = defineDict["THOST_FTDC_PC_Combination"] 61 | productClassMapReverse = {v:k for k,v in productClassMap.items()} 62 | 63 | # 委托状态映射 64 | statusMap = {} 65 | statusMap[STATUS_ALLTRADED] = defineDict["THOST_FTDC_OST_AllTraded"] 66 | statusMap[STATUS_PARTTRADED] = defineDict["THOST_FTDC_OST_PartTradedQueueing"] 67 | statusMap[STATUS_NOTTRADED] = defineDict["THOST_FTDC_OST_NoTradeQueueing"] 68 | statusMap[STATUS_CANCELLED] = defineDict["THOST_FTDC_OST_Canceled"] 69 | statusMapReverse = {v:k for k,v in statusMap.items()} 70 | 71 | class CtpMdApi(MdApi): 72 | """ 73 | Demo中的行情API封装 74 | 封装后所有数据自动推送到事件驱动引擎中,由其负责推送到各个监听该事件的回调函数上 75 | 76 | 对用户暴露的主动函数包括: 77 | 连接connect 78 | 登陆 login 79 | 订阅合约 subscribe 80 | """ 81 | def __init__(self, eventEngine): 82 | """ 83 | API对象的初始化函数 84 | """ 85 | super(CtpMdApi, self).__init__() 86 | 87 | self.__eventEngine = eventEngine 88 | 89 | self.reqID = 0 # 操作请求编号 90 | 91 | self.connectionStatus = False # 连接状态 92 | self.loginStatus = False # 登录状态 93 | 94 | self.subscribedSymbols = set() # 已订阅合约代码 95 | self.TradingDay = '' 96 | 97 | self.userID = '' # 账号 98 | self.password = '' # 密码 99 | self.brokerID = '' # 经纪商代码 100 | self.address = '' # 服务器地址 101 | self.gatewayName = 'CTP' # 网关名称 102 | 103 | def put_log_event(self, log): # log事件注册 104 | event = Event(type_=EVENT_LOG) 105 | event.dict_['log'] = log 106 | self.__eventEngine.put(event) 107 | 108 | def put_alarm_event(self, alarm): # log事件注册 109 | event = Event(type_=EVENT_ALARM) 110 | event.dict_['data'] = alarm 111 | self.__eventEngine.put(event) 112 | 113 | def onFrontConnected(self): 114 | """服务器连接""" 115 | self.connectionStatus = True 116 | 117 | log = u'行情服务器连接成功' 118 | self.put_log_event(log) 119 | 120 | self.login() 121 | #---------------------------------------------------------------------- 122 | def onFrontDisconnected(self, n): 123 | """服务器断开""" 124 | self.connectionStatus = False 125 | self.loginStatus = False 126 | 127 | log = u'行情服务器连接断开' 128 | self.put_log_event(log) 129 | 130 | now = datetime.now().time() 131 | if time(8, 48) < now < time(15, 30) or time(20, 48) < now <= time(23, 59) or time(0, 0) < now < time(2, 31): 132 | alarm = '行情服务器断开连接' 133 | self.put_alarm_event(alarm) 134 | #---------------------------------------------------------------------- 135 | def login(self): 136 | """登录""" 137 | # 如果填入了用户名密码等,则登录 138 | if self.userID and self.password and self.brokerID: 139 | req = {} 140 | req['UserID'] = self.userID 141 | req['Password'] = self.password 142 | req['BrokerID'] = self.brokerID 143 | self.reqID += 1 144 | self.reqUserLogin(req, self.reqID) 145 | 146 | #---------------------------------------------------------------------- 147 | def close(self): 148 | """关闭""" 149 | self.exit() 150 | 151 | def connect(self, userID, password, brokerID, address): 152 | """初始化连接""" 153 | self.userID = userID # 账号 154 | self.password = password # 密码 155 | self.brokerID = brokerID # 经纪商代码 156 | self.address = address # 服务器地址 157 | 158 | # 如果尚未建立服务器连接,则进行连接 159 | if not self.connectionStatus: 160 | # 创建C++环境中的API对象,这里传入的参数是需要用来保存.con文件的文件夹路径 161 | path = os.getcwd() + '/temp/' 162 | if not os.path.exists(path): 163 | os.makedirs(path) 164 | self.createFtdcMdApi(path) 165 | 166 | # 注册服务器地址 167 | self.registerFront(self.address) 168 | 169 | # 初始化连接,成功会调用onFrontConnected 170 | self.init() 171 | 172 | # 若已经连接但尚未登录,则进行登录 173 | else: 174 | if not self.loginStatus: 175 | self.login() 176 | 177 | def onRtnDepthMarketData(self, data): 178 | """行情推送""" 179 | if not data['Volume']: 180 | return 181 | 182 | # 创建对象 183 | tick = CtaTickData() 184 | 185 | tick.symbol = data['InstrumentID'] 186 | tick.exchange = data['ExchangeID'] #exchangeMapReverse.get(data['ExchangeID'], u'未知') 187 | tick.vtSymbol = tick.symbol #'.'.join([tick.symbol, EXCHANGE_UNKNOWN]) # 只用到ctp一个接口,这里没有必要区分 188 | 189 | tick.lastPrice = data['LastPrice'] 190 | tick.volume = data['Volume'] 191 | tick.openInterest = data['OpenInterest'] 192 | 193 | 194 | tick.time = '.'.join([data['UpdateTime'], str(int(data['UpdateMillisec']/100))]) 195 | # 不带毫秒的时间,方便转换datetime 196 | tick.time2 = data['UpdateTime'] 197 | # 把交易日也保存下来,转换datetime用 198 | tick.tradedate = self.TradingDay 199 | # print('tick.tradedate:%s'%tick.tradedate) 200 | 201 | # 这里由于交易所夜盘时段的交易日数据有误,所以选择本地获取 202 | tick.date = datetime.now().strftime('%Y%m%d') 203 | 204 | tick.openPrice = data['OpenPrice'] 205 | tick.highPrice = data['HighestPrice'] 206 | tick.lowPrice = data['LowestPrice'] 207 | tick.preClosePrice = data['PreClosePrice'] 208 | 209 | tick.upperLimit = data['UpperLimitPrice'] 210 | tick.lowerLimit = data['LowerLimitPrice'] 211 | 212 | # CTP只有一档行情 213 | # 无报价时用涨跌停板价替换,如果没有推送涨跌停价会出错,可以自行用一个很大的数字替代,或者就用推送的巨大数字 214 | # if data['BidPrice1'] > tick.upperLimit: 215 | # tick.bidPrice1 = tick.lowerLimit 216 | # else: 217 | # tick.bidPrice1 = data['BidPrice1'] 218 | # if data['AskPrice1'] > tick.upperLimit: 219 | # tick.askPrice1 = tick.upperLimit 220 | # else: 221 | # tick.askPrice1 = data['AskPrice1'] 222 | tick.askPrice1 = data['AskPrice1'] 223 | tick.bidPrice1 = data['BidPrice1'] 224 | 225 | tick.bidVolume1 = data['BidVolume1'] 226 | tick.askVolume1 = data['AskVolume1'] 227 | 228 | event1 = Event(type_=(EVENT_TICK + data['InstrumentID'])) 229 | event1.dict_['data'] = tick 230 | self.__eventEngine.put(event1) 231 | 232 | event2 = Event(type_=(EVENT_TICK)) 233 | event2.dict_['data'] = tick 234 | self.__eventEngine.put(event2) 235 | 236 | def subscribe(self, symbol): 237 | """订阅合约""" 238 | # 这里的设计是,如果尚未登录就调用了订阅方法 239 | # 则先保存订阅请求,登录完成后会自动订阅 240 | if self.loginStatus: 241 | self.subscribeMarketData(str(symbol)) 242 | self.subscribedSymbols.add(symbol) 243 | 244 | #---------------------------------------------------------------------- 245 | def unsubscribe(self, symbol): 246 | """退订合约""" 247 | self.unSubscribeMarketData(str(symbol)) 248 | 249 | #---------------------------------------------------------------------- 250 | def onRspError(self, error, n, last): 251 | """错误回报""" 252 | log = error['ErrorMsg'] 253 | self.put_log_event(log) 254 | 255 | #---------------------------------------------------------------------- 256 | def onRspUserLogin(self, data, error, n, last): 257 | """登陆回报""" 258 | # 如果登录成功,推送日志信息 259 | if not error or 0 == error['ErrorID']: 260 | self.loginStatus = True 261 | 262 | log = u'行情服务器登录完成' 263 | self.put_log_event(log) 264 | 265 | # 重新订阅之前订阅的合约 266 | for symbol in self.subscribedSymbols: 267 | self.subscribe(symbol) 268 | 269 | # 否则,推送错误信息 270 | else: 271 | log = error['ErrorMsg'] 272 | self.put_log_event(log) 273 | 274 | #---------------------------------------------------------------------- 275 | def onRspUserLogout(self, data, error, n, last): 276 | """登出回报""" 277 | # 如果登出成功,推送日志信息 278 | if not error or 0 == error['ErrorID']: 279 | self.loginStatus = False 280 | self.gateway.mdConnected = False 281 | 282 | log = u'行情服务器登出完成' 283 | self.put_log_event(log) 284 | 285 | # 否则,推送错误信息 286 | else: 287 | log = error['ErrorMsg'].decode('gbk') 288 | self.put_log_event(log) 289 | 290 | #---------------------------------------------------------------------- 291 | def onRspSubMarketData(self, data, error, n, last): 292 | """订阅合约回报""" 293 | # 通常不在乎订阅错误,选择忽略 294 | pass 295 | 296 | #---------------------------------------------------------------------- 297 | def onRspUnSubMarketData(self, data, error, n, last): 298 | """退订合约回报""" 299 | # 同上 300 | pass 301 | 302 | ######################################################################## 303 | class CtpTdApi(TdApi): 304 | """CTP交易API实现""" 305 | 306 | #---------------------------------------------------------------------- 307 | def __init__(self, riskengine, eventEngine): 308 | """API对象的初始化函数""" 309 | super(CtpTdApi, self).__init__() 310 | 311 | self.__eventEngine = eventEngine 312 | self.__riskengine = riskengine # 风控引擎 313 | 314 | self.reqID = 0 # 操作请求编号 315 | self.orderRef = random.randrange(start=1000,stop=9000,step=random.randint(10,100) ) # 订单编号 316 | 317 | 318 | self.connectionStatus = False # 连接状态 319 | self.loginStatus = False # 登录状态 320 | self.authStatus = False # 验证状态 321 | 322 | self.userID = '' # 账号 323 | self.password = '' # 密码 324 | self.brokerID = '' # 经纪商代码 325 | self.address = '' # 服务器地址 326 | self.authCode = '' # 授权码 327 | self.appID = '' # 软件代号 328 | self.userProductInfo = '' # 产品信息 329 | 330 | self.frontID = 0 # 前置机编号 331 | self.sessionID = 0 # 会话编号 332 | 333 | self.posDict = {} # 持仓缓存 334 | # self.posBufferDict = {} # 缓存持仓数据的字典 335 | self.symbolExchangeDict = {} # 保存合约代码和交易所的映射关系 336 | self.symbolSizeDict = {} # 保存合约代码和合约大小的映射关系 337 | self.symbolNameDict = {} # 保存合约代码和合约名称的映射关系 338 | 339 | #---------------------------------------------------------------------- 340 | def put_log_event(self, log): # 投放log事件 341 | event = Event(type_=EVENT_LOG) 342 | event.dict_['log'] = log 343 | self.__eventEngine.put(event) 344 | #---------------------------------------------------------------------- 345 | def onFrontConnected(self): 346 | """服务器连接""" 347 | self.connectionStatus = True 348 | 349 | log = u'交易服务器连接成功' 350 | self.put_log_event(log) 351 | 352 | self.authenticate() 353 | 354 | #---------------------------------------------------------------------- 355 | def onFrontDisconnected(self, n): 356 | """服务器断开""" 357 | self.connectionStatus = False 358 | self.loginStatus = False 359 | 360 | log = u'交易服务器连接断开' 361 | self.put_log_event(log) 362 | 363 | #---------------------------------------------------------------------- 364 | def onRspAuthenticate(self, data, error, n, last): 365 | """验证客户端回报""" 366 | if not error or 0 == error['ErrorID']: 367 | self.authStatus = True 368 | log = u'授权码验证成功' 369 | self.put_log_event(log) 370 | self.login() 371 | else: 372 | self.put_log_event(u'授权码验证失败:{}'.format(error)) 373 | 374 | #---------------------------------------------------------------------- 375 | def onRspUserLogin(self, data, error, n, last): 376 | """登陆回报""" 377 | # 如果登录成功,推送日志信息 378 | if not error or 0 == error['ErrorID']: 379 | self.frontID = str(data['FrontID']) 380 | self.sessionID = str(data['SessionID']) 381 | self.loginStatus = True 382 | 383 | log = data['UserID'] + u'交易服务器登录完成' 384 | self.put_log_event(log) 385 | 386 | # 确认结算信息 387 | req = {} 388 | req['BrokerID'] = self.brokerID 389 | req['InvestorID'] = self.userID 390 | self.reqID += 1 391 | self.reqSettlementInfoConfirm(req, self.reqID) 392 | 393 | # 否则,推送错误信息 394 | else: 395 | log = error['ErrorMsg'] 396 | self.put_log_event(log) 397 | 398 | #---------------------------------------------------------------------- 399 | def onRspUserLogout(self, data, error, n, last): 400 | """登出回报""" 401 | # 如果登出成功,推送日志信息 402 | if not error or 0 == error['ErrorID']: 403 | self.loginStatus = False 404 | 405 | log = u'交易服务器登出完成' 406 | self.put_log_event(log) 407 | 408 | # 否则,推送错误信息 409 | else: 410 | log = error['ErrorMsg'] 411 | self.put_log_event(log) 412 | #---------------------------------------------------------------------- 413 | def connect(self, userID, password, brokerID, address, appID, authCode, userProductInfo=''): 414 | """初始化连接""" 415 | self.userID = userID # 账号 416 | self.password = password # 密码 417 | self.brokerID = brokerID # 经纪商代码 418 | self.address = address # 服务器地址 419 | self.appID = appID # 软件代号 420 | self.authCode = authCode # 授权码 421 | self.userProductInfo = userProductInfo # 产品信息 422 | 423 | # 如果尚未建立服务器连接,则进行连接 424 | if not self.connectionStatus: 425 | # 创建C++环境中的API对象,这里传入的参数是需要用来保存.con文件的文件夹路径 426 | path = os.getcwd() + '/temp/' 427 | if not os.path.exists(path): 428 | os.makedirs(path) 429 | self.createFtdcTraderApi(path) 430 | 431 | # 注册服务器地址 432 | self.registerFront(self.address) 433 | 434 | # 初始化连接,成功会调用onFrontConnected 435 | self.init() 436 | 437 | # 若已经连接但尚未验证/登录,则进行验证 438 | else: 439 | if not self.authStatus or not self.loginStatus: 440 | self.authenticate() 441 | # ---------------------------------------------------------------------- 442 | def authenticate(self): 443 | """申请验证""" 444 | self.put_log_event(u'申请授权码验证') 445 | if self.userID and self.brokerID and self.authCode: 446 | req = {} 447 | req['UserID'] = self.userID 448 | req['BrokerID'] = self.brokerID 449 | req['AuthCode'] = self.authCode 450 | if len(self.userProductInfo) > 0: 451 | req['UserProductInfo'] = self.userProductInfo 452 | req['AppID'] = self.appID 453 | self.reqID +=1 454 | self.put_log_event(u'提交验证...') 455 | self.reqAuthenticate(req, self.reqID) 456 | 457 | #---------------------------------------------------------------------- 458 | def login(self): 459 | """连接服务器""" 460 | # 如果填入了用户名密码等,则登录 461 | if self.userID and self.password and self.brokerID: 462 | req = {} 463 | req['UserID'] = self.userID 464 | req['Password'] = self.password 465 | req['BrokerID'] = self.brokerID 466 | self.reqID += 1 467 | self.reqUserLogin(req, self.reqID) 468 | 469 | #---------------------------------------------------------------------- 470 | def qryAccount(self): 471 | """查询账户""" 472 | self.reqID += 1 473 | self.reqQryTradingAccount({}, self.reqID) 474 | 475 | #---------------------------------------------------------------------- 476 | def qryPosition(self): 477 | """查询持仓""" 478 | self.reqID += 1 479 | req = {} 480 | req['BrokerID'] = self.brokerID 481 | req['InvestorID'] = self.userID 482 | self.reqQryInvestorPosition(req, self.reqID) 483 | #---------------------------------------------------------------------- 484 | def qryInstrument(self): 485 | """查询合约""" 486 | self.reqID += 1 487 | req = {} 488 | self.reqQryInstrument(req, self.reqID) 489 | #---------------------------------------------------------------------- 490 | def qryMarketData(self): 491 | """查询合约截面数据""" 492 | self.reqID += 1 493 | req = {} 494 | self.reqQryDepthMarketData(req, self.reqID) # 查询合约截面数据 495 | #---------------------------------------------------------------------- 496 | def sendOrder(self, orderReq): 497 | """发单""" 498 | self.reqID += 1 499 | self.orderRef += 1 500 | 501 | #风控检查 502 | if not self.__riskengine.checkRisk(orderReq): 503 | return 504 | 505 | # # 不发单实盘测试 506 | # log = '接收到委托指令,未发出' 507 | # self.put_log_event(log) 508 | # return 509 | 510 | req = {} 511 | 512 | req['InstrumentID'] = orderReq.symbol # 合约代码 513 | req['LimitPrice'] = orderReq.price # 价格 514 | req['VolumeTotalOriginal'] = orderReq.volume # 数量 515 | 516 | # 下面如果由于传入的类型本接口不支持,则会返回空字符串 517 | req['OrderPriceType'] = priceTypeMap.get(orderReq.priceType, '') # 价格类型 518 | req['Direction'] = directionMap.get(orderReq.direction, '') # 方向 519 | req['CombOffsetFlag'] = offsetMap.get(orderReq.offset, '') # 组合标志 520 | 521 | req['OrderRef'] = str(self.orderRef) # 报单引用 522 | req['InvestorID'] = self.userID # 投资者代码 523 | req['UserID'] = self.userID # 账号 524 | req['BrokerID'] = self.brokerID # 经纪商代码 525 | 526 | req['CombHedgeFlag'] = defineDict['THOST_FTDC_HF_Speculation'] # 投机单 527 | req['ContingentCondition'] = defineDict['THOST_FTDC_CC_Immediately'] # 立即发单 528 | req['ForceCloseReason'] = defineDict['THOST_FTDC_FCC_NotForceClose'] # 非强平 529 | req['IsAutoSuspend'] = 0 # 非自动挂起 530 | req['TimeCondition'] = defineDict['THOST_FTDC_TC_GFD'] # 今日有效 531 | req['VolumeCondition'] = defineDict['THOST_FTDC_VC_AV'] # 任意成交量 532 | req['MinVolume'] = 1 # 最小成交量为1 533 | 534 | # 判断FAK和FOK 535 | if orderReq.priceType == PRICETYPE_FAK: 536 | req['OrderPriceType'] = defineDict["THOST_FTDC_OPT_LimitPrice"] 537 | req['TimeCondition'] = defineDict['THOST_FTDC_TC_IOC'] 538 | req['VolumeCondition'] = defineDict['THOST_FTDC_VC_AV'] 539 | if orderReq.priceType == PRICETYPE_FOK: 540 | req['OrderPriceType'] = defineDict["THOST_FTDC_OPT_LimitPrice"] 541 | req['TimeCondition'] = defineDict['THOST_FTDC_TC_IOC'] 542 | req['VolumeCondition'] = defineDict['THOST_FTDC_VC_CV'] 543 | 544 | self.reqOrderInsert(req, self.reqID) 545 | 546 | # 返回订单号(字符串),便于某些算法进行动态管理 547 | return '.'.join(["CTP", str(self.orderRef)]) 548 | #---------------------------------------------------------------------- 549 | def buy(self, symbol, price, vol): # 多开 550 | orderReq = CtaOrderReq() 551 | orderReq.symbol = symbol # 代码 552 | orderReq.price = price # 价格 553 | orderReq.volume = vol # 数量 554 | 555 | orderReq.priceType = PRICETYPE_LIMITPRICE # 价格类型 556 | orderReq.direction = DIRECTION_LONG # 买卖 557 | orderReq.offset = OFFSET_OPEN # 开平 558 | 559 | return self.sendOrder(orderReq) 560 | 561 | def sell(self, symbol, price, vol): # 多平 562 | orderReq = CtaOrderReq() 563 | orderReq.symbol = symbol # 代码 564 | orderReq.price = price # 价格 565 | orderReq.volume = vol # 数量 566 | 567 | orderReq.priceType = PRICETYPE_LIMITPRICE # 价格类型 568 | orderReq.direction = DIRECTION_SHORT # 买卖 569 | orderReq.offset = OFFSET_CLOSE # 开平 570 | 571 | return self.sendOrder(orderReq) 572 | 573 | def selltoday(self, symbol, price, vol): # 多头平今 574 | orderReq = CtaOrderReq() 575 | orderReq.symbol = symbol # 代码 576 | orderReq.price = price # 价格 577 | orderReq.volume = vol # 数量 578 | 579 | orderReq.priceType = PRICETYPE_LIMITPRICE # 价格类型 580 | orderReq.direction = DIRECTION_SHORT # 买卖 581 | orderReq.offset = OFFSET_CLOSETODAY # 开平 582 | 583 | return self.sendOrder(orderReq) 584 | 585 | def short(self, symbol, price, vol): # 空开 586 | orderReq = CtaOrderReq() 587 | orderReq.symbol = symbol # 代码 588 | orderReq.price = price # 价格 589 | orderReq.volume = vol # 数量 590 | 591 | orderReq.priceType = PRICETYPE_LIMITPRICE # 价格类型 592 | orderReq.direction = DIRECTION_SHORT # 买卖 593 | orderReq.offset = OFFSET_OPEN # 开平 594 | 595 | return self.sendOrder(orderReq) 596 | 597 | def cover(self, symbol, price, vol): # 空平 598 | orderReq = CtaOrderReq() 599 | orderReq.symbol = symbol # 代码 600 | orderReq.price = price # 价格 601 | orderReq.volume = vol # 数量 602 | 603 | orderReq.priceType = PRICETYPE_LIMITPRICE # 价格类型 604 | orderReq.direction = DIRECTION_LONG # 买卖 605 | orderReq.offset = OFFSET_CLOSE # 开平 606 | 607 | return self.sendOrder(orderReq) 608 | 609 | def covertoday(self, symbol, price, vol): # 空头平今 610 | orderReq = CtaOrderReq() 611 | orderReq.symbol = symbol # 代码 612 | orderReq.price = price # 价格 613 | orderReq.volume = vol # 数量 614 | 615 | orderReq.priceType = PRICETYPE_LIMITPRICE # 价格类型 616 | orderReq.direction = DIRECTION_LONG # 买卖 617 | orderReq.offset = OFFSET_CLOSETODAY # 开平 618 | 619 | return self.sendOrder(orderReq) 620 | #---------------------------------------------------------------------- 621 | def cancelOrder(self, cancelOrderReq): 622 | """撤单""" 623 | self.reqID += 1 624 | 625 | req = {} 626 | 627 | # req['InstrumentID'] = cancelOrderReq.symbol 628 | # req['OrderRef'] = cancelOrderReq.orderID 629 | # req['FrontID'] = cancelOrderReq.frontID 630 | # req['SessionID'] = cancelOrderReq.sessionID 631 | # 撤单有两种字段组合,其中一种没试成功 632 | req['ExchangeID'] = cancelOrderReq.exchange 633 | req['OrderSysID'] = cancelOrderReq.OrderSysID 634 | 635 | req['ActionFlag'] = defineDict['THOST_FTDC_AF_Delete'] 636 | req['BrokerID'] = self.brokerID 637 | req['InvestorID'] = self.userID 638 | 639 | self.reqOrderAction(req, self.reqID) 640 | 641 | #---------------------------------------------------------------------- 642 | def close(self): 643 | """关闭""" 644 | self.exit() 645 | 646 | #---------------------------------------------------------------------- 647 | def onRspSettlementInfoConfirm(self, data, error, n, last): 648 | """确认结算信息回报""" 649 | log = u'结算信息确认完成' 650 | self.put_log_event(log) 651 | 652 | # 查询合约代码 653 | self.reqID += 1 654 | self.reqQryInstrument({}, self.reqID) 655 | 656 | #---------------------------------------------------------------------- 657 | def onRspQryInstrument(self, data, error, n, last): 658 | """ 659 | 合约查询回报 660 | 由于该回报的推送速度极快,因此不适合全部存入队列中处理, 661 | 选择先储存在一个本地字典中,全部收集完毕后再推送到队列中 662 | (由于耗时过长目前使用其他进程读取) 663 | """ 664 | if not error or 0 == error['ErrorID']: 665 | self.symbolExchangeDict[data['InstrumentID']] = data['ExchangeID'] # 合约代码和交易所的映射关系 666 | self.symbolSizeDict[data['InstrumentID']] = data['VolumeMultiple'] # 合约代码和合约乘数映射关系 667 | self.symbolNameDict[data['InstrumentID']] = data['InstrumentName'] # 合约代码和合约名称映射关系 668 | 669 | event = Event(type_=EVENT_INSTRUMENT) 670 | event.dict_['data'] = data 671 | event.dict_['last'] = last 672 | self.__eventEngine.put(event) 673 | 674 | if last: 675 | sleep(1) 676 | self.reqID += 1 677 | self.reqQryDepthMarketData({}, self.reqID) # 查询合约截面数据 678 | 679 | else: 680 | log = '合约投资者回报,错误代码:' + str(error['ErrorID']) + ', 错误信息:' + str(error['ErrorMsg']) 681 | self.put_log_event(log) 682 | 683 | #---------------------------------------------------------------------- 684 | def onRspQryInvestorPosition(self, data, error, n, last): 685 | """持仓查询回报""" 686 | if not data['InstrumentID']: 687 | return 688 | if not error or 0 == error['ErrorID']: 689 | # 读取交易所id|合约名称|方向|合约乘数 690 | ExchangeID = data['ExchangeID'] = self.symbolExchangeDict.get(data['InstrumentID'], EXCHANGE_UNKNOWN) 691 | data['InstrumentName'] = self.symbolNameDict.get(data['InstrumentID'], PRODUCT_UNKNOWN) 692 | data['PosiDirection'] = posiDirectionMapReverse.get(data['PosiDirection'], '') 693 | # 读取不到的先按1计算,持仓中的开仓均价虽然会显示错误的数字,但程序不会崩溃 694 | data['VolumeMultiple'] = self.symbolSizeDict.get(data['InstrumentID'], 1) 695 | # 组合持仓的合约乘数为0,会导致除数为零的错误,暂且修改为1 696 | if data['VolumeMultiple'] == 0: 697 | data['VolumeMultiple'] = 1 698 | 699 | event = Event(type_=EVENT_POSITION) 700 | event.dict_['data'] = data 701 | event.dict_['last'] = last 702 | self.__eventEngine.put(event) 703 | 704 | # 获取持仓缓存对象 705 | posName = '.'.join([data['InstrumentID'], data['PosiDirection']]) 706 | 707 | if posName in self.posDict: 708 | pos = self.posDict[posName] 709 | else: 710 | pos = CtaPositionData() 711 | self.posDict[posName] = pos 712 | 713 | pos.gatewayName = 'CTP' 714 | pos.symbol = data['InstrumentID'] 715 | pos.vtSymbol = pos.symbol 716 | pos.direction = data['PosiDirection'] 717 | pos.vtPositionName = '.'.join([pos.vtSymbol, pos.direction]) 718 | pos.name = self.symbolNameDict.get(data['InstrumentID'], PRODUCT_UNKNOWN) 719 | 720 | # 针对上期所持仓的今昨分条返回(有昨仓、无今仓),读取昨仓数据.其他交易所只有一条,直接读取 721 | if (data['YdPosition'] and not data['TodayPosition']) and ExchangeID == EXCHANGE_SHFE: 722 | pos.ydPosition = data['Position'] 723 | # YdPosition字段存在一个问题,今天平昨仓不会减少这个字段的数量,改为从TodayPosition计算 724 | if ExchangeID != EXCHANGE_SHFE: 725 | pos.ydPosition = data['Position'] - data['TodayPosition'] 726 | 727 | # 计算成本 728 | size = self.symbolSizeDict[pos.symbol] 729 | cost = pos.price * pos.position * size 730 | openCost = pos.openPrice * pos.position * size 731 | 732 | # 汇总总仓 733 | pos.position += data['Position'] 734 | pos.positionProfit += data['PositionProfit'] 735 | # 计算开仓盈亏(浮) 736 | sign = 1 if pos.direction == DIRECTION_LONG else -1 737 | op = data["PositionProfit"] + (data["PositionCost"] - data["OpenCost"]) * sign 738 | pos.openProfit += op 739 | 740 | # 计算持仓均价和开仓均价 741 | if pos.position and size: 742 | pos.price = (cost + data['PositionCost']) / (pos.position * size) 743 | pos.openPrice = (openCost + data["OpenCost"]) / (pos.position * size) 744 | 745 | # 读取冻结 746 | if pos.direction == DIRECTION_LONG: 747 | pos.frozen += data['LongFrozen'] 748 | else: 749 | pos.frozen += data['ShortFrozen'] 750 | 751 | # 查询回报结束 752 | if last: 753 | # 遍历推送 754 | i = 0 755 | for pos in self.posDict.values(): 756 | # vnpy格式持仓事件 757 | i += 1 758 | lastPos = True if i >= len(self.posDict) else False 759 | 760 | event2 = Event(type_=EVENT_VNPOSITION) 761 | event2.dict_['data'] = pos 762 | event2.dict_['last'] = lastPos 763 | self.__eventEngine.put(event2) 764 | 765 | # 清空缓存 766 | self.posDict.clear() 767 | 768 | else: 769 | log = ('持仓查询回报,错误代码:' + str(error['ErrorID']) + ', 错误信息:' +str(error['ErrorMsg'])) 770 | self.put_log_event(log) 771 | 772 | #---------------------------------------------------------------------- 773 | def onRspQryDepthMarketData(self, data, error, n, last): 774 | # 常规行情事件,查询合约截面数据的回报 775 | event = Event(type_=EVENT_MARKETDATA) 776 | event.dict_['data'] = data 777 | event.dict_['last'] = last 778 | self.__eventEngine.put(event) 779 | 780 | #---------------------------------------------------------------------- 781 | def onRspQryTradingAccount(self, data, error, n, last): 782 | """资金账户查询回报""" 783 | if not error or 0 == error['ErrorID']: 784 | event = Event(type_=EVENT_ACCOUNT) 785 | event.dict_['data'] = data 786 | self.__eventEngine.put(event) 787 | else: 788 | log = ('账户查询回报,错误代码:' +str(error['ErrorID']) + ', 错误信息:' +str(error['ErrorMsg'])) 789 | self.put_log_event(log) 790 | 791 | #---------------------------------------------------------------------- 792 | def onRspOrderInsert(self, data, error, n, last): 793 | """发单错误(柜台)""" 794 | # 推送委托信息 795 | order = CtaOrderData() 796 | order.gatewayName = self.gatewayName 797 | order.symbol = data['InstrumentID'] 798 | order.exchange = exchangeMapReverse[data['ExchangeID']] 799 | order.vtSymbol = order.symbol 800 | order.orderID = data['OrderRef'] 801 | order.vtOrderID = '.'.join([self.gatewayName, order.orderID]) 802 | order.direction = directionMapReverse.get(data['Direction'], DIRECTION_UNKNOWN) 803 | order.offset = offsetMapReverse.get(data['CombOffsetFlag'], OFFSET_UNKNOWN) 804 | order.status = STATUS_REJECTED 805 | order.price = data['LimitPrice'] 806 | order.totalVolume = data['VolumeTotalOriginal'] 807 | # order.OrderSysID = data['OrderSysID'] 808 | 809 | # vnpy格式报单事件 810 | event = Event(type_=EVENT_VNORDER) 811 | event.dict_['data'] = order 812 | self.__eventEngine.put(event) 813 | 814 | log = '{msg},{id},{direction},{offset},p:{price},v:{vol},ref:{ref},from:onRspOrderInsert'.format( 815 | msg=error['ErrorMsg'], 816 | id=order.symbol, 817 | direction=order.direction, 818 | offset=order.offset, 819 | price=order.price, 820 | vol=order.totalVolume, 821 | ref=order.orderID) 822 | self.put_log_event(log) 823 | 824 | #---------------------------------------------------------------------- 825 | def onRspOrderAction(self, data, error, n, last): 826 | """撤单错误(柜台)""" 827 | log = '{msg},{id},ref:{ref},from:onRspOrderAction'.format( 828 | msg=error['ErrorMsg'], 829 | id=data['InstrumentID'], 830 | ref=data['OrderRef']) 831 | self.put_log_event(log) 832 | 833 | #---------------------------------------------------------------------- 834 | def onRspError(self, error, n, last): 835 | """错误回报""" 836 | log = error['ErrorMsg'] 837 | self.put_log_event(log) 838 | 839 | #---------------------------------------------------------------------- 840 | def onRtnOrder(self, data): 841 | """报单回报""" 842 | # 更新最大报单编号 843 | newref = data['OrderRef'] 844 | self.orderRef = max(self.orderRef, int(newref)) 845 | 846 | # 合约名称 847 | data['InstrumentName'] = self.symbolNameDict.get(data['InstrumentID'], PRODUCT_UNKNOWN) 848 | # 方向 849 | data['Direction'] = directionMapReverse.get(data['Direction'], DIRECTION_UNKNOWN) 850 | # 开平 851 | data['CombOffsetFlag'] = offsetMapReverse.get(data['CombOffsetFlag'], OFFSET_UNKNOWN) 852 | # 状态 853 | data['OrderStatus'] = statusMapReverse.get(data['OrderStatus'], STATUS_UNKNOWN) 854 | 855 | # 常规报单事件 856 | event1 = Event(type_=EVENT_ORDER) 857 | event1.dict_['data'] = data 858 | self.__eventEngine.put(event1) 859 | # # 特定合约报单事件 860 | # event2 = Event(type_=EVENT_ORDER+data['InstrumentID']) 861 | # event2.dict_['data'] = data 862 | # self.__eventEngine.put(event2) 863 | # 创建报单数据对象 864 | order = CtaOrderData() 865 | order.gatewayName = 'CTP' 866 | 867 | # # 保存代码和报单号 868 | order.symbol = data['InstrumentID'] 869 | order.exchange = exchangeMapReverse[data['ExchangeID']] 870 | order.vtSymbol = order.symbol # '.'.join([order.symbol, order.exchange]) 871 | 872 | # 报单号 873 | order.orderID = data['OrderRef'] 874 | # # CTP的报单号一致性维护需要基于frontID, sessionID, orderID三个字段 875 | # # 但在本接口设计中,已经考虑了CTP的OrderRef的自增性,避免重复 876 | # # 唯一可能出现OrderRef重复的情况是多处登录并在非常接近的时间内(几乎同时发单) 877 | # # 考虑到VtTrader的应用场景,认为以上情况不会构成问题 878 | order.vtOrderID = '.'.join([order.gatewayName, order.orderID]) 879 | 880 | order.direction = data['Direction'] 881 | order.offset = data['CombOffsetFlag'] 882 | order.status = data['OrderStatus'] 883 | # order.direction = directionMapReverse.get(data['Direction'], DIRECTION_UNKNOWN) 884 | # order.offset = offsetMapReverse.get(data['CombOffsetFlag'], OFFSET_UNKNOWN) 885 | # order.status = statusMapReverse.get(data['OrderStatus'], STATUS_UNKNOWN) 886 | 887 | # # 价格、报单量等数值 888 | order.price = data['LimitPrice'] 889 | order.totalVolume = data['VolumeTotalOriginal'] 890 | order.tradedVolume = data['VolumeTraded'] 891 | order.orderTime = data['InsertTime'] 892 | order.cancelTime = data['CancelTime'] 893 | order.frontID = data['FrontID'] 894 | order.sessionID = data['SessionID'] 895 | order.OrderSysID = data['OrderSysID'] 896 | # # vnpy格式报单事件 897 | event2 = Event(type_=EVENT_VNORDER) 898 | event2.dict_['data'] = order 899 | self.__eventEngine.put(event2) 900 | 901 | #---------------------------------------------------------------------- 902 | def onRtnTrade(self, data): 903 | """成交回报""" 904 | # 合约名称 905 | data['InstrumentName'] = self.symbolNameDict.get(data['InstrumentID'], PRODUCT_UNKNOWN) 906 | 907 | # 方向 908 | data['Direction'] = directionMapReverse.get(data['Direction'], '') 909 | 910 | # 开平 911 | data['OffsetFlag'] = offsetMapReverse.get(data['OffsetFlag'], '') 912 | 913 | event1 = Event(type_=EVENT_TRADE) 914 | event1.dict_['data'] = data 915 | self.__eventEngine.put(event1) 916 | 917 | # 创建报单数据对象 918 | trade = CtaTradeData() 919 | trade.gatewayName = "CTP" 920 | 921 | # 保存代码和报单号 922 | trade.symbol = data['InstrumentID'] 923 | trade.exchange = exchangeMapReverse[data['ExchangeID']] 924 | trade.vtSymbol = trade.symbol #'.'.join([trade.symbol, trade.exchange]) 925 | 926 | # 成交号 927 | trade.tradeID = data['TradeID'] 928 | trade.vtTradeID = '.'.join([trade.gatewayName, trade.tradeID]) 929 | 930 | # 报单号 931 | trade.orderID = data['OrderRef'] 932 | trade.vtOrderID = '.'.join([trade.gatewayName, trade.orderID]) 933 | # 方向 934 | trade.direction = data['Direction'] # directionMapReverse.get(data['Direction'], '') 935 | 936 | # 开平 937 | trade.offset = data['OffsetFlag'] # offsetMapReverse.get(data['OffsetFlag'], '') 938 | 939 | # 价格、报单量等数值 940 | trade.price = data['Price'] 941 | trade.volume = data['Volume'] 942 | trade.tradeTime = data['TradeTime'] 943 | 944 | # vn格式成交推送 945 | event2 = Event(type_=EVENT_VNTRADE) 946 | event2.dict_['data'] = trade 947 | self.__eventEngine.put(event2) 948 | #---------------------------------------------------------------------- 949 | def onErrRtnOrderInsert(self, data, error): 950 | """发单错误回报(交易所)""" 951 | # 推送委托信息 952 | order = CtaOrderData() 953 | order.gatewayName = self.gatewayName 954 | order.symbol = data['InstrumentID'] 955 | order.exchange = exchangeMapReverse[data['ExchangeID']] 956 | order.vtSymbol = order.symbol 957 | order.orderID = data['OrderRef'] 958 | order.vtOrderID = '.'.join([self.gatewayName, order.orderID]) 959 | order.direction = directionMapReverse.get(data['Direction'], DIRECTION_UNKNOWN) 960 | order.offset = offsetMapReverse.get(data['CombOffsetFlag'], OFFSET_UNKNOWN) 961 | order.status = STATUS_REJECTED 962 | order.price = data['LimitPrice'] 963 | order.totalVolume = data['VolumeTotalOriginal'] 964 | order.OrderSysID = data['OrderSysID'] 965 | 966 | # vnpy格式报单事件 967 | event2 = Event(type_=EVENT_VNORDER) 968 | event2.dict_['data'] = order 969 | self.__eventEngine.put(event2) 970 | 971 | log = '{msg},{id},{direction},{offset},p:{price},v:{vol},ref:{ref},from:onErrRtnOrderInsert'.format( 972 | msg=error['ErrorMsg'], 973 | id=order.symbol, 974 | direction=order.direction, 975 | offset=order.offset, 976 | price=order.price, 977 | vol=order.totalVolume, 978 | ref=order.orderID) 979 | self.put_log_event(log) 980 | 981 | #---------------------------------------------------------------------- 982 | def onErrRtnOrderAction(self, data, error): 983 | """撤单错误回报(交易所)""" 984 | log = error['ErrorMsg'] 985 | self.put_log_event(log) 986 | 987 | #---------------------------------------------------------------------- 988 | def onHeartBeatWarning(self, n): 989 | """""" 990 | pass 991 | 992 | #---------------------------------------------------------------------- 993 | def onRspRemoveParkedOrder(self, data, error, n, last): 994 | """""" 995 | pass 996 | 997 | #---------------------------------------------------------------------- 998 | def onRspQueryMaxOrderVolume(self, data, error, n, last): 999 | """""" 1000 | pass 1001 | 1002 | #---------------------------------------------------------------------- 1003 | def onRspRemoveParkedOrderAction(self, data, error, n, last): 1004 | """""" 1005 | pass 1006 | 1007 | #---------------------------------------------------------------------- 1008 | def onRspUserPasswordUpdate(self, data, error, n, last): 1009 | """""" 1010 | pass 1011 | 1012 | #---------------------------------------------------------------------- 1013 | def onRspTradingAccountPasswordUpdate(self, data, error, n, last): 1014 | """""" 1015 | pass 1016 | 1017 | #---------------------------------------------------------------------- 1018 | def onRspParkedOrderInsert(self, data, error, n, last): 1019 | """""" 1020 | pass 1021 | 1022 | #---------------------------------------------------------------------- 1023 | def onRspParkedOrderAction(self, data, error, n, last): 1024 | """""" 1025 | pass 1026 | 1027 | #---------------------------------------------------------------------- 1028 | def onRspExecOrderInsert(self, data, error, n, last): 1029 | """""" 1030 | pass 1031 | 1032 | #---------------------------------------------------------------------- 1033 | def onRspExecOrderAction(self, data, error, n, last): 1034 | """""" 1035 | pass 1036 | 1037 | #---------------------------------------------------------------------- 1038 | def onRspForQuoteInsert(self, data, error, n, last): 1039 | """""" 1040 | pass 1041 | 1042 | #---------------------------------------------------------------------- 1043 | def onRspQuoteInsert(self, data, error, n, last): 1044 | """""" 1045 | pass 1046 | 1047 | #---------------------------------------------------------------------- 1048 | def onRspQuoteAction(self, data, error, n, last): 1049 | """""" 1050 | pass 1051 | 1052 | #---------------------------------------------------------------------- 1053 | def onRspLockInsert(self, data, error, n, last): 1054 | """""" 1055 | pass 1056 | 1057 | #---------------------------------------------------------------------- 1058 | def onRspCombActionInsert(self, data, error, n, last): 1059 | """""" 1060 | pass 1061 | 1062 | #---------------------------------------------------------------------- 1063 | def onRspQryOrder(self, data, error, n, last): 1064 | """""" 1065 | pass 1066 | 1067 | #---------------------------------------------------------------------- 1068 | def onRspQryTrade(self, data, error, n, last): 1069 | """""" 1070 | pass 1071 | 1072 | 1073 | #---------------------------------------------------------------------- 1074 | def onRspQryInvestor(self, data, error, n, last): 1075 | """""" 1076 | pass 1077 | 1078 | #---------------------------------------------------------------------- 1079 | def onRspQryTradingCode(self, data, error, n, last): 1080 | """""" 1081 | pass 1082 | 1083 | #---------------------------------------------------------------------- 1084 | def onRspQryInstrumentMarginRate(self, data, error, n, last): 1085 | """""" 1086 | pass 1087 | 1088 | #---------------------------------------------------------------------- 1089 | def onRspQryInstrumentCommissionRate(self, data, error, n, last): 1090 | """""" 1091 | pass 1092 | 1093 | #---------------------------------------------------------------------- 1094 | def onRspQryExchange(self, data, error, n, last): 1095 | """""" 1096 | pass 1097 | 1098 | #---------------------------------------------------------------------- 1099 | def onRspQryProduct(self, data, error, n, last): 1100 | """""" 1101 | pass 1102 | 1103 | #---------------------------------------------------------------------- 1104 | def onRspQrySettlementInfo(self, data, error, n, last): 1105 | """""" 1106 | pass 1107 | 1108 | #---------------------------------------------------------------------- 1109 | def onRspQryTransferBank(self, data, error, n, last): 1110 | """""" 1111 | pass 1112 | 1113 | #---------------------------------------------------------------------- 1114 | def onRspQryInvestorPositionDetail(self, data, error, n, last): 1115 | """""" 1116 | pass 1117 | 1118 | #---------------------------------------------------------------------- 1119 | def onRspQryNotice(self, data, error, n, last): 1120 | """""" 1121 | pass 1122 | 1123 | #---------------------------------------------------------------------- 1124 | def onRspQrySettlementInfoConfirm(self, data, error, n, last): 1125 | """""" 1126 | pass 1127 | 1128 | #---------------------------------------------------------------------- 1129 | def onRspQryInvestorPositionCombineDetail(self, data, error, n, last): 1130 | """""" 1131 | pass 1132 | 1133 | #---------------------------------------------------------------------- 1134 | def onRspQryCFMMCTradingAccountKey(self, data, error, n, last): 1135 | """""" 1136 | pass 1137 | 1138 | #---------------------------------------------------------------------- 1139 | def onRspQryEWarrantOffset(self, data, error, n, last): 1140 | """""" 1141 | pass 1142 | 1143 | #---------------------------------------------------------------------- 1144 | def onRspQryInvestorProductGroupMargin(self, data, error, n, last): 1145 | """""" 1146 | pass 1147 | 1148 | #---------------------------------------------------------------------- 1149 | def onRspQryExchangeMarginRate(self, data, error, n, last): 1150 | """""" 1151 | pass 1152 | 1153 | #---------------------------------------------------------------------- 1154 | def onRspQryExchangeMarginRateAdjust(self, data, error, n, last): 1155 | """""" 1156 | pass 1157 | 1158 | #---------------------------------------------------------------------- 1159 | def onRspQryExchangeRate(self, data, error, n, last): 1160 | """""" 1161 | pass 1162 | 1163 | #---------------------------------------------------------------------- 1164 | def onRspQrySecAgentACIDMap(self, data, error, n, last): 1165 | """""" 1166 | pass 1167 | 1168 | #---------------------------------------------------------------------- 1169 | def onRspQryProductExchRate(self, data, error, n, last): 1170 | """""" 1171 | pass 1172 | 1173 | #---------------------------------------------------------------------- 1174 | def onRspQryProductGroup(self, data, error, n, last): 1175 | """""" 1176 | pass 1177 | 1178 | #---------------------------------------------------------------------- 1179 | def onRspQryOptionInstrTradeCost(self, data, error, n, last): 1180 | """""" 1181 | pass 1182 | 1183 | #---------------------------------------------------------------------- 1184 | def onRspQryOptionInstrCommRate(self, data, error, n, last): 1185 | """""" 1186 | pass 1187 | 1188 | #---------------------------------------------------------------------- 1189 | def onRspQryExecOrder(self, data, error, n, last): 1190 | """""" 1191 | pass 1192 | 1193 | #---------------------------------------------------------------------- 1194 | def onRspQryForQuote(self, data, error, n, last): 1195 | """""" 1196 | pass 1197 | 1198 | #---------------------------------------------------------------------- 1199 | def onRspQryQuote(self, data, error, n, last): 1200 | """""" 1201 | pass 1202 | 1203 | #---------------------------------------------------------------------- 1204 | def onRspQryLock(self, data, error, n, last): 1205 | """""" 1206 | pass 1207 | 1208 | #---------------------------------------------------------------------- 1209 | def onRspQryLockPosition(self, data, error, n, last): 1210 | """""" 1211 | pass 1212 | 1213 | #---------------------------------------------------------------------- 1214 | def onRspQryInvestorLevel(self, data, error, n, last): 1215 | """""" 1216 | pass 1217 | 1218 | #---------------------------------------------------------------------- 1219 | def onRspQryExecFreeze(self, data, error, n, last): 1220 | """""" 1221 | pass 1222 | 1223 | #---------------------------------------------------------------------- 1224 | def onRspQryCombInstrumentGuard(self, data, error, n, last): 1225 | """""" 1226 | pass 1227 | 1228 | #---------------------------------------------------------------------- 1229 | def onRspQryCombAction(self, data, error, n, last): 1230 | """""" 1231 | pass 1232 | 1233 | #---------------------------------------------------------------------- 1234 | def onRspQryTransferSerial(self, data, error, n, last): 1235 | """""" 1236 | pass 1237 | 1238 | #---------------------------------------------------------------------- 1239 | def onRspQryAccountregister(self, data, error, n, last): 1240 | """""" 1241 | pass 1242 | 1243 | 1244 | #---------------------------------------------------------------------- 1245 | def onRtnInstrumentStatus(self, data): 1246 | """""" 1247 | pass 1248 | 1249 | #---------------------------------------------------------------------- 1250 | def onRtnTradingNotice(self, data): 1251 | """""" 1252 | pass 1253 | 1254 | #---------------------------------------------------------------------- 1255 | def onRtnErrorConditionalOrder(self, data): 1256 | """""" 1257 | pass 1258 | 1259 | #---------------------------------------------------------------------- 1260 | def onRtnExecOrder(self, data): 1261 | """""" 1262 | pass 1263 | 1264 | #---------------------------------------------------------------------- 1265 | def onErrRtnExecOrderInsert(self, data, error): 1266 | """""" 1267 | pass 1268 | 1269 | #---------------------------------------------------------------------- 1270 | def onErrRtnExecOrderAction(self, data, error): 1271 | """""" 1272 | pass 1273 | 1274 | #---------------------------------------------------------------------- 1275 | def onErrRtnForQuoteInsert(self, data, error): 1276 | """""" 1277 | pass 1278 | 1279 | #---------------------------------------------------------------------- 1280 | def onRtnQuote(self, data): 1281 | """""" 1282 | pass 1283 | 1284 | #---------------------------------------------------------------------- 1285 | def onErrRtnQuoteInsert(self, data, error): 1286 | """""" 1287 | pass 1288 | 1289 | #---------------------------------------------------------------------- 1290 | def onErrRtnQuoteAction(self, data, error): 1291 | """""" 1292 | pass 1293 | 1294 | #---------------------------------------------------------------------- 1295 | def onRtnForQuoteRsp(self, data): 1296 | """""" 1297 | pass 1298 | 1299 | #---------------------------------------------------------------------- 1300 | def onRtnCFMMCTradingAccountToken(self, data): 1301 | """""" 1302 | pass 1303 | 1304 | #---------------------------------------------------------------------- 1305 | def onRtnLock(self, data): 1306 | """""" 1307 | pass 1308 | 1309 | #---------------------------------------------------------------------- 1310 | def onErrRtnLockInsert(self, data, error): 1311 | """""" 1312 | pass 1313 | 1314 | #---------------------------------------------------------------------- 1315 | def onRtnCombAction(self, data): 1316 | """""" 1317 | pass 1318 | 1319 | #---------------------------------------------------------------------- 1320 | def onErrRtnCombActionInsert(self, data, error): 1321 | """""" 1322 | pass 1323 | 1324 | #---------------------------------------------------------------------- 1325 | def onRspQryContractBank(self, data, error, n, last): 1326 | """""" 1327 | pass 1328 | 1329 | #---------------------------------------------------------------------- 1330 | def onRspQryParkedOrder(self, data, error, n, last): 1331 | """""" 1332 | pass 1333 | 1334 | #---------------------------------------------------------------------- 1335 | def onRspQryParkedOrderAction(self, data, error, n, last): 1336 | """""" 1337 | pass 1338 | 1339 | #---------------------------------------------------------------------- 1340 | def onRspQryTradingNotice(self, data, error, n, last): 1341 | """""" 1342 | pass 1343 | 1344 | #---------------------------------------------------------------------- 1345 | def onRspQryBrokerTradingParams(self, data, error, n, last): 1346 | """""" 1347 | pass 1348 | 1349 | #---------------------------------------------------------------------- 1350 | def onRspQryBrokerTradingAlgos(self, data, error, n, last): 1351 | """""" 1352 | pass 1353 | 1354 | #---------------------------------------------------------------------- 1355 | def onRspQueryCFMMCTradingAccountToken(self, data, error, n, last): 1356 | """""" 1357 | pass 1358 | 1359 | #---------------------------------------------------------------------- 1360 | def onRtnFromBankToFutureByBank(self, data): 1361 | """""" 1362 | pass 1363 | 1364 | #---------------------------------------------------------------------- 1365 | def onRtnFromFutureToBankByBank(self, data): 1366 | """""" 1367 | pass 1368 | 1369 | #---------------------------------------------------------------------- 1370 | def onRtnRepealFromBankToFutureByBank(self, data): 1371 | """""" 1372 | pass 1373 | 1374 | #---------------------------------------------------------------------- 1375 | def onRtnRepealFromFutureToBankByBank(self, data): 1376 | """""" 1377 | pass 1378 | 1379 | #---------------------------------------------------------------------- 1380 | def onRtnFromBankToFutureByFuture(self, data): 1381 | """""" 1382 | pass 1383 | 1384 | #---------------------------------------------------------------------- 1385 | def onRtnFromFutureToBankByFuture(self, data): 1386 | """""" 1387 | pass 1388 | 1389 | #---------------------------------------------------------------------- 1390 | def onRtnRepealFromBankToFutureByFutureManual(self, data): 1391 | """""" 1392 | pass 1393 | 1394 | #---------------------------------------------------------------------- 1395 | def onRtnRepealFromFutureToBankByFutureManual(self, data): 1396 | """""" 1397 | pass 1398 | 1399 | #---------------------------------------------------------------------- 1400 | def onRtnQueryBankBalanceByFuture(self, data): 1401 | """""" 1402 | pass 1403 | 1404 | #---------------------------------------------------------------------- 1405 | def onErrRtnBankToFutureByFuture(self, data, error): 1406 | """""" 1407 | pass 1408 | 1409 | #---------------------------------------------------------------------- 1410 | def onErrRtnFutureToBankByFuture(self, data, error): 1411 | """""" 1412 | pass 1413 | 1414 | #---------------------------------------------------------------------- 1415 | def onErrRtnRepealBankToFutureByFutureManual(self, data, error): 1416 | """""" 1417 | pass 1418 | 1419 | #---------------------------------------------------------------------- 1420 | def onErrRtnRepealFutureToBankByFutureManual(self, data, error): 1421 | """""" 1422 | pass 1423 | 1424 | #---------------------------------------------------------------------- 1425 | def onErrRtnQueryBankBalanceByFuture(self, data, error): 1426 | """""" 1427 | pass 1428 | 1429 | #---------------------------------------------------------------------- 1430 | def onRtnRepealFromBankToFutureByFuture(self, data): 1431 | """""" 1432 | pass 1433 | 1434 | #---------------------------------------------------------------------- 1435 | def onRtnRepealFromFutureToBankByFuture(self, data): 1436 | """""" 1437 | pass 1438 | 1439 | #---------------------------------------------------------------------- 1440 | def onRspFromBankToFutureByFuture(self, data, error, n, last): 1441 | """""" 1442 | pass 1443 | 1444 | #---------------------------------------------------------------------- 1445 | def onRspFromFutureToBankByFuture(self, data, error, n, last): 1446 | """""" 1447 | pass 1448 | 1449 | #---------------------------------------------------------------------- 1450 | def onRspQueryBankAccountMoneyByFuture(self, data, error, n, last): 1451 | """""" 1452 | pass 1453 | 1454 | #---------------------------------------------------------------------- 1455 | def onRtnOpenAccountByBank(self, data): 1456 | """""" 1457 | pass 1458 | 1459 | #---------------------------------------------------------------------- 1460 | def onRtnCancelAccountByBank(self, data): 1461 | """""" 1462 | pass 1463 | 1464 | #---------------------------------------------------------------------- 1465 | def onRtnChangeAccountByBank(self, data): 1466 | """""" 1467 | pass 1468 | 1469 | 1470 | 1471 | -------------------------------------------------------------------------------- /py3_ctp_demo_on_vnpy/modules/eventEngine.py: -------------------------------------------------------------------------------- 1 | # encoding: UTF-8 2 | ''' 3 | VNPY的事件引擎原有两版,一个用QTimer计时,一个用子线程计时 4 | 这里选择了子线程的版本 5 | ''' 6 | # 系统模块 7 | from queue import Queue, Empty 8 | from threading import Thread 9 | from collections import defaultdict 10 | from time import sleep 11 | 12 | EVENT_TIMER = 'eTimer' 13 | 14 | ######################################################################## 15 | class EventEngine(object): 16 | """ 17 | 计时器使用python线程的事件驱动引擎 18 | """ 19 | 20 | #---------------------------------------------------------------------- 21 | def __init__(self): 22 | """初始化事件引擎""" 23 | # 事件队列 24 | self.__queue = Queue() 25 | 26 | # 事件引擎开关 27 | self.__active = False 28 | 29 | # 事件处理线程 30 | self.__thread = Thread(target = self.__run) 31 | 32 | # 计时器,用于触发计时器事件 33 | self.__timer = Thread(target = self.__runTimer) 34 | self.__timerActive = False # 计时器工作状态 35 | self.__timerSleep = 1 # 计时器触发间隔(默认1秒) 36 | 37 | # 这里的__handlers是一个字典,用来保存对应的事件调用关系 38 | # 其中每个键对应的值是一个列表,列表中保存了对该事件进行监听的函数功能 39 | self.__handlers = defaultdict(list) 40 | 41 | # __generalHandlers是一个列表,用来保存通用回调函数(所有事件均调用) 42 | self.__generalHandlers = [] 43 | 44 | #---------------------------------------------------------------------- 45 | def __run(self): 46 | """引擎运行""" 47 | while self.__active == True: 48 | try: 49 | event = self.__queue.get(block = True, timeout = 1) # 获取事件的阻塞时间设为1秒 50 | self.__process(event) 51 | except Empty: 52 | pass 53 | 54 | #---------------------------------------------------------------------- 55 | def __process(self, event): 56 | """处理事件""" 57 | # 检查是否存在对该事件进行监听的处理函数 58 | if event.type_ in self.__handlers: 59 | # 若存在,则按顺序将事件传递给处理函数执行 60 | [handler(event) for handler in self.__handlers[event.type_]] 61 | 62 | # 以上语句为Python列表解析方式的写法,对应的常规循环写法为: 63 | #for handler in self.__handlers[event.type_]: 64 | #handler(event) 65 | 66 | # 调用通用处理函数进行处理 67 | if self.__generalHandlers: 68 | [handler(event) for handler in self.__generalHandlers] 69 | 70 | #---------------------------------------------------------------------- 71 | def __runTimer(self): 72 | """运行在计时器线程中的循环函数""" 73 | while self.__timerActive: 74 | # 创建计时器事件 75 | event = Event(type_=EVENT_TIMER) 76 | 77 | # 向队列中存入计时器事件 78 | self.put(event) 79 | 80 | # 等待 81 | sleep(self.__timerSleep) 82 | 83 | #---------------------------------------------------------------------- 84 | def start(self, timer=True): 85 | """ 86 | 引擎启动 87 | timer:是否要启动计时器 88 | """ 89 | # 将引擎设为启动 90 | self.__active = True 91 | 92 | # 启动事件处理线程 93 | self.__thread.start() 94 | 95 | # 启动计时器,计时器事件间隔默认设定为1秒 96 | if timer: 97 | self.__timerActive = True 98 | self.__timer.start() 99 | 100 | #---------------------------------------------------------------------- 101 | def stop(self): 102 | """停止引擎""" 103 | # 将引擎设为停止 104 | self.__active = False 105 | 106 | # 停止计时器 107 | self.__timerActive = False 108 | self.__timer.join() 109 | 110 | # 等待事件处理线程退出 111 | self.__thread.join() 112 | 113 | #---------------------------------------------------------------------- 114 | def register(self, type_, handler): 115 | """注册事件处理函数监听""" 116 | # 尝试获取该事件类型对应的处理函数列表,若无defaultDict会自动创建新的list 117 | handlerList = self.__handlers[type_] 118 | 119 | # 若要注册的处理器不在该事件的处理器列表中,则注册该事件 120 | if handler not in handlerList: 121 | handlerList.append(handler) 122 | 123 | #---------------------------------------------------------------------- 124 | def unregister(self, type_, handler): 125 | """注销事件处理函数监听""" 126 | # 尝试获取该事件类型对应的处理函数列表,若无则忽略该次注销请求 127 | handlerList = self.__handlers[type_] 128 | 129 | # 如果该函数存在于列表中,则移除 130 | if handler in handlerList: 131 | handlerList.remove(handler) 132 | 133 | # 如果函数列表为空,则从引擎中移除该事件类型 134 | if not handlerList: 135 | del self.__handlers[type_] 136 | 137 | #---------------------------------------------------------------------- 138 | def put(self, event): 139 | """向事件队列中存入事件""" 140 | self.__queue.put(event) 141 | 142 | #---------------------------------------------------------------------- 143 | def registerGeneralHandler(self, handler): 144 | """注册通用事件处理函数监听""" 145 | if handler not in self.__generalHandlers: 146 | self.__generalHandlers.append(handler) 147 | 148 | #---------------------------------------------------------------------- 149 | def unregisterGeneralHandler(self, handler): 150 | """注销通用事件处理函数监听""" 151 | if handler in self.__generalHandlers: 152 | self.__generalHandlers.remove(handler) 153 | ######################################################################## 154 | class Event: 155 | """事件对象""" 156 | 157 | #---------------------------------------------------------------------- 158 | def __init__(self, type_=None): 159 | """Constructor""" 160 | self.type_ = type_ # 事件类型 161 | self.dict_ = {} # 字典用于保存具体的事件数据 162 | 163 | 164 | #---------------------------------------------------------------------- 165 | 166 | 167 | # 直接运行脚本可以进行测试 168 | if __name__ == '__main__': 169 | def test(): 170 | """测试函数""" 171 | from datetime import datetime 172 | 173 | def simpletest(event): 174 | print(u'处理每秒触发的计时器事件:%s' % str(datetime.now())) 175 | 176 | 177 | ee = EventEngine() 178 | ee.register(EVENT_TIMER, simpletest) 179 | ee.start() 180 | test() -------------------------------------------------------------------------------- /py3_ctp_demo_on_vnpy/modules/eventType.py: -------------------------------------------------------------------------------- 1 | # encoding: UTF-8 2 | 3 | ''' 4 | 本文件仅用于存放对于事件类型常量的定义。 5 | 由于python中不存在真正的常量概念,因此选择使用全大写的变量名来代替常量。 6 | 这里设计的命名规则以EVENT_前缀开头。 7 | 常量的内容通常选择一个能够代表真实意义的字符串(便于理解)。 8 | 建议将所有的常量定义放在该文件中,便于检查是否存在重复的现象。 9 | ''' 10 | 11 | 12 | EVENT_TIMER = 'eTimer' # 计时器事件,每隔1秒发送一次 13 | EVENT_LOG = 'eLog' # 日志事件 14 | EVENT_MARKETDATA_CONTRACT = 'eMarketdataContract' # TICK行情事件 15 | EVENT_INSTRUMENT = 'eInstrument' # 合约事件 16 | EVENT_MARKETDATA = 'eMarketData' # 常规行情事件 17 | EVENT_CONTRACT = 'eContract' # 合约信息 18 | EVENT_ACCOUNT = 'eAccount' # 账户事件 19 | EVENT_POSITION = 'ePosition' # 常规持仓事件 20 | EVENT_VNPOSITION = 'eVnPosition' # vn格式的持仓事件 21 | EVENT_ORDER = 'eOrder' # 常规报单事件 22 | EVENT_VNORDER = 'eVnOrder' # vn格式的报单事件 23 | EVENT_TRADE ='eTrade' # 常规成交事件 24 | EVENT_VNTRADE = 'eVnTrade' # vn格式的成交事件 25 | EVENT_PRODUCT = 'eProduct' # 主力合约事件 26 | EVENT_TICK = 'eTick' # tick行情事件 27 | EVENT_TURTLE = 'eTurtle' # 土鳖策略事件 28 | EVENT_GRAPH = 'eGraph' # 绘图事件 29 | EVENT_PLOT = 'ePlot' # 新版绘图事件 30 | EVENT_GRAPHSHOW = 'eGraphshow' # 显示图形监控界面事件 31 | EVENT_ALARM = 'eAlarm' # 警报事件 32 | EVENT_ALARMSEND = 'eAlarmSend' # 警报已发送事件 33 | EVENT_ALIVE = 'eAlive' # 存活确认事件 34 | EVENT_STATUS = 'eStatus' # 观察者的状态事件 35 | EVENT_CTA_ROBOT = 'eCtaRobot' # 策略自动启动事件 36 | 37 | 38 | # 直接运行脚本可以进行测试 39 | if __name__ == '__main__': 40 | #---------------------------------------------------------------------- 41 | def test(): 42 | """检查是否存在内容重复的常量定义""" 43 | check_dict = {} 44 | 45 | global_dict = globals() 46 | 47 | for key, value in global_dict.items(): 48 | if '__' not in key: # 不检查python内置对象 49 | if value in check_dict: 50 | check_dict[value].append(key) 51 | else: 52 | check_dict[value] = [key] 53 | 54 | for key, value in check_dict.items(): 55 | if len(value)>1: 56 | print(u'存在重复的常量定义:' + str(key) ) 57 | for name in value: 58 | print(name) 59 | print('') 60 | 61 | print(u'测试完毕') 62 | test() -------------------------------------------------------------------------------- /py3_ctp_demo_on_vnpy/modules/functions.py: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | import json 3 | 4 | def load_json(jsonfile): 5 | '''读取json中的数据''' 6 | try: 7 | with open(jsonfile, 'r', encoding='utf-8') as f: 8 | data = json.load(f) 9 | return data 10 | except IOError: 11 | raise 12 | 13 | def save_json(data, savepath): 14 | '''以json格式保存数据到目标路径''' 15 | with open(savepath, 'w', encoding='utf-8') as f: 16 | jsonD = json.dumps(data, indent=4) 17 | f.write(jsonD) 18 | -------------------------------------------------------------------------------- /py3_ctp_demo_on_vnpy/modules/objects.py: -------------------------------------------------------------------------------- 1 | # encoding: UTF-8 2 | 3 | # 默认空值 4 | EMPTY_STRING = '' 5 | EMPTY_UNICODE = u'' 6 | EMPTY_INT = 0 7 | EMPTY_FLOAT = 0.0 8 | 9 | # 方向常量 10 | DIRECTION_NONE = u'无方向' 11 | DIRECTION_LONG = u'买入' 12 | DIRECTION_SHORT = u'卖出' 13 | DIRECTION_UNKNOWN = u'未知' 14 | DIRECTION_NET = u'净' 15 | DIRECTION_SELL = u'卖出' # IB接口 16 | DIRECTION_COVEREDSHORT = u'备兑空' # 证券期权 17 | 18 | # 开平常量 19 | OFFSET_NONE = u'无开平' 20 | OFFSET_OPEN = u'开仓' 21 | OFFSET_CLOSE = u'平仓' 22 | OFFSET_CLOSETODAY = u'平今' 23 | OFFSET_CLOSEYESTERDAY = u'平昨' 24 | OFFSET_UNKNOWN = u'未知' 25 | 26 | # 状态常量 27 | STATUS_NOTTRADED = u'未成交' 28 | STATUS_PARTTRADED = u'部分成交' 29 | STATUS_ALLTRADED = u'全部成交' 30 | STATUS_CANCELLED = u'已撤销' 31 | STATUS_REJECTED = u'拒单' 32 | STATUS_UNKNOWN = u'未知' 33 | 34 | # 合约类型常量 35 | PRODUCT_EQUITY = u'股票' 36 | PRODUCT_FUTURES = u'期货' 37 | PRODUCT_OPTION = u'期权' 38 | PRODUCT_INDEX = u'指数' 39 | PRODUCT_COMBINATION = u'组合' 40 | PRODUCT_FOREX = u'外汇' 41 | PRODUCT_UNKNOWN = u'未知' 42 | PRODUCT_SPOT = u'现货' 43 | PRODUCT_DEFER = u'延期' 44 | PRODUCT_NONE = '' 45 | 46 | # 价格类型常量 47 | PRICETYPE_LIMITPRICE = u'限价' 48 | PRICETYPE_MARKETPRICE = u'市价' 49 | PRICETYPE_FAK = u'FAK' 50 | PRICETYPE_FOK = u'FOK' 51 | 52 | # 期权类型 53 | OPTION_CALL = u'看涨期权' 54 | OPTION_PUT = u'看跌期权' 55 | 56 | # 交易所类型 57 | EXCHANGE_SSE = 'SSE' # 上交所 58 | EXCHANGE_SZSE = 'SZSE' # 深交所 59 | EXCHANGE_CFFEX = 'CFFEX' # 中金所 60 | EXCHANGE_SHFE = 'SHFE' # 上期所 61 | EXCHANGE_CZCE = 'CZCE' # 郑商所 62 | EXCHANGE_DCE = 'DCE' # 大商所 63 | EXCHANGE_SGE = 'SGE' # 上金所 64 | EXCHANGE_INE = 'INE' # 国际能源交易中心 65 | EXCHANGE_UNKNOWN = 'UNKNOWN'# 未知交易所 66 | EXCHANGE_NONE = '' # 空交易所 67 | 68 | 69 | # 货币类型 70 | CURRENCY_USD = 'USD' # 美元 71 | CURRENCY_CNY = 'CNY' # 人民币 72 | CURRENCY_HKD = 'HKD' # 港币 73 | CURRENCY_UNKNOWN = 'UNKNOWN' # 未知货币 74 | CURRENCY_NONE = '' # 空货币 75 | 76 | # 接口类型 77 | GATEWAYTYPE_FUTURES = 'futures' # 期货、期权、贵金属 78 | GATEWAYTYPE_DATA = 'data' # 数据(非交易) 79 | 80 | # CTA相关常量定义 81 | # CTA引擎中涉及到的交易方向类型 82 | CTAORDER_BUY = u'买开' 83 | CTAORDER_SELL = u'卖平' 84 | CTAORDER_SHORT = u'卖开' 85 | CTAORDER_COVER = u'买平' 86 | 87 | # 本地停止单状态 88 | STOPORDER_WAITING = u'等待中' 89 | STOPORDER_CANCELLED = u'已撤销' 90 | STOPORDER_TRIGGERED = u'已触发' 91 | 92 | # 本地停止单前缀 93 | STOPORDERPREFIX = 'CtaStopOrder.' 94 | # 引擎类型,用于区分当前策略的运行环境 95 | ENGINETYPE_BACKTESTING = 'backtesting' # 回测 96 | ENGINETYPE_TRADING = 'trading' # 实盘 97 | 98 | # CTA模块事件 99 | EVENT_CTA_LOG = 'eCtaLog' # CTA相关的日志事件 100 | EVENT_CTA_STRATEGY = 'eCtaStrategy.' # CTA策略状态变化事件 101 | 102 | # CTA text 103 | INIT = u'初始化' 104 | START = u'启动' 105 | STOP = u'停止' 106 | 107 | CTA_ENGINE_STARTED = u'CTA引擎启动成功' 108 | 109 | CTA_STRATEGY = u'CTA策略' 110 | LOAD_STRATEGY = u'加载策略' 111 | INIT_ALL = u'全部初始化' 112 | START_ALL = u'全部启动' 113 | STOP_ALL = u'全部停止' 114 | SAVE_POSITION_DATA = u'保存持仓' 115 | 116 | STRATEGY_LOADED = u'策略加载成功' 117 | 118 | SAVE_POSITION_QUESTION = u'是否要保存策略持仓数据到数据库?' 119 | class CtaTickData(object): 120 | """Tick数据""" 121 | 122 | #---------------------------------------------------------------------- 123 | def __init__(self): 124 | """Constructor""" 125 | self.vtSymbol = '' # vt系统代码 126 | self.symbol = '' # 合约代码 127 | self.exchange = '' # 交易所代码 128 | 129 | # 成交数据 130 | self.lastPrice = 0.0 # 最新成交价 131 | self.volume = 0 # 最新成交量 132 | self.openInterest = 0 # 持仓量 133 | 134 | self.upperLimit = 0.0 # 涨停价 135 | self.lowerLimit = 0.0 # 跌停价 136 | 137 | # tick的时间 138 | self.date = '' # 日期 139 | self.time = '' # 时间 140 | self.datetime = None # python的datetime时间对象 141 | 142 | # 五档行情 143 | self.bidPrice1 = 0.0 144 | self.bidPrice2 = 0.0 145 | self.bidPrice3 = 0.0 146 | self.bidPrice4 = 0.0 147 | self.bidPrice5 = 0.0 148 | 149 | self.askPrice1 = 0.0 150 | self.askPrice2 = 0.0 151 | self.askPrice3 = 0.0 152 | self.askPrice4 = 0.0 153 | self.askPrice5 = 0.0 154 | 155 | self.bidVolume1 = 0 156 | self.bidVolume2 = 0 157 | self.bidVolume3 = 0 158 | self.bidVolume4 = 0 159 | self.bidVolume5 = 0 160 | 161 | self.askVolume1 = 0 162 | self.askVolume2 = 0 163 | self.askVolume3 = 0 164 | self.askVolume4 = 0 165 | self.askVolume5 = 0 166 | 167 | ######################################################################## 168 | class CtaOrderReq(object): 169 | """发单时传入的对象类""" 170 | 171 | #---------------------------------------------------------------------- 172 | def __init__(self): 173 | """Constructor""" 174 | self.symbol = EMPTY_STRING # 代码 175 | self.exchange = EMPTY_STRING # 交易所 176 | self.OrderSysID = EMPTY_STRING # 本地报单编号 177 | self.price = EMPTY_FLOAT # 价格 178 | self.volume = EMPTY_INT # 数量 179 | 180 | self.priceType = EMPTY_STRING # 价格类型 181 | self.direction = EMPTY_STRING # 买卖 182 | self.offset = EMPTY_STRING # 开平 183 | 184 | ######################################################################## 185 | class CtaCancelOrderReq(object): 186 | """撤单时传入的对象类""" 187 | 188 | #---------------------------------------------------------------------- 189 | def __init__(self): 190 | """Constructor""" 191 | self.symbol = EMPTY_STRING # 代码 192 | self.exchange = EMPTY_STRING # 交易所 193 | 194 | # 以下字段主要和CTP、LTS类接口相关 195 | self.orderID = EMPTY_STRING # 报单号 196 | self.frontID = EMPTY_STRING # 前置机号 197 | self.sessionID = EMPTY_STRING # 会话号 198 | self.OrderSysID = EMPTY_STRING 199 | ######################################################################## 200 | class CtaOrderData(object): 201 | """订单数据类""" 202 | 203 | #---------------------------------------------------------------------- 204 | def __init__(self): 205 | """Constructor""" 206 | self.gatewayName = EMPTY_STRING # Gateway名称 207 | # 代码编号相关 208 | self.symbol = EMPTY_STRING # 合约代码 209 | self.exchange = EMPTY_STRING # 交易所代码 210 | self.vtSymbol = EMPTY_STRING # 合约在vt系统中的唯一代码,通常是 合约代码.交易所代码 211 | 212 | self.orderID = EMPTY_STRING # 订单编号 213 | self.vtOrderID = EMPTY_STRING # 订单在vt系统中的唯一编号,通常是 Gateway名.订单编号 214 | 215 | # 报单相关 216 | self.direction = EMPTY_UNICODE # 报单方向 217 | self.offset = EMPTY_UNICODE # 报单开平仓 218 | self.price = EMPTY_FLOAT # 报单价格 219 | self.totalVolume = EMPTY_INT # 报单总数量 220 | self.tradedVolume = EMPTY_INT # 报单成交数量 221 | self.status = EMPTY_UNICODE # 报单状态 222 | 223 | self.orderTime = EMPTY_STRING # 发单时间 224 | self.cancelTime = EMPTY_STRING # 撤单时间 225 | 226 | # CTP/LTS相关 227 | self.frontID = EMPTY_INT # 前置机编号 228 | self.sessionID = EMPTY_INT # 连接编号 229 | self.OrderSysID = EMPTY_INT 230 | 231 | class CtaPositionData(object): 232 | """持仓数据类""" 233 | 234 | #---------------------------------------------------------------------- 235 | def __init__(self): 236 | """Constructor""" 237 | self.gatewayName = EMPTY_STRING # Gateway名称 238 | # 代码编号相关 239 | self.symbol = EMPTY_STRING # 合约代码 240 | self.exchange = EMPTY_STRING # 交易所代码 241 | self.vtSymbol = EMPTY_STRING # 合约在vt系统中的唯一代码,合约代码.交易所代码 242 | 243 | # 持仓相关 244 | self.direction = EMPTY_STRING # 持仓方向 245 | self.position = EMPTY_INT # 持仓量 246 | self.frozen = EMPTY_INT # 冻结数量 247 | self.price = EMPTY_FLOAT # 持仓均价 248 | self.vtPositionName = EMPTY_STRING # 持仓在vt系统中的唯一代码,通常是vtSymbol.方向 249 | self.ydPosition = EMPTY_INT # 昨持仓 250 | self.positionProfit = EMPTY_FLOAT # 持仓盈亏(盯) 251 | 252 | # 自行添加 253 | self.openPrice = EMPTY_FLOAT # 开仓均价 254 | self.openProfit = EMPTY_FLOAT # 开仓盈亏(浮) 255 | self.name = EMPTY_STRING # 合约名称 256 | ######################################################################## 257 | class CtaTradeData(object): 258 | """成交数据类""" 259 | 260 | #---------------------------------------------------------------------- 261 | def __init__(self): 262 | """Constructor""" 263 | self.gatewayName = EMPTY_STRING # Gateway名称 264 | # 代码编号相关 265 | self.symbol = EMPTY_STRING # 合约代码 266 | self.exchange = EMPTY_STRING # 交易所代码 267 | self.vtSymbol = EMPTY_STRING # 合约在vt系统中的唯一代码,通常是 合约代码.交易所代码 268 | 269 | self.tradeID = EMPTY_STRING # 成交编号 270 | self.vtTradeID = EMPTY_STRING # 成交在vt系统中的唯一编号,通常是 Gateway名.成交编号 271 | 272 | self.orderID = EMPTY_STRING # 订单编号 273 | self.vtOrderID = EMPTY_STRING # 订单在vt系统中的唯一编号,通常是 Gateway名.订单编号 274 | 275 | # 成交相关 276 | self.direction = EMPTY_UNICODE # 成交方向 277 | self.offset = EMPTY_UNICODE # 成交开平仓 278 | self.price = EMPTY_FLOAT # 成交价格 279 | self.volume = EMPTY_INT # 成交数量 280 | self.tradeTime = EMPTY_STRING # 成交时间 281 | ######################################################################## 282 | class CtaContractData(object): 283 | """合约详细信息类""" 284 | 285 | #---------------------------------------------------------------------- 286 | def __init__(self): 287 | """Constructor""" 288 | self.gatewayName = EMPTY_STRING # Gateway名称 289 | self.symbol = EMPTY_STRING # 代码 290 | self.exchange = EMPTY_STRING # 交易所代码 291 | self.vtSymbol = EMPTY_STRING # 合约在vt系统中的唯一代码,通常是 合约代码.交易所代码 292 | self.name = EMPTY_UNICODE # 合约中文名 293 | 294 | self.productClass = EMPTY_UNICODE # 合约类型 295 | self.size = EMPTY_INT # 合约大小 296 | self.priceTick = EMPTY_FLOAT # 合约最小价格TICK 297 | 298 | # 期权相关 299 | self.strikePrice = EMPTY_FLOAT # 期权行权价 300 | self.underlyingSymbol = EMPTY_STRING # 标的物合约代码 301 | self.optionType = EMPTY_UNICODE # 期权类型 302 | ######################################################################## 303 | 304 | class CtaSubscribeReq(object): 305 | """订阅行情时传入的对象类""" 306 | 307 | #---------------------------------------------------------------------- 308 | def __init__(self): 309 | """Constructor""" 310 | self.symbol = EMPTY_STRING # 代码 311 | self.exchange = EMPTY_STRING # 交易所 312 | 313 | # 以下为IB相关 314 | self.productClass = EMPTY_UNICODE # 合约类型 315 | self.currency = EMPTY_STRING # 合约货币 316 | self.expiry = EMPTY_STRING # 到期日 317 | self.strikePrice = EMPTY_FLOAT # 行权价 318 | self.optionType = EMPTY_UNICODE # 期权类型 319 | 320 | ######################################################################## 321 | class StopOrder(object): 322 | """本地停止单""" 323 | 324 | #---------------------------------------------------------------------- 325 | def __init__(self): 326 | """Constructor""" 327 | self.vtSymbol = EMPTY_STRING 328 | self.orderType = EMPTY_UNICODE 329 | self.direction = EMPTY_UNICODE 330 | self.offset = EMPTY_UNICODE 331 | self.price = EMPTY_FLOAT 332 | self.volume = EMPTY_INT 333 | 334 | self.strategy = None # 下停止单的策略对象 335 | self.stopOrderID = EMPTY_STRING # 停止单的本地编号 336 | self.status = EMPTY_STRING # 停止单状态 337 | -------------------------------------------------------------------------------- /py3_ctp_demo_on_vnpy/modules/rmEngine.py: -------------------------------------------------------------------------------- 1 | # encoding: UTF-8 2 | 3 | ''' 4 | 本文件中实现了风控引擎,用于提供一系列常用的风控功能: 5 | 1. 委托流控(单位时间内最大允许发出的委托数量) 6 | 2. 总成交限制(每日总成交数量限制) 7 | 3. 单笔委托的委托数量控制 8 | ''' 9 | 10 | import json 11 | import os 12 | import platform 13 | from modules.eventEngine import * 14 | from modules.eventType import * 15 | 16 | 17 | ######################################################################## 18 | class RmEngine(object): 19 | """风控引擎""" 20 | settingFileName = 'RM_setting.json' 21 | path = os.path.abspath(os.path.dirname(__file__)) 22 | settingFileName = os.path.join(path, settingFileName) 23 | 24 | name = u'风控模块' 25 | 26 | #---------------------------------------------------------------------- 27 | def __init__(self, eventEngine): 28 | """Constructor""" 29 | #self.mainEngine = mainEngine 30 | self.eventEngine = eventEngine 31 | 32 | # 是否启动风控 33 | self.active = True 34 | 35 | # 流控相关 36 | self.orderFlowCount = 0 # 单位时间内委托计数 37 | self.orderFlowLimit = 0 # 委托限制 38 | self.orderFlowClear = 0 # 计数清空时间(秒) 39 | self.orderFlowTimer = 0 # 计数清空时间计时 40 | 41 | # 单笔委托相关 42 | self.orderSizeLimit = 0 # 单笔委托最大限制 43 | 44 | # 成交统计相关 45 | self.tradeCount = 0 # 当日成交合约数量统计 46 | self.tradeLimit = 0 # 当日成交合约数量限制 47 | 48 | # 活动合约相关 49 | #self.workingOrderLimit = 0 # 活动合约最大限制 50 | 51 | self.loadSetting() 52 | self.registerEvent() 53 | 54 | #---------------------------------------------------------------------- 55 | def loadSetting(self): 56 | """读取配置""" 57 | with open(self.settingFileName) as f: 58 | d = json.load(f) 59 | 60 | # 设置风控参数 61 | self.active = d['active'] 62 | 63 | self.orderFlowLimit = d['orderFlowLimit'] 64 | self.orderFlowClear = d['orderFlowClear'] 65 | 66 | self.orderSizeLimit = d['orderSizeLimit'] 67 | 68 | self.tradeLimit = d['tradeLimit'] 69 | 70 | #---------------------------------------------------------------------- 71 | def saveSetting(self): 72 | """保存风控参数""" 73 | with open(self.settingFileName, 'w') as f: 74 | # 保存风控参数 75 | d = {} 76 | 77 | d['active'] = self.active 78 | 79 | d['orderFlowLimit'] = self.orderFlowLimit 80 | d['orderFlowClear'] = self.orderFlowClear 81 | 82 | d['orderSizeLimit'] = self.orderSizeLimit 83 | 84 | d['tradeLimit'] = self.tradeLimit 85 | 86 | #d['workingOrderLimit'] = self.workingOrderLimit 87 | 88 | # 写入json 89 | jsonD = json.dumps(d, indent=4) 90 | f.write(jsonD) 91 | 92 | #---------------------------------------------------------------------- 93 | def registerEvent(self): 94 | """注册事件监听""" 95 | self.eventEngine.register(EVENT_TRADE, self.updateTrade) 96 | self.eventEngine.register(EVENT_TIMER, self.updateTimer) 97 | 98 | #---------------------------------------------------------------------- 99 | def updateTrade(self, event): 100 | """更新成交数据""" 101 | trade = event.dict_['data'] 102 | self.tradeCount += trade['Volume'] 103 | 104 | #---------------------------------------------------------------------- 105 | def updateTimer(self, event): 106 | """更新定时器""" 107 | self.orderFlowTimer += 1 108 | 109 | # 如果计时超过了流控清空的时间间隔,则执行清空 110 | if self.orderFlowTimer >= self.orderFlowClear: 111 | self.orderFlowCount = 0 112 | self.orderFlowTimer = 0 113 | 114 | #---------------------------------------------------------------------- 115 | def writeRiskLog(self, content): 116 | """快速发出日志事件""" 117 | # 发出报警提示音 118 | 119 | if platform.uname() == 'Windows': 120 | import winsound 121 | winsound.PlaySound("SystemHand", winsound.SND_ASYNC) 122 | 123 | # 发出日志事件 124 | 125 | event = Event(type_=EVENT_LOG) 126 | event.dict_['log'] = content 127 | self.eventEngine.put(event) 128 | 129 | #---------------------------------------------------------------------- 130 | def checkRisk(self, orderReq): 131 | """检查风险""" 132 | # 如果没有启动风控检查,则直接返回成功 133 | if not self.active: 134 | return True 135 | 136 | # 检查委托数量 137 | if orderReq.volume > self.orderSizeLimit: 138 | self.writeRiskLog(u'单笔委托数量%s,超过限制%s' 139 | %(orderReq.volume, self.orderSizeLimit)) 140 | return False 141 | 142 | # 检查成交合约量 143 | if self.tradeCount >= self.tradeLimit: 144 | self.writeRiskLog(u'今日总成交合约数量%s,超过限制%s' 145 | %(self.tradeCount, self.tradeLimit)) 146 | return False 147 | 148 | # 检查流控 149 | if self.orderFlowCount >= self.orderFlowLimit: 150 | self.writeRiskLog(u'委托流数量%s,超过限制每%s秒%s' 151 | %(self.orderFlowCount, self.orderFlowClear, self.orderFlowLimit)) 152 | return False 153 | """ 154 | # 检查总活动合约 155 | workingOrderCount = len(self.mainEngine.getAllWorkingOrders()) 156 | if workingOrderCount >= self.workingOrderLimit: 157 | self.writeRiskLog(u'当前活动委托数量%s,超过限制%s' 158 | %(workingOrderCount, self.workingOrderLimit)) 159 | return False 160 | """ 161 | # 对于通过风控的委托,增加流控计数 162 | self.orderFlowCount += 1 163 | 164 | return True 165 | 166 | #---------------------------------------------------------------------- 167 | def clearOrderFlowCount(self): 168 | """清空流控计数""" 169 | self.orderFlowCount = 0 170 | self.writeRiskLog(u'清空流控计数') 171 | 172 | #---------------------------------------------------------------------- 173 | def clearTradeCount(self): 174 | """清空成交数量计数""" 175 | self.tradeCount = 0 176 | self.writeRiskLog(u'清空总成交计数') 177 | 178 | #---------------------------------------------------------------------- 179 | def setOrderFlowLimit(self, n): 180 | """设置流控限制""" 181 | self.orderFlowLimit = n 182 | 183 | #---------------------------------------------------------------------- 184 | def setOrderFlowClear(self, n): 185 | """设置流控清空时间""" 186 | self.orderFlowClear = n 187 | 188 | #---------------------------------------------------------------------- 189 | def setOrderSizeLimit(self, n): 190 | """设置委托最大限制""" 191 | self.orderSizeLimit = n 192 | 193 | #---------------------------------------------------------------------- 194 | def setTradeLimit(self, n): 195 | """设置成交限制""" 196 | self.tradeLimit = n 197 | 198 | #---------------------------------------------------------------------- 199 | def switchEngineStatus(self): 200 | """开关风控引擎""" 201 | self.active = not self.active 202 | 203 | if self.active: 204 | self.writeRiskLog(u'风险管理功能启动') 205 | else: 206 | self.writeRiskLog(u'风险管理功能停止') 207 | -------------------------------------------------------------------------------- /py3_ctp_demo_on_vnpy/modules/thostmduserapi_se.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vvipi/py3_demo_on_vnpy_ctp/491f6394ce049a7dd93a34415cd2ffe499bf9ffa/py3_ctp_demo_on_vnpy/modules/thostmduserapi_se.dll -------------------------------------------------------------------------------- /py3_ctp_demo_on_vnpy/modules/thosttraderapi_se.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vvipi/py3_demo_on_vnpy_ctp/491f6394ce049a7dd93a34415cd2ffe499bf9ffa/py3_ctp_demo_on_vnpy/modules/thosttraderapi_se.dll -------------------------------------------------------------------------------- /py3_ctp_demo_on_vnpy/modules/uiWidgets.py: -------------------------------------------------------------------------------- 1 | # encoding: UTF-8 2 | """ 3 | 窗口部件 4 | """ 5 | from PyQt5.QtCore import * 6 | from PyQt5.QtGui import * 7 | from PyQt5.QtWidgets import * 8 | from datetime import datetime, date 9 | import json 10 | import logging 11 | 12 | from modules.eventEngine import * 13 | from modules.eventType import * 14 | from modules.objects import * 15 | 16 | 17 | class MainWindow(QMainWindow): 18 | """主窗口""" 19 | # signal = pyqtSignal(type(Event())) 20 | #---------------------------------------------------------------------- 21 | def __init__(self, mainEngine, eventEngine): 22 | """Constructor""" 23 | super(MainWindow, self).__init__() 24 | 25 | self.widgetDict = {} # 用来保存子窗口的字典 26 | self.me = mainEngine 27 | self.ee = eventEngine 28 | 29 | self.initUi() 30 | #---------------------------------------------------------------------- 31 | def initUi(self): 32 | """初始化界面""" 33 | self.setWindowTitle("CTP DEMO——基于vnpy的ctp接口") 34 | self.setWindowIcon(QIcon('resource/fs.ico')) 35 | 36 | widgetStrategyM, dockStrategyM = self.createDock(CtaEngineManager, '策略', Qt.TopDockWidgetArea, engine=self.me.ce) 37 | widgetLogM, dockLogM = self.createDock(LogMonitor, '日志', Qt.BottomDockWidgetArea, engine=None, floatable=True) 38 | widgetAccountM, dockAccountM = self.createDock(AccountMonitor, '账户资金', Qt.BottomDockWidgetArea) 39 | widgetPositionM, dockPositionM = self.createDock(PositionMonitor, '持仓', Qt.BottomDockWidgetArea) 40 | widgetTradeM, dockTradeM = self.createDock(TradeMonitor, '成交', Qt.BottomDockWidgetArea) 41 | widgetOrderM, dockOrderM = self.createDock(OrderMonitor, '委托', Qt.BottomDockWidgetArea) 42 | widgetNonetradeM, dockNonetradeM = self.createDock(NonetradeMonitor, '撤单', Qt.BottomDockWidgetArea, engine=self.me) 43 | 44 | self.tabifyDockWidget(dockAccountM, dockPositionM) 45 | self.tabifyDockWidget(dockAccountM, dockTradeM) 46 | self.tabifyDockWidget(dockAccountM, dockOrderM) 47 | self.tabifyDockWidget(dockAccountM, dockNonetradeM) 48 | self.tabifyDockWidget(dockAccountM, dockLogM) 49 | 50 | # dockPlotM.raise_() 51 | dockLogM.raise_() 52 | dockAccountM.setMinimumWidth(720) 53 | dockLogM.setMinimumWidth(260) 54 | 55 | aboutAction = QAction(u'关于', self) 56 | aboutAction.triggered.connect(self.openAbout) 57 | 58 | rmAction = QAction(u'风险管理', self) 59 | rmAction.triggered.connect(self.openRM) 60 | 61 | menubar = self.menuBar() 62 | sysMenu = menubar.addMenu(u'系统') 63 | sysMenu.addAction(rmAction) 64 | helpMenu = menubar.addMenu(u'帮助') 65 | helpMenu.addAction(aboutAction) 66 | 67 | def createDock(self, widgetClass, widgetName, widgetArea, engine=None, floatable=False): 68 | """创建停靠组件""" 69 | widget = widgetClass(self.ee, engine) if engine else widgetClass(self.ee) 70 | 71 | dock = QDockWidget(widgetName) 72 | dock.setWidget(widget) 73 | dock.setObjectName(widgetName) 74 | if floatable: 75 | dock.setFeatures(dock.DockWidgetFloatable|dock.DockWidgetMovable|dock.DockWidgetClosable) 76 | else: 77 | dock.setFeatures(dock.DockWidgetMovable) 78 | self.addDockWidget(widgetArea, dock) 79 | return widget, dock 80 | 81 | def openAbout(self): 82 | """打开关于""" 83 | try: 84 | self.widgetDict['aboutW'].show() 85 | except KeyError: 86 | self.widgetDict['aboutW'] = AboutWidget(self) 87 | self.widgetDict['aboutW'].show() 88 | 89 | def openRM(self): 90 | """打开风控模块""" 91 | try: 92 | self.widgetDict['rmW'].show() 93 | except KeyError: 94 | self.widgetDict['rmW'] = RmEngineManager(self.me.re, self.ee) 95 | self.widgetDict['rmW'].show() 96 | 97 | def closeEvent(self, event): 98 | """关闭事件""" 99 | reply = QMessageBox.question(self, '退出', 100 | '确认退出?', QMessageBox.Yes | 101 | QMessageBox.No, QMessageBox.No) 102 | 103 | if reply == QMessageBox.Yes: 104 | for widget in self.widgetDict.values(): 105 | widget.close() 106 | 107 | self.me.exit() 108 | event.accept() 109 | else: 110 | event.ignore() 111 | 112 | ######################################################################## 113 | class LogMonitor(QTableWidget): 114 | """用于显示日志""" 115 | signal = pyqtSignal(type(Event())) 116 | #---------------------------------------------------------------------- 117 | def __init__(self,eventEngine, parent=None): 118 | """Constructor""" 119 | super(LogMonitor, self).__init__(parent) 120 | self.__eventEngine = eventEngine 121 | self.setWindowTitle('日志') 122 | self.setColumnCount(2) 123 | self.setHorizontalHeaderLabels(['时间', '日志']) 124 | self.verticalHeader().setVisible(False) # 关闭左边的垂直表头 125 | self.setEditTriggers(QTableWidget.NoEditTriggers) # 设为不可编辑状态 126 | self.setColumnWidth(0, 80) 127 | self.setColumnWidth(1, 800) 128 | # Qt图形组件的GUI更新必须使用Signal/Slot机制,否则有可能导致程序崩溃 129 | # 因此这里先将图形更新函数作为Slot,和信号连接起来 130 | # 然后将信号的触发函数注册到事件驱动引擎中 131 | self.signal.connect(self.updateLog) 132 | self.__eventEngine.register(EVENT_LOG, self.signal.emit) 133 | #保存日志到文件 134 | path = 'log/EventLog/eventLog{date}'.format(date=date.today()) 135 | logging.basicConfig(filename=path, level=logging.INFO) 136 | #---------------------------------------------------------------------- 137 | def updateLog(self, event): 138 | """更新日志""" 139 | # 获取当前时间和日志内容 140 | t = datetime.now() 141 | t = t.strftime('%H:%M:%S') 142 | log = event.dict_['log'] 143 | # 在表格最上方插入一行 144 | self.insertRow(0) 145 | # 创建单元格 146 | cellTime = QTableWidgetItem(t) 147 | cellLog = QTableWidgetItem(log) 148 | 149 | # 将单元格插入表格 150 | self.setItem(0, 0, cellTime) 151 | self.setItem(0, 1, cellLog) 152 | 153 | logging.info(','.join([t, log])) 154 | 155 | ######################################################################## 156 | class AccountMonitor(QTableWidget): 157 | """用于显示账户""" 158 | signal = pyqtSignal(type(Event()))# 这里的TYPE也可以是DICT,需要在注册事件中进行数据格式转换 159 | #---------------------------------------------------------------------- 160 | def __init__(self, eventEngine, parent=None): 161 | """Constructor""" 162 | super(AccountMonitor, self).__init__(parent) 163 | self.dictLabels = ["动态权益", "总保证金", "冻结保证金", "手续费", "平仓盈亏", "持仓盈亏", "可用资金", "可取资金"] 164 | self.__eventEngine = eventEngine 165 | #self.__mainEngine = mainEngine 166 | self.list_account = [] # 保存账户数据的LIST 167 | self.count = 0 # 账户数据第一次保存记号 168 | self.dict = {} # 用来保存账户对应的单元格 169 | self.setWindowTitle('账户') 170 | self.setColumnCount(len(self.dictLabels)) # 设置列 171 | self.insertRow(0) # 因为只有1行数据,直接初始化 172 | col = 0 # 表格列计数器 173 | for i in self.dictLabels: # 初始化表格为空格 174 | self.dict[i] = QTableWidgetItem('') 175 | self.dict[i].setTextAlignment(0x0004 | 0x0080) # 居中 176 | self.setItem(0, col, self.dict[i]) 177 | col += 1 178 | self.setHorizontalHeaderLabels(self.dictLabels) 179 | self.verticalHeader().setVisible(False) # 关闭左边的垂直表头 180 | self.setEditTriggers(QTableWidget.NoEditTriggers) # 设为不可编辑状态 181 | self.signal.connect(self.updateAccount) 182 | self.__eventEngine.register(EVENT_ACCOUNT, self.signal.emit) 183 | 184 | def updateAccount(self, event): 185 | var = self.TradingAccountField(event.dict_['data']) # 这里的dict'keys要包含self.dictLabels,否则会出错。 186 | self.count += 1 # 也可以每执行一次保存一次,收盘后可以看到账户的曲线。 187 | if self.count == 1: # 记录一次账户数据,只记录登陆后的第一个数据。 188 | self.list_account.append(var) # 这个代码可有可无,看个人的使用而言。 189 | for i in self.dictLabels: # 刷新表格 190 | value = var[i] # i就是DICT的key 191 | try: 192 | value = str(round(value, 2)) # 保留2位小数 193 | except: 194 | value = str(value) 195 | self.dict[i].setText(value) # 刷新单元格数据 196 | #---------------------------------------------------------------------- 197 | def TradingAccountField(self, var): 198 | tmp = {} 199 | tmp["投资者帐号"] = var["AccountID"] 200 | tmp["静态权益"] = var["PreBalance"] 201 | tmp["上次存款额"] = var["PreDeposit"] 202 | tmp["入金金额"] = var["Deposit"] 203 | tmp["出金金额"] = var["Withdraw"] 204 | tmp["冻结保证金"] = var["FrozenMargin"] 205 | tmp["总保证金"] = var["CurrMargin"] 206 | tmp["手续费"] = var["Commission"] 207 | tmp["平仓盈亏"] = var["CloseProfit"] 208 | tmp["持仓盈亏"] = var["PositionProfit"] 209 | tmp["动态权益"] = var["Balance"] 210 | tmp["可用资金"] = var["Available"] 211 | tmp["可取资金"] = var["WithdrawQuota"] 212 | tmp["交易日"] = var["TradingDay"] 213 | tmp["时间"] = datetime.now() 214 | return tmp 215 | 216 | ######################################################################## 217 | class PositionMonitor(QTableWidget): 218 | """用于显示持仓""" 219 | signal = pyqtSignal(type(Event())) 220 | #---------------------------------------------------------------------- 221 | def __init__(self, eventEngine, parent=None): 222 | """Constructor""" 223 | super(PositionMonitor, self).__init__(parent) 224 | self.dictLabels = [ 225 | "合约代码", "合约名称", "持仓方向", "总持仓量", "昨持仓量", "今持仓量", "总冻结", 226 | "持仓均价", "开仓均价", "持仓盈亏", "开仓盈亏"] 227 | self.__eventEngine = eventEngine 228 | self.dict = {} 229 | self.setWindowTitle('持仓') 230 | self.setColumnCount(len(self.dictLabels)) 231 | self.setHorizontalHeaderLabels(self.dictLabels) 232 | self.verticalHeader().setVisible(False) # 关闭左边的垂直表头 233 | self.setEditTriggers(QTableWidget.NoEditTriggers) # 设为不可编辑状态 234 | self.signal.connect(self.updateposition) 235 | self.__eventEngine.register(EVENT_VNPOSITION, self.signal.emit) 236 | self.insertRow(0) # 插入合计的表格 237 | col = 0 238 | self.dict['合计'] = {} 239 | for i in self.dictLabels: 240 | self.dict['合计'][i] = QTableWidgetItem('') 241 | self.dict['合计'][i].setTextAlignment(0x0004 | 0x0080) # 居中 242 | self.setItem(0, col, self.dict['合计'][i]) 243 | col += 1 244 | self.dict['合计']["合约代码"].setText('合计') 245 | 246 | def updateposition(self, event): 247 | pos = event.dict_['data'] 248 | last = event.dict_['last'] 249 | dm = { 250 | DIRECTION_LONG: "多持", 251 | DIRECTION_SHORT: "空持", 252 | } 253 | 254 | posName = '.'.join([pos.symbol + pos.direction]) 255 | if pos.position != 0: # 有持仓 256 | if posName not in self.dict.keys(): # 插入新持仓 257 | self.dict[posName] = {} 258 | self.dict[posName]["合约代码"] = QTableWidgetItem(str(pos.symbol)) 259 | self.dict[posName]["合约名称"] = QTableWidgetItem(str(pos.name)) 260 | self.dict[posName]["持仓方向"] = QTableWidgetItem(str(dm.get(pos.direction, '未知方向'))) 261 | self.dict[posName]["总持仓量"] = QTableWidgetItem(str(pos.position)) 262 | self.dict[posName]["昨持仓量"] = QTableWidgetItem(str(pos.ydPosition)) 263 | self.dict[posName]["今持仓量"] = QTableWidgetItem(str(pos.position - pos.ydPosition)) 264 | self.dict[posName]["总冻结"] = QTableWidgetItem(str(pos.frozen)) 265 | self.dict[posName]["持仓均价"] = QTableWidgetItem(str(round(pos.price, 2))) 266 | self.dict[posName]["开仓均价"] = QTableWidgetItem(str(round(pos.openPrice, 2))) 267 | self.dict[posName]["持仓盈亏"] = QTableWidgetItem(str(round(pos.positionProfit, 2))) 268 | self.dict[posName]["开仓盈亏"] = QTableWidgetItem(str(round(pos.openProfit, 2))) 269 | self.insertRow(0) # 插入表格第一行 270 | col = 0 # 列计数 271 | for label in self.dictLabels: 272 | self.dict[posName][label].setTextAlignment(0x0004 | 0x0080) # 居中 273 | self.setItem(0, col, self.dict[posName][label]) 274 | col += 1 275 | else: # 更新可能会变的数值 276 | self.dict[posName]["总持仓量"].setText(str(pos.position)) 277 | self.dict[posName]["昨持仓量"].setText(str(pos.ydPosition)) 278 | self.dict[posName]["今持仓量"].setText(str(pos.position - pos.ydPosition)) 279 | self.dict[posName]["总冻结"].setText(str(pos.frozen)) 280 | self.dict[posName]["持仓均价"].setText(str(round(pos.price, 2))) 281 | self.dict[posName]["开仓均价"].setText(str(round(pos.openPrice, 2))) 282 | self.dict[posName]["持仓盈亏"].setText(str(round(pos.positionProfit, 2))) 283 | self.dict[posName]["开仓盈亏"].setText(str(round(pos.openProfit, 2))) 284 | # 设置颜色 285 | if pos.direction == DIRECTION_LONG: 286 | self.dict[posName]["持仓方向"].setBackground(QColor(255, 0, 0)) 287 | else: 288 | self.dict[posName]["持仓方向"].setBackground(QColor(34, 139, 34)) 289 | if pos.positionProfit > 0: 290 | self.dict[posName]["持仓盈亏"].setBackground(QColor(255, 0, 0)) 291 | else: 292 | self.dict[posName]["持仓盈亏"].setBackground(QColor(34, 139, 34)) 293 | if pos.openProfit > 0 : 294 | self.dict[posName]["开仓盈亏"].setBackground(QColor(255, 0, 0)) 295 | else: 296 | self.dict[posName]["开仓盈亏"].setBackground(QColor(34, 139, 34)) 297 | 298 | else: # 无持仓 299 | if posName in self.dict.keys(): 300 | del self.dict[posName] 301 | r = self.rowCount() 302 | for i in range(r): 303 | row = r - 1 - i 304 | if (self.item(row, 0).text() == str(pos.symbol)) and (self.item(row, 2).text() == dm[pos.direction]): 305 | self.removeRow(row) # 删除表格 306 | 307 | if last: # 处理合计表格 308 | row = self.rowCount() 309 | p = {} 310 | p["总持仓量"] = 0 311 | p["昨持仓量"] = 0 312 | p["今持仓量"] = 0 313 | p["总冻结"] = 0 314 | p["持仓盈亏"] = float(0) 315 | p["开仓盈亏"] = float(0) 316 | 317 | for i in range(row - 1): 318 | p["总持仓量"] += int(self.item(i, 3).text()) 319 | p["昨持仓量"] += int(self.item(i, 4).text()) 320 | p["今持仓量"] += int(self.item(i, 5).text()) 321 | p["总冻结"] += int(self.item(i, 6).text()) 322 | p["持仓盈亏"] += float(self.item(i, 9).text()) 323 | p["开仓盈亏"] += float(self.item(i, 10).text()) 324 | self.dict['合计']['总持仓量'].setText(str(p["总持仓量"])) 325 | self.dict['合计']['昨持仓量'].setText(str(p["昨持仓量"])) 326 | self.dict['合计']['今持仓量'].setText(str(p["今持仓量"])) 327 | self.dict['合计']['总冻结'].setText(str(p["总冻结"])) 328 | self.dict['合计']['持仓盈亏'].setText(str(round(p["持仓盈亏"],2))) 329 | self.dict['合计']['开仓盈亏'].setText(str(round(p["开仓盈亏"],2))) 330 | 331 | ######################################################################## 332 | class TradeMonitor(QTableWidget): 333 | """用于显示成交记录""" 334 | signal = pyqtSignal(type(Event())) 335 | #---------------------------------------------------------------------- 336 | def __init__(self, eventEngine, parent=None): 337 | """Constructor""" 338 | super(TradeMonitor, self).__init__(parent) 339 | self.dictLabels = ["合约名称","合约代码", "买卖标志","成交时间", "价格", "数量", "成交编号", "报单引用", 340 | "报单编号","本地报单编号"] 341 | self.__eventEngine = eventEngine 342 | self.setWindowTitle('成交') 343 | self.c = len(self.dictLabels) 344 | self.setColumnCount(self.c) 345 | self.setHorizontalHeaderLabels(self.dictLabels) 346 | self.verticalHeader().setVisible(False) # 关闭左边的垂直表头 347 | self.setEditTriggers(QTableWidget.NoEditTriggers) # 设为不可编辑状态 348 | self.signal.connect(self.updateTrade) 349 | self.__eventEngine.register(EVENT_TRADE, self.signal.emit) 350 | 351 | 352 | #---------------------------------------------------------------------- 353 | def updateTrade(self, event): 354 | """""" 355 | var = event.dict_['data'] 356 | data = self.TradeField(event.dict_['data']) 357 | 358 | self.insertRow(0) 359 | for i in range(4, self.c): 360 | value = str(data[self.dictLabels[i]]) 361 | item = QTableWidgetItem(value) 362 | self.setItem(0, i, item) 363 | self.setItem(0, 1, QTableWidgetItem(data["合约代码"])) 364 | self.setItem(0, 0, QTableWidgetItem(var["InstrumentName"])) 365 | 366 | if data["开平标志"] == OFFSET_OPEN: 367 | kp = '多开' if data['买卖方向'] == DIRECTION_LONG else '空开' 368 | else: 369 | kp = '空平' if data['买卖方向'] == DIRECTION_LONG else '多平' 370 | 371 | self.setItem(0, 2, QTableWidgetItem(kp)) 372 | 373 | t = data['成交日期'] 374 | value = t[:4] +'-'+t[4:6] +'-'+t[6:8] +' ' + data["成交时间"] 375 | self.setItem(0, 3, QTableWidgetItem(value)) 376 | self.setColumnWidth(3, 150) 377 | 378 | def TradeField(self, var): 379 | tmp = {} 380 | tmp["合约代码"] = var["InstrumentID"] 381 | tmp["报单引用"] = var["OrderRef"] 382 | tmp["交易所代码"] = var["ExchangeID"] 383 | tmp["成交编号"] = var["TradeID"] 384 | tmp["买卖方向"] = var["Direction"] 385 | tmp["报单编号"] = var["OrderSysID"] 386 | tmp["合约在交易所的代码"] = var["ExchangeInstID"] 387 | tmp["开平标志"] = var["OffsetFlag"] 388 | tmp["价格"] = var["Price"] 389 | tmp["数量"] = var["Volume"] 390 | tmp["成交日期"] = var["TradeDate"] 391 | tmp["成交时间"] = var["TradeTime"] 392 | tmp["本地报单编号"] = var["OrderLocalID"] 393 | tmp["交易日"] = var["TradingDay"] 394 | return tmp 395 | 396 | ######################################################################## 397 | class OrderMonitor(QTableWidget): 398 | """用于显示所有报单""" 399 | signal = pyqtSignal(type(Event())) 400 | #---------------------------------------------------------------------- 401 | def __init__(self, eventEngine, parent=None): 402 | """Constructor""" 403 | super(OrderMonitor, self).__init__(parent) 404 | self.__eventEngine = eventEngine 405 | self.dictLabels = ["报单日期","合约代码","状态信息", "买卖开平标志", "价格", "数量","今成交数量", 406 | "剩余数量", "前置编号", "会话编号", "报单引用","本地报单编号","报单编号",] 407 | self.dict = {} # 用来保存报单号对应的单元格对象 408 | 409 | self.setWindowTitle('报单') 410 | self.setColumnCount(len(self.dictLabels)) 411 | self.setHorizontalHeaderLabels(self.dictLabels) 412 | self.verticalHeader().setVisible(False) # 关闭左边的垂直表头 413 | self.setEditTriggers(QTableWidget.NoEditTriggers) # 设为不可编辑状态 414 | self.signal.connect(self.updateOrder) 415 | self.__eventEngine.register(EVENT_ORDER, self.signal.emit) 416 | #---------------------------------------------------------------------- 417 | def updateOrder(self, event): 418 | """""" 419 | var = event.dict_['data'] 420 | index = str(var["OrderLocalID"])+'.'+var["InstrumentID"] 421 | if index not in self.dict.keys(): 422 | self.insertRow(0) 423 | self.dict[index] = {} 424 | self.dict[index]["合约代码"] = QTableWidgetItem (str(var["InstrumentID"])) 425 | self.dict[index]["报单引用"] = QTableWidgetItem(str(var["OrderRef"])) 426 | self.dict[index]["价格"] = QTableWidgetItem(str(var["LimitPrice"])) 427 | self.dict[index]["数量"] = QTableWidgetItem(str(var["VolumeTotalOriginal"])) 428 | self.dict[index]["本地报单编号"] = QTableWidgetItem(str(var["OrderLocalID"])) 429 | self.dict[index]["今成交数量"] = QTableWidgetItem(str(var["VolumeTraded"])) 430 | self.dict[index]["剩余数量"] = QTableWidgetItem(str(var["VolumeTotal"])) 431 | self.dict[index]["报单编号"] = QTableWidgetItem(str(var["OrderSysID"])) 432 | self.dict[index]["前置编号"] = QTableWidgetItem(str(var["FrontID"])) 433 | self.dict[index]["会话编号"] = QTableWidgetItem(str(var["SessionID"])) 434 | self.dict[index]["状态信息"] = QTableWidgetItem(str(var["OrderStatus"])) 435 | t =str(var["InsertDate"]) + ' '+str(var["InsertTime"]) 436 | self.dict[index]["报单日期"] = QTableWidgetItem(str(t)) 437 | if var["CombOffsetFlag"] == OFFSET_OPEN: 438 | if var["Direction"] == DIRECTION_LONG: 439 | self.dict[index]["买卖开平标志"] = QTableWidgetItem('多开') 440 | kp = '多开' #方便log 441 | else: 442 | self.dict[index]["买卖开平标志"] = QTableWidgetItem('空开') 443 | kp = '空开' #方便log 444 | else: 445 | if var["Direction"] == DIRECTION_SHORT: 446 | self.dict[index]["买卖开平标志"] = QTableWidgetItem('多平') 447 | kp = '多平' #方便log 448 | else: 449 | self.dict[index]["买卖开平标志"] = QTableWidgetItem('空平') 450 | kp = '空平' #方便log 451 | col =0 452 | for i in self.dictLabels : 453 | self.setItem(0,col,self.dict[index][i]) 454 | col +=1 455 | self.setColumnWidth(0,150) 456 | self.setColumnWidth(2, 120) 457 | 458 | """ 459 | log首次报单,由于每次更新状态都有order事件,后面的不更新 460 | """ 461 | log = u'委托回报:%s,方向:%s,价格:%d,数量:%d,已成交:%d,未成交:%d,状态:%s' %(var["InstrumentID"], kp, var["LimitPrice"], var["VolumeTotalOriginal"], var["VolumeTraded"], var["VolumeTotal"], var["StatusMsg"]) 462 | event = Event(type_=EVENT_LOG) 463 | event.dict_['log'] = log 464 | self.__eventEngine.put(event) 465 | 466 | else:#更新可能变化的数据 467 | self.dict[index]["本地报单编号"] .setText(str(var["OrderLocalID"])) 468 | self.dict[index]["今成交数量"] .setText(str(var["VolumeTraded"])) 469 | self.dict[index]["剩余数量"] .setText(str(var["VolumeTotal"])) 470 | self.dict[index]["报单编号"] .setText(str(var["OrderSysID"])) 471 | self.dict[index]["前置编号"] .setText(str(var["FrontID"])) 472 | self.dict[index]["会话编号"] .setText(str(var["SessionID"])) 473 | self.dict[index]["状态信息"] .setText(str(var["OrderStatus"])) 474 | 475 | 476 | 477 | ######################################################################## 478 | class NonetradeMonitor(QTableWidget): 479 | """用于未成交报单""" 480 | signal = pyqtSignal(type(Event())) 481 | FINISHED_STATUS = [STATUS_ALLTRADED, STATUS_REJECTED, STATUS_CANCELLED] 482 | #---------------------------------------------------------------------- 483 | def __init__(self, eventEngine,mainEngine, parent=None): 484 | """Constructor""" 485 | super(NonetradeMonitor, self).__init__(parent) 486 | self.__eventEngine = eventEngine 487 | self.__mainEngine = mainEngine 488 | self.dictLabels = ["报单日期", "交易所代码", "合约代码", "买卖开平标志", "价格", "数量", "今成交数量", "剩余数量", "状态信息", "本地报单编号", "报单编号", "报单引用", "前置编号", "会话编号"] 489 | self.L=len(self.dictLabels ) 490 | #self.orderref= {} # 用来保存报单号 491 | self.dict = {} # 用来保存报单数据 492 | 493 | self.setWindowTitle('未成交') 494 | self.setColumnCount(len(self.dictLabels)) 495 | self.setHorizontalHeaderLabels(self.dictLabels) 496 | self.verticalHeader().setVisible(False) # 关闭左边的垂直表头 497 | self.setEditTriggers(QTableWidget.NoEditTriggers) # 设为不可编辑状态 498 | self.signal.connect(self.updateOrder) 499 | self.__eventEngine.register(EVENT_ORDER, self.signal.emit) 500 | self.itemDoubleClicked.connect(self.cancelOrder) 501 | 502 | def cancelOrder(self): 503 | """撤单""" 504 | req = CtaCancelOrderReq() 505 | 506 | req.symbol = self.item(self.currentRow(),2).text() # 代码 507 | req.exchange = self.item(self.currentRow(),1).text() # 交易所 508 | req.orderID = self.item(self.currentRow(),11).text() # 报单引用 509 | req.frontID = self.item(self.currentRow(),12).text() # 前置机号 510 | req.sessionID = self.item(self.currentRow(),13).text() # 会话号 511 | req.OrderSysID = self.item(self.currentRow(),10).text() #报单编号 512 | 513 | 514 | self.__mainEngine.cancelOrder(req) 515 | #---------------------------------------------------------------------- 516 | def updateOrder(self, event): 517 | """更新可撤单""" 518 | var = event.dict_['data'] 519 | index = str(var["OrderSysID"])+'.'+var["InstrumentID"] 520 | 521 | if index not in self.dict.keys() and var["OrderStatus"] == STATUS_NOTTRADED: 522 | self.insertRow(0) 523 | self.dict[index] = {} 524 | self.dict[index]["合约代码"] = QTableWidgetItem (str(var["InstrumentID"])) 525 | self.dict[index]["报单引用"] = QTableWidgetItem(str(var["OrderRef"])) 526 | self.dict[index]["价格"] = QTableWidgetItem(str(var["LimitPrice"])) 527 | self.dict[index]["数量"] = QTableWidgetItem(str(var["VolumeTotalOriginal"])) 528 | self.dict[index]["本地报单编号"] = QTableWidgetItem(str(var["OrderLocalID"])) 529 | self.dict[index]["今成交数量"] = QTableWidgetItem(str(var["VolumeTraded"])) 530 | self.dict[index]["剩余数量"] = QTableWidgetItem(str(var["VolumeTotal"])) 531 | self.dict[index]["报单编号"] = QTableWidgetItem(str(var["OrderSysID"])) 532 | self.dict[index]["前置编号"] = QTableWidgetItem(str(var["FrontID"])) 533 | self.dict[index]["会话编号"] = QTableWidgetItem(str(var["SessionID"])) 534 | self.dict[index]["状态信息"] = QTableWidgetItem(str(var["OrderStatus"])) 535 | self.dict[index]["交易所代码"] = QTableWidgetItem(str(var["ExchangeID"])) 536 | t =str(var["InsertDate"]) + ' '+str(var["InsertTime"]) 537 | self.dict[index]["报单日期"] = QTableWidgetItem(str(t)) 538 | if var["CombOffsetFlag"] == OFFSET_OPEN: 539 | if var["Direction"] == DIRECTION_LONG: 540 | self.dict[index]["买卖开平标志"] = QTableWidgetItem('多开') 541 | else: 542 | self.dict[index]["买卖开平标志"] = QTableWidgetItem('空开') 543 | else: 544 | if var["Direction"] == DIRECTION_SHORT: 545 | self.dict[index]["买卖开平标志"] = QTableWidgetItem('多平') 546 | else: 547 | self.dict[index]["买卖开平标志"] = QTableWidgetItem('空平') 548 | col =0 549 | for i in self.dictLabels : 550 | self.setItem(0,col,self.dict[index][i]) 551 | col +=1 552 | self.setColumnWidth(0,120) 553 | self.setColumnWidth(2, 120) 554 | 555 | 556 | 557 | if index in self.dict.keys(): # 撤单 558 | 559 | self.dict[index]["状态信息"].setText(str(var["OrderStatus"])) 560 | if var['OrderStatus'] in self.FINISHED_STATUS: 561 | r = self.rowCount() 562 | for i in range(r): 563 | j = r-1-i 564 | if self.item(j,10).text() == str(var["OrderSysID"]): 565 | self.removeRow(j) 566 | 567 | 568 | ######################################################################## 569 | class RmSpinBox(QSpinBox): 570 | """调整参数用的数值框""" 571 | 572 | #---------------------------------------------------------------------- 573 | def __init__(self, value): 574 | """Constructor""" 575 | super(RmSpinBox, self).__init__() 576 | 577 | self.setMinimum(0) 578 | self.setMaximum(1000000) 579 | 580 | self.setValue(value) 581 | 582 | ######################################################################## 583 | class RmLine(QFrame): 584 | """水平分割线""" 585 | 586 | #---------------------------------------------------------------------- 587 | def __init__(self): 588 | """Constructor""" 589 | super(RmLine, self).__init__() 590 | self.setFrameShape(self.HLine) 591 | self.setFrameShadow(self.Sunken) 592 | 593 | ######################################################################## 594 | class RmEngineManager(QWidget): 595 | """风控引擎的管理组件""" 596 | 597 | #---------------------------------------------------------------------- 598 | def __init__(self, rmEngine, eventEngine, parent=None): 599 | """Constructor""" 600 | super(RmEngineManager, self).__init__(parent) 601 | 602 | self.rmEngine = rmEngine 603 | self.eventEngine = eventEngine 604 | 605 | self.initUi() 606 | self.updateEngineStatus() 607 | 608 | #---------------------------------------------------------------------- 609 | def initUi(self): 610 | """初始化界面""" 611 | self.setWindowTitle(u'风险管理') 612 | 613 | # 设置界面 614 | self.buttonSwitchEngineStatus = QPushButton(u'风控模块运行中') 615 | 616 | self.spinOrderFlowLimit = RmSpinBox(self.rmEngine.orderFlowLimit) 617 | self.spinOrderFlowClear = RmSpinBox(self.rmEngine.orderFlowClear) 618 | self.spinOrderSizeLimit = RmSpinBox(self.rmEngine.orderSizeLimit) 619 | self.spinTradeLimit = RmSpinBox(self.rmEngine.tradeLimit) 620 | #self.spinWorkingOrderLimit = RmSpinBox(self.rmEngine.workingOrderLimit) 621 | 622 | buttonClearOrderFlowCount = QPushButton(u'清空流控计数') 623 | buttonClearTradeCount = QPushButton(u'清空总成交计数') 624 | buttonSaveSetting = QPushButton(u'保存设置') 625 | 626 | Label = QLabel 627 | grid = QGridLayout() 628 | grid.addWidget(Label(u'工作状态'), 0, 0) 629 | grid.addWidget(self.buttonSwitchEngineStatus, 0, 1) 630 | grid.addWidget(RmLine(), 1, 0, 1, 2) 631 | grid.addWidget(Label(u'流控上限'), 2, 0) 632 | grid.addWidget(self.spinOrderFlowLimit, 2, 1) 633 | grid.addWidget(Label(u'流控清空(秒)'), 3, 0) 634 | grid.addWidget(self.spinOrderFlowClear, 3, 1) 635 | grid.addWidget(RmLine(), 4, 0, 1, 2) 636 | grid.addWidget(Label(u'单笔委托上限'), 5, 0) 637 | grid.addWidget(self.spinOrderSizeLimit, 5, 1) 638 | grid.addWidget(RmLine(), 6, 0, 1, 2) 639 | grid.addWidget(Label(u'总成交上限'), 7, 0) 640 | grid.addWidget(self.spinTradeLimit, 7, 1) 641 | grid.addWidget(RmLine(), 8, 0, 1, 2) 642 | #grid.addWidget(Label(u'活动订单上限'), 9, 0) 643 | #grid.addWidget(self.spinWorkingOrderLimit, 9, 1) 644 | 645 | hbox = QHBoxLayout() 646 | hbox.addWidget(buttonClearOrderFlowCount) 647 | hbox.addWidget(buttonClearTradeCount) 648 | hbox.addStretch() 649 | hbox.addWidget(buttonSaveSetting) 650 | 651 | vbox = QVBoxLayout() 652 | vbox.addLayout(grid) 653 | vbox.addLayout(hbox) 654 | self.setLayout(vbox) 655 | 656 | # 连接组件信号 657 | self.spinOrderFlowLimit.valueChanged.connect(self.rmEngine.setOrderFlowLimit) 658 | self.spinOrderFlowClear.valueChanged.connect(self.rmEngine.setOrderFlowClear) 659 | self.spinOrderSizeLimit.valueChanged.connect(self.rmEngine.setOrderSizeLimit) 660 | self.spinTradeLimit.valueChanged.connect(self.rmEngine.setTradeLimit) 661 | #self.spinWorkingOrderLimit.valueChanged.connect(self.rmEngine.setWorkingOrderLimit) 662 | 663 | self.buttonSwitchEngineStatus.clicked.connect(self.switchEngineSatus) 664 | buttonClearOrderFlowCount.clicked.connect(self.rmEngine.clearOrderFlowCount) 665 | buttonClearTradeCount.clicked.connect(self.rmEngine.clearTradeCount) 666 | buttonSaveSetting.clicked.connect(self.rmEngine.saveSetting) 667 | 668 | # 设为固定大小 669 | self.setFixedSize(self.sizeHint()) 670 | 671 | #---------------------------------------------------------------------- 672 | def switchEngineSatus(self): 673 | """控制风控引擎开关""" 674 | self.rmEngine.switchEngineStatus() 675 | self.updateEngineStatus() 676 | 677 | #---------------------------------------------------------------------- 678 | def updateEngineStatus(self): 679 | """更新引擎状态""" 680 | if self.rmEngine.active: 681 | self.buttonSwitchEngineStatus.setText(u'风控模块运行中') 682 | else: 683 | self.buttonSwitchEngineStatus.setText(u'风控模块未启动') 684 | 685 | ######################################################################## 686 | class CtaValueMonitor(QTableWidget): 687 | """参数监控""" 688 | 689 | #---------------------------------------------------------------------- 690 | def __init__(self, parent=None): 691 | """Constructor""" 692 | super(CtaValueMonitor, self).__init__(parent) 693 | 694 | self.keyCellDict = {} 695 | self.data = None 696 | self.inited = False 697 | 698 | self.initUi() 699 | 700 | #---------------------------------------------------------------------- 701 | def initUi(self): 702 | """初始化界面""" 703 | self.setRowCount(1) 704 | self.verticalHeader().setVisible(False) 705 | self.setEditTriggers(self.NoEditTriggers) 706 | 707 | self.setMaximumHeight(self.sizeHint().height()) 708 | 709 | #---------------------------------------------------------------------- 710 | def updateData(self, data): 711 | """更新数据""" 712 | if not self.inited: 713 | self.setColumnCount(len(data)) 714 | self.setHorizontalHeaderLabels(data.keys()) 715 | 716 | col = 0 717 | for k, v in data.items(): 718 | # cell = QTableWidgetItem(unicode(v)) 719 | cell = QTableWidgetItem(str(v)) 720 | self.keyCellDict[k] = cell 721 | self.setItem(0, col, cell) 722 | col += 1 723 | 724 | self.inited = True 725 | else: 726 | for k, v in data.items(): 727 | cell = self.keyCellDict[k] 728 | # cell.setText(unicode(v)) 729 | cell.setText(str(v)) 730 | 731 | ######################################################################## 732 | class CtaStrategyManager(QGroupBox): 733 | """策略管理组件""" 734 | signal = pyqtSignal(type(Event())) 735 | 736 | #---------------------------------------------------------------------- 737 | def __init__(self, ctaEngine, eventEngine, name, parent=None): 738 | """Constructor""" 739 | super(CtaStrategyManager, self).__init__(parent) 740 | 741 | self.ctaEngine = ctaEngine 742 | self.eventEngine = eventEngine 743 | self.name = name 744 | 745 | self.initUi() 746 | self.updateMonitor() 747 | self.registerEvent() 748 | 749 | #---------------------------------------------------------------------- 750 | def initUi(self): 751 | """初始化界面""" 752 | self.setTitle(self.name) 753 | 754 | self.paramMonitor = CtaValueMonitor(self) 755 | self.varMonitor = CtaValueMonitor(self) 756 | 757 | height = 65 758 | self.paramMonitor.setFixedHeight(height) 759 | self.varMonitor.setFixedHeight(height) 760 | 761 | buttonInit = QPushButton(INIT) 762 | buttonStart = QPushButton(START) 763 | buttonStop = QPushButton(STOP) 764 | buttonInit.clicked.connect(self.init) 765 | buttonStart.clicked.connect(self.start) 766 | buttonStop.clicked.connect(self.stop) 767 | 768 | hbox1 = QHBoxLayout() 769 | hbox1.addWidget(buttonInit) 770 | hbox1.addWidget(buttonStart) 771 | hbox1.addWidget(buttonStop) 772 | hbox1.addStretch() 773 | 774 | hbox2 = QHBoxLayout() 775 | hbox2.addWidget(self.paramMonitor) 776 | 777 | hbox3 = QHBoxLayout() 778 | hbox3.addWidget(self.varMonitor) 779 | 780 | vbox = QVBoxLayout() 781 | vbox.addLayout(hbox1) 782 | vbox.addLayout(hbox2) 783 | vbox.addLayout(hbox3) 784 | 785 | self.setLayout(vbox) 786 | 787 | #---------------------------------------------------------------------- 788 | def updateMonitor(self, event=None): 789 | """显示策略最新状态""" 790 | paramDict = self.ctaEngine.getStrategyParam(self.name) 791 | if paramDict: 792 | self.paramMonitor.updateData(paramDict) 793 | 794 | varDict = self.ctaEngine.getStrategyVar(self.name) 795 | if varDict: 796 | self.varMonitor.updateData(varDict) 797 | 798 | #---------------------------------------------------------------------- 799 | def registerEvent(self): 800 | """注册事件监听""" 801 | self.signal.connect(self.updateMonitor) 802 | self.eventEngine.register(EVENT_CTA_STRATEGY+self.name, self.signal.emit) 803 | 804 | #---------------------------------------------------------------------- 805 | def init(self): 806 | """初始化策略""" 807 | self.ctaEngine.initStrategy(self.name) 808 | 809 | #---------------------------------------------------------------------- 810 | def start(self): 811 | """启动策略""" 812 | self.ctaEngine.startStrategy(self.name) 813 | 814 | #---------------------------------------------------------------------- 815 | def stop(self): 816 | """停止策略""" 817 | self.ctaEngine.stopStrategy(self.name) 818 | 819 | 820 | ######################################################################## 821 | class CtaEngineManager(QWidget): 822 | """CTA引擎管理组件""" 823 | signal = pyqtSignal(type(Event())) 824 | 825 | #---------------------------------------------------------------------- 826 | def __init__(self, eventEngine, ctaEngine, parent=None): 827 | """Constructor""" 828 | super(CtaEngineManager, self).__init__(parent) 829 | 830 | self.ctaEngine = ctaEngine 831 | self.eventEngine = eventEngine 832 | 833 | self.strategyLoaded = False 834 | 835 | self.initUi() 836 | self.registerEvent() 837 | 838 | # 记录日志 839 | self.ctaEngine.writeCtaLog(CTA_ENGINE_STARTED) 840 | 841 | #---------------------------------------------------------------------- 842 | def initUi(self): 843 | """初始化界面""" 844 | self.setWindowTitle(CTA_STRATEGY) 845 | 846 | # 按钮 847 | loadButton = QPushButton(LOAD_STRATEGY) 848 | initAllButton = QPushButton(INIT_ALL) 849 | startAllButton = QPushButton(START_ALL) 850 | stopAllButton = QPushButton(STOP_ALL) 851 | 852 | loadButton.clicked.connect(self.load) 853 | initAllButton.clicked.connect(self.initAll) 854 | startAllButton.clicked.connect(self.startAll) 855 | stopAllButton.clicked.connect(self.stopAll) 856 | 857 | # 滚动区域,放置所有的CtaStrategyManager 858 | self.scrollArea = QScrollArea() 859 | self.scrollArea.setWidgetResizable(True) 860 | 861 | # CTA组件的日志监控 862 | # self.ctaLogMonitor = QTextEdit() 863 | # self.ctaLogMonitor.setReadOnly(True) 864 | # self.ctaLogMonitor.setMaximumHeight(200) 865 | 866 | # 设置布局 867 | hbox2 = QHBoxLayout() 868 | hbox2.addWidget(loadButton) 869 | hbox2.addWidget(initAllButton) 870 | hbox2.addWidget(startAllButton) 871 | hbox2.addWidget(stopAllButton) 872 | hbox2.addStretch() 873 | 874 | vbox = QVBoxLayout() 875 | vbox.addLayout(hbox2) 876 | vbox.addWidget(self.scrollArea) 877 | # vbox.addWidget(self.ctaLogMonitor) 878 | self.setLayout(vbox) 879 | 880 | #---------------------------------------------------------------------- 881 | def initStrategyManager(self): 882 | """初始化策略管理组件界面""" 883 | w = QWidget() 884 | vbox = QVBoxLayout() 885 | 886 | for name in self.ctaEngine.strategyDict.keys(): 887 | strategyManager = CtaStrategyManager(self.ctaEngine, self.eventEngine, name) 888 | vbox.addWidget(strategyManager) 889 | 890 | vbox.addStretch() 891 | 892 | w.setLayout(vbox) 893 | self.scrollArea.setWidget(w) 894 | 895 | #---------------------------------------------------------------------- 896 | def initAll(self): 897 | """全部初始化""" 898 | self.ctaEngine.initAll() 899 | 900 | #---------------------------------------------------------------------- 901 | def startAll(self): 902 | """全部启动""" 903 | self.ctaEngine.startAll() 904 | 905 | #---------------------------------------------------------------------- 906 | def stopAll(self): 907 | """全部停止""" 908 | self.ctaEngine.stopAll() 909 | 910 | #---------------------------------------------------------------------- 911 | def load(self): 912 | """加载策略""" 913 | if not self.strategyLoaded: 914 | self.ctaEngine.loadSetting() 915 | self.initStrategyManager() 916 | self.strategyLoaded = True 917 | self.ctaEngine.writeCtaLog(STRATEGY_LOADED) 918 | 919 | #---------------------------------------------------------------------- 920 | # def updateCtaLog(self, event): 921 | # """更新CTA相关日志""" 922 | # log = event.dict_['data'] 923 | # content = '\t'.join([log.logTime, log.logContent]) 924 | # self.ctaLogMonitor.append(content) 925 | def robot(self): 926 | '''自动载入、初始化、启动''' 927 | self.load() 928 | self.initAll() 929 | self.startAll() 930 | #---------------------------------------------------------------------- 931 | def registerEvent(self): 932 | """注册事件监听""" 933 | # self.signal.connect(self.updateCtaLog) 934 | # self.eventEngine.register(EVENT_CTA_LOG, self.signal.emit) 935 | self.signal.connect(self.robot) 936 | self.eventEngine.register(EVENT_CTA_ROBOT, self.signal.emit) 937 | 938 | ####################################################################### 939 | class AboutWidget(QDialog): 940 | """显示关于信息""" 941 | 942 | #---------------------------------------------------------------------- 943 | def __init__(self, parent=None): 944 | """Constructor""" 945 | super(AboutWidget, self).__init__(parent) 946 | 947 | self.initUi() 948 | 949 | #---------------------------------------------------------------------- 950 | def initUi(self): 951 | """""" 952 | self.setWindowTitle(u'关于刀削面') 953 | 954 | text = u""" 955 | Developed by vvipi. 956 | 957 | 感谢VNPY! 958 | 959 | 感谢PYCTP! 960 | 961 | 感谢何先生的封装教程! 962 | 963 | 本交易助手是为刀削面定制的自动交易终端 964 | 交易策略采用土鳖交易法则 965 | 966 | 八月,是期货投机最危险的月份,同样危险的月份还有: 967 | 二月、十月、七月、三月、六月、一月、十二月、十一月、五月、九月、四月。 968 | 969 | 刀削面加油! 970 | 971 | 2017.8 972 | 973 | """ 974 | 975 | label = QLabel() 976 | label.setText(text) 977 | label.setMinimumWidth(500) 978 | 979 | vbox = QVBoxLayout() 980 | vbox.addWidget(label) 981 | 982 | self.setLayout(vbox) -------------------------------------------------------------------------------- /py3_ctp_demo_on_vnpy/modules/vnctpmd.pyd: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vvipi/py3_demo_on_vnpy_ctp/491f6394ce049a7dd93a34415cd2ffe499bf9ffa/py3_ctp_demo_on_vnpy/modules/vnctpmd.pyd -------------------------------------------------------------------------------- /py3_ctp_demo_on_vnpy/modules/vnctptd.pyd: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vvipi/py3_demo_on_vnpy_ctp/491f6394ce049a7dd93a34415cd2ffe499bf9ffa/py3_ctp_demo_on_vnpy/modules/vnctptd.pyd -------------------------------------------------------------------------------- /py3_ctp_demo_on_vnpy/resource/fs.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vvipi/py3_demo_on_vnpy_ctp/491f6394ce049a7dd93a34415cd2ffe499bf9ffa/py3_ctp_demo_on_vnpy/resource/fs.ico -------------------------------------------------------------------------------- /py3_ctp_demo_on_vnpy/setting/CTA_setting.json: -------------------------------------------------------------------------------- 1 | 2 | [ 3 | { 4 | "name": "strategy Random", 5 | "className": "StrategyRandom", 6 | "vtSymbol": "ni1805", 7 | "size": 500, 8 | "volume": 1, 9 | "priceTick": 10 10 | } 11 | ] -------------------------------------------------------------------------------- /py3_ctp_demo_on_vnpy/setting/syncdata/deleteme: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vvipi/py3_demo_on_vnpy_ctp/491f6394ce049a7dd93a34415cd2ffe499bf9ffa/py3_ctp_demo_on_vnpy/setting/syncdata/deleteme -------------------------------------------------------------------------------- /py3_ctp_demo_on_vnpy/setting/user.json: -------------------------------------------------------------------------------- 1 | { 2 | "userID": "", 3 | "password": "", 4 | "brokerID": "9999", 5 | "MdIp": "tcp://180.168.146.187:13040", 6 | "TdIp": "tcp://180.168.146.187:13030", 7 | "authCode":"0000000000000000", 8 | "appID": "client_xxxx_1.0.1", 9 | "userProductInfo":"" 10 | } -------------------------------------------------------------------------------- /py3_ctp_demo_on_vnpy/strategy/__init__.py: -------------------------------------------------------------------------------- 1 | # encoding: UTF-8 2 | 3 | ''' 4 | 动态载入所有的策略类 5 | ''' 6 | 7 | import os 8 | import importlib 9 | import traceback 10 | 11 | # 用来保存策略类的字典 12 | STRATEGY_CLASS = {} 13 | 14 | #---------------------------------------------------------------------- 15 | def loadStrategyModule(moduleName): 16 | """使用importlib动态载入模块""" 17 | try: 18 | module = importlib.import_module(moduleName) 19 | 20 | # 遍历模块下的对象,只有名称中包含'Strategy'的才是策略类 21 | for k in dir(module): 22 | if 'Strategy' in k: 23 | v = module.__getattribute__(k) 24 | STRATEGY_CLASS[k] = v 25 | except: 26 | print ('-' * 20) 27 | print ('Failed to import strategy file %s:' %moduleName) 28 | traceback.print_exc() 29 | 30 | 31 | # 遍历strategy目录下的文件 32 | path = os.path.abspath(os.path.dirname(__file__)) 33 | for root, subdirs, files in os.walk(path): 34 | for name in files: 35 | # 只有文件名中包含strategy且非.pyc的文件,才是策略文件 36 | if 'strategy' in name and '.pyc' not in name: 37 | # 模块名称需要模块路径前缀 38 | moduleName = 'strategy.' + name.replace('.py', '') 39 | loadStrategyModule(moduleName) 40 | 41 | # print(STRATEGY_CLASS) -------------------------------------------------------------------------------- /py3_ctp_demo_on_vnpy/strategy/__pycache__/__init__.cpython-36.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vvipi/py3_demo_on_vnpy_ctp/491f6394ce049a7dd93a34415cd2ffe499bf9ffa/py3_ctp_demo_on_vnpy/strategy/__pycache__/__init__.cpython-36.pyc -------------------------------------------------------------------------------- /py3_ctp_demo_on_vnpy/strategy/__pycache__/strategyFlashStalker.cpython-36.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vvipi/py3_demo_on_vnpy_ctp/491f6394ce049a7dd93a34415cd2ffe499bf9ffa/py3_ctp_demo_on_vnpy/strategy/__pycache__/strategyFlashStalker.cpython-36.pyc -------------------------------------------------------------------------------- /py3_ctp_demo_on_vnpy/strategy/__pycache__/strategyRandom.cpython-36.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vvipi/py3_demo_on_vnpy_ctp/491f6394ce049a7dd93a34415cd2ffe499bf9ffa/py3_ctp_demo_on_vnpy/strategy/__pycache__/strategyRandom.cpython-36.pyc -------------------------------------------------------------------------------- /py3_ctp_demo_on_vnpy/strategy/strategyRandom.py: -------------------------------------------------------------------------------- 1 | # encoding: UTF-8 2 | 3 | ''' 4 | 模仿vnpy的CTA策略模板开发的策略,无数据库,数据来源csv或json。 5 | ''' 6 | import numpy as np 7 | from datetime import datetime, time 8 | from random import randint 9 | from modules.objects import * 10 | 11 | 12 | 13 | 14 | ######################################################################## 15 | class StrategyRandom(object): 16 | """随机策略""" 17 | 18 | # 策略类的名称和作者 19 | className = 'StrategyRandom' # 随便下单 20 | author = 'vvipi' 21 | 22 | #---------------------------------------------------------------------- 23 | def __init__(self, ctaEngine, setting): 24 | """Constructor""" 25 | self.ctaEngine = ctaEngine 26 | 27 | # 直接从json读tick数据,本策略初始化时不需要读取历史数据,这里忽略 28 | self.TickJsonName = '' 29 | 30 | # 策略的基本参数 31 | self.name = EMPTY_UNICODE # 策略实例名称 32 | self.vtSymbol = EMPTY_STRING # 交易的合约vt系统代码 33 | self.volume = 1 # 默认开仓手数 34 | self.priceTick = 0 # 最小变动单位 35 | self.size = 500 # 缓存tick的数量 36 | 37 | 38 | # 策略的基本变量,由引擎管理 39 | self.inited = False # 是否进行了初始化 40 | self.trading = False # 是否启动交易,由引擎管理 41 | self.pos = 0 # 持仓情况 42 | self.status = 0 # 策略状态(0 空闲, 1 等待确认, 2 持仓中 43 | self.direction = '' # 持仓方向 44 | self.loaded = False # 已缓存足够的tick 45 | self.tickCount = EMPTY_INT # tick计数 46 | self.openPrice = EMPTY_FLOAT 47 | self.stopPrice = EMPTY_FLOAT 48 | self.exitPrice = EMPTY_FLOAT 49 | 50 | # 其他参数 51 | self.arrayTick = np.zeros(self.size) 52 | 53 | # 参数列表,保存了参数的名称 54 | self.paramList = [ 55 | 'name', 56 | 'className', 57 | 'author', 58 | 'vtSymbol', 59 | 'volume', 60 | 'priceTick', 61 | 'size', 62 | ] 63 | 64 | # 变量列表,保存了变量的名称 65 | self.varList = [ 66 | 'inited', 67 | 'trading', 68 | 'pos', 69 | 'direction', 70 | 'status', 71 | 'direction', 72 | 'loaded', 73 | 'openPrice', 74 | 'stopPrice', 75 | 'exitPrice', 76 | 'tickCount', 77 | ] 78 | 79 | # 同步列表,保存了需要保存到本地文件的变量名称 80 | self.syncList = [ 81 | 'pos', 82 | ] 83 | # 设置策略的参数 84 | if setting: 85 | d = self.__dict__ 86 | for key in self.paramList: 87 | if key in setting: 88 | d[key] = setting[key] 89 | 90 | #---------------------------------------------------------------------- 91 | def onInit(self): 92 | """初始化策略""" 93 | # raise NotImplementedError 94 | self.writeCtaLog('%s策略初始化' %self.name) 95 | 96 | # # 载入历史数据,并采用回放计算的方式初始化策略数值 97 | # initData = self.loadBar(self.initDays) 98 | # for bar in initData: 99 | # self.onBar(bar) 100 | 101 | self.putEvent() 102 | 103 | #---------------------------------------------------------------------- 104 | def onStart(self): 105 | """启动策略""" 106 | self.writeCtaLog('%s策略启动' %self.name) 107 | self.putEvent() 108 | 109 | #---------------------------------------------------------------------- 110 | def onStop(self): 111 | """停止策略""" 112 | self.writeCtaLog('%s策略停止' %self.name) 113 | self.putEvent() 114 | 115 | def initArray(self, tick): 116 | '''初始化tick缓存''' 117 | self.tickCount += 1 118 | if not self.loaded and self.tickCount >= self.size: 119 | self.loaded = True 120 | self.tickCount = 0 121 | self.newTick(tick) 122 | 123 | def newTick(self, tick): 124 | '''更新缓存的tick''' 125 | self.arrayTick[0:self.size-1] = self.arrayTick[1:self.size] 126 | self.arrayTick[-1] = tick.lastPrice 127 | # 计算指标 128 | self.rangeMax = np.percentile(self.arrayTick, 99.5) 129 | self.rangeMin = np.percentile(self.arrayTick, 0.5) 130 | 131 | def onTick(self, tick): 132 | """ 处理tick数据,产生交易信号""" 133 | 134 | # 先初始化 135 | if not self.loaded: 136 | self.initArray(tick) 137 | return 138 | else: 139 | self.newTick(tick) 140 | 141 | # 根据策略阶段选择方法 142 | func = self.case0 if self.status == 0 else self.case1 143 | func(tick) 144 | 145 | def case0(self, tick): 146 | """空仓阶段""" 147 | 148 | r = self.rangeMax / self.rangeMin 149 | 150 | if r > 1.01: 151 | n = randint(0, 9) % 2 152 | self.direction = DIRECTION_LONG if n else DIRECTION_SHORT # 随机多空 153 | 154 | if self.direction is DIRECTION_LONG: 155 | price = tick.bidPrice1 - self.priceTick * 3 156 | self.buy(price, self.volume) 157 | self.openPrice = tick.askPrice1 158 | self.stopPrice = tick.askPrice1 * 0.998 159 | self.exitPrice = tick.askPrice1 * 1.006 160 | elif self.direction is DIRECTION_SHORT: 161 | price = tick.bidPrice1 - self.priceTick * 3 162 | self.short(price, self.volume) 163 | self.openPrice = tick.bidPrice1 164 | self.stopPrice = tick.bidPrice1 * 1.002 165 | self.exitPrice = tick.bidPrice1 * 0.994 166 | self.status = 1 167 | self.putEvent() 168 | 169 | def case1(self, tick): 170 | """持仓阶段""" 171 | l = tick.lastPrice 172 | 173 | if self.direction is DIRECTION_LONG: # 多头退出 174 | if l > self.exitPrice or l < self.stopPrice: 175 | self.closePos(tick) 176 | self.putEvent() 177 | 178 | if self.direction is DIRECTION_SHORT: # 空头退出 179 | if l < self.exitPrice or l > self.stopPrice: 180 | self.closePos(tick) 181 | self.putEvent() 182 | #---------------------------------------------------------------------- 183 | def closePos(self, tick): 184 | """平仓""" 185 | if self.pos > 0: 186 | price = tick.bidPrice1 - self.priceTick * 3 187 | self.sell(price, self.pos) 188 | 189 | if self.pos < 0: 190 | price = tick.bidPrice1 + self.priceTick * 3 191 | self.cover(price, self.pos) 192 | 193 | self.status = 0 194 | self.putEvent() 195 | 196 | #---------------------------------------------------------------------- 197 | def onOrder(self, order): 198 | """收到委托变化推送""" 199 | 200 | pass 201 | 202 | #---------------------------------------------------------------------- 203 | def onTrade(self, trade): 204 | """收到成交推送""" 205 | 206 | self.putEvent() 207 | 208 | #---------------------------------------------------------------------- 209 | def onBar(self, bar): 210 | """收到Bar推送(本策略没有用到Bar)""" 211 | pass 212 | 213 | #---------------------------------------------------------------------- 214 | def onStopOrder(self, so): 215 | """收到停止单推送(本策略没有用到停止单)""" 216 | pass 217 | 218 | #---------------------------------------------------------------------- 219 | def buy(self, price, volume, stop=False): 220 | """买开""" 221 | return self.sendOrder(CTAORDER_BUY, price, volume, stop) 222 | 223 | #---------------------------------------------------------------------- 224 | def sell(self, price, volume, stop=False): 225 | """卖平""" 226 | return self.sendOrder(CTAORDER_SELL, price, volume, stop) 227 | 228 | #---------------------------------------------------------------------- 229 | # def sellToday(self, price, volume, stop=False): 230 | # """卖平今""" 231 | # return self.sendOrder(CTAORDER_SELLTODAY, price, volume, stop) 232 | 233 | #---------------------------------------------------------------------- 234 | def short(self, price, volume, stop=False): 235 | """卖开""" 236 | return self.sendOrder(CTAORDER_SHORT, price, volume, stop) 237 | 238 | #---------------------------------------------------------------------- 239 | def cover(self, price, volume, stop=False): 240 | """买平""" 241 | return self.sendOrder(CTAORDER_COVER, price, volume, stop) 242 | 243 | #---------------------------------------------------------------------- 244 | # def coverToday(self, price, volume, stop=False): 245 | # """买平今""" 246 | # return self.sendOrder(CTAORDER_COVERTODAY, price, volume, stop) 247 | 248 | #---------------------------------------------------------------------- 249 | def sendOrder(self, orderType, price, volume, stop=False): 250 | """发送委托""" 251 | if self.trading: 252 | # 如果stop为True,则意味着发本地停止单 253 | if stop: 254 | vtOrderIDList = self.ctaEngine.sendStopOrder(self.vtSymbol, orderType, price, volume, self) 255 | else: 256 | vtOrderIDList = self.ctaEngine.sendOrder(self.vtSymbol, orderType, price, volume, self) 257 | return vtOrderIDList 258 | else: 259 | # 交易停止时发单返回空字符串 260 | return [] 261 | 262 | #---------------------------------------------------------------------- 263 | def cancelOrder(self, vtOrderID): 264 | """撤单""" 265 | # 如果发单号为空字符串,则不进行后续操作 266 | if not vtOrderID: 267 | return 268 | 269 | if STOPORDERPREFIX in vtOrderID: 270 | self.ctaEngine.cancelStopOrder(vtOrderID) 271 | else: 272 | self.ctaEngine.cancelOrder(vtOrderID) 273 | 274 | #---------------------------------------------------------------------- 275 | def cancelAll(self): 276 | """全部撤单""" 277 | self.ctaEngine.cancelAll(self.name) 278 | 279 | #---------------------------------------------------------------------- 280 | # def insertTick(self, tick): 281 | # """向数据库中插入tick数据""" 282 | # self.ctaEngine.insertData(self.tickDbName, self.vtSymbol, tick) 283 | 284 | #---------------------------------------------------------------------- 285 | # def insertBar(self, bar): 286 | # """向数据库中插入bar数据""" 287 | # self.ctaEngine.insertData(self.barDbName, self.vtSymbol, bar) 288 | 289 | #---------------------------------------------------------------------- 290 | def loadTick(self, days): 291 | """读取tick数据""" 292 | return self.ctaEngine.loadTick(self.TickJsonName, self.vtSymbol, days) 293 | 294 | #---------------------------------------------------------------------- 295 | # def loadBar(self, days): 296 | # """读取bar数据""" 297 | # return self.ctaEngine.loadBar(self.TickJsonName, self.vtSymbol, days) 298 | 299 | #---------------------------------------------------------------------- 300 | def writeCtaLog(self, content): 301 | """记录CTA日志""" 302 | content = self.name + ':' + content 303 | self.ctaEngine.writeCtaLog(content) 304 | 305 | #---------------------------------------------------------------------- 306 | def putEvent(self): 307 | """发出策略状态变化事件""" 308 | self.ctaEngine.putStrategyEvent(self.name) 309 | 310 | #---------------------------------------------------------------------- 311 | def getEngineType(self): 312 | """查询当前运行的环境""" 313 | return self.ctaEngine.engineType 314 | 315 | -------------------------------------------------------------------------------- /py3_ctp_demo_on_vnpy/temp/deleteme: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vvipi/py3_demo_on_vnpy_ctp/491f6394ce049a7dd93a34415cd2ffe499bf9ffa/py3_ctp_demo_on_vnpy/temp/deleteme -------------------------------------------------------------------------------- /screenshots/screenshot20180308.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vvipi/py3_demo_on_vnpy_ctp/491f6394ce049a7dd93a34415cd2ffe499bf9ffa/screenshots/screenshot20180308.PNG --------------------------------------------------------------------------------