diff --git a/vnpy/trader/app/ctaStrategy/ctaBacktesting.py b/vnpy/trader/app/ctaStrategy/ctaBacktesting.py index d4a6d581..e3efcb91 100644 --- a/vnpy/trader/app/ctaStrategy/ctaBacktesting.py +++ b/vnpy/trader/app/ctaStrategy/ctaBacktesting.py @@ -35,6 +35,7 @@ from vnpy.trader.vtEvent import * from vnpy.trader.setup_logger import setup_logger from vnpy.trader.data_source import DataSource from vnpy.trader.app.ctaStrategy.ctaEngine import PositionBuffer +from vnpy.trader.app.ctaStrategy.fundKline import FundKline ######################################################################## class BacktestingEngine(object): @@ -183,6 +184,35 @@ class BacktestingEngine(object): self.useBreakoutMode = False + self.logs_path = None + self.data_path = None + + self.fund_kline = None + self.fund_renko = False + + self.price_dict = {} + + def create_fund_kline(self): + setting = {} + setting.update({'name': self.strategy_name}) + setting['inputMa1Len'] = 5 + setting['inputMa2Len'] = 10 + setting['inputMa3Len'] = 20 + setting['inputYb'] = True + setting['minDiff'] = 0.01 + setting['shortSymbol'] = 'fund' + if self.fund_renko: + setting['height'] = self.initCapital * 0.001 + setting['use_renko'] = True + + self.fund_kline = FundKline(cta_engine=self, setting=setting) + + return self.fund_kline + + def get_fund_kline(self,name=None): + """获取资金曲线""" + return self.fund_kline + def getAccountInfo(self): """返回账号的实时权益,可用资金,仓位比例,投资仓位比例上限""" if self.netCapital == EMPTY_FLOAT: @@ -203,7 +233,7 @@ class BacktestingEngine(object): self.strategyStartDate = self.dataStartDate + initTimeDelta - #---------------------------------------------------------------------- + # ---------------------------------------------------------------------- def setEndDate(self, endDate=''): """设置回测的结束日期""" self.endDate = endDate @@ -219,12 +249,12 @@ class BacktestingEngine(object): self.minDiff = minDiff self.priceTick = minDiff - #---------------------------------------------------------------------- + # ---------------------------------------------------------------------- def setBacktestingMode(self, mode): """设置回测模式""" self.mode = mode - #---------------------------------------------------------------------- + # ---------------------------------------------------------------------- def setDatabase(self, dbName, symbol): """设置历史数据所用的数据库""" self.dbName = dbName @@ -257,10 +287,26 @@ class BacktestingEngine(object): """查询合约的size""" return self.size + def setPrice(self,vtSymbol, price): + self.price_dict.update({vtSymbol:price}) + + def qryPrice(self,vtSymbol): + return self.price_dict.get(vtSymbol,None) # ---------------------------------------------------------------------- def setRate(self, rate): """设置佣金比例""" self.rate = rate + if rate >=0.1: + self.fixCommission = rate + + def setInitCapital(self,capital): + """设置期初资金""" + self.output(u'设置期初资金:{}'.format(capital)) + self.capital = capital + self.initCapital = capital + self.netCapital = capital + self.maxCapital = capital + self.maxNetCapital = capital # ---------------------------------------------------------------------- def setPriceTick(self, priceTick): @@ -268,7 +314,7 @@ class BacktestingEngine(object): self.priceTick = priceTick self.minDiff = priceTick - def setStrategyName(self,strategy_name): + def setStrategyName(self, strategy_name): """ 设置策略的运行实例名称 :param strategy_name: @@ -276,6 +322,7 @@ class BacktestingEngine(object): """ self.strategy_name = strategy_name + def setDailyReportName(self, report_file): """ 设置策略的日净值记录csv保存文件名(含路径) @@ -349,6 +396,47 @@ class BacktestingEngine(object): # 保存本地cache文件 self.__saveDataHistoryToLocalCache(symbol, startDate, endDate) + + + + #---------------------------------------------------------------------- + def getMysqlDeltaDate(self,symbol, startDate, decreaseDays): + """从mysql库中获取交易日前若干天 + added by IncenseLee + """ + try: + if self.__mysqlConnected: + + # 获取mysql指针 + cur = self.__mysqlConnection.cursor() + + sqlstring='select distinct ndate from TB_{0}MI where ndate < ' \ + 'cast(\'{1}\' as date) order by ndate desc limit {2},1'.format(symbol, startDate, decreaseDays-1) + + # self.writeCtaLog(sqlstring) + + count = cur.execute(sqlstring) + + if count > 0: + + # 提取第一条记录 + result = cur.fetchone() + + return result[0] + + else: + self.writeCtaLog(u'MysqlDB没有查询结果,请检查日期') + + else: + self.writeCtaLog(u'MysqlDB未连接,请检查') + + except MySQLdb.Error as e: + self.writeCtaLog(u'MysqlDB载入数据失败,请检查.Error {0}: {1}'.format(e.arg[0],e.arg[1])) + + # 出错后缺省返回 + return startDate-timedelta(days=3) + + # ---------------------------------------------------------------------- def runBackTestingWithMongoDBTicks(self, symbol): """ 根据测试的每一天,从MongoDB载入历史数据,并推送Tick至回测函数 @@ -385,8 +473,8 @@ class BacktestingEngine(object): self.strategy.onStart() self.output(u'策略启动完成') - # isOffline = False # WJ - isOffline = True + isOffline = False # WJ + host, port, log = loadMongoSetting() self.dbClient = pymongo.MongoClient(host, port) @@ -424,7 +512,7 @@ class BacktestingEngine(object): query_time = datetime.now() # 载入初始化需要用的数据 - flt = {'tradingDay': testday.strftime('%Y%m%d')} # WJ: using TradingDay instead of calandar day + flt = {'tradingDay': testday.strftime('%Y-%m-%d')} # WJ: using TradingDay instead of calandar day # flt = {'datetime': {'$gte': testday_monrning, '$lt': testday_midnight}} initCursor = collection.find(flt).sort('datetime', pymongo.ASCENDING) @@ -463,13 +551,844 @@ class BacktestingEngine(object): if len(rawTicks) > 1: self.savingDailyData(testday, self.capital, self.maxCapital, self.totalCommission) + def runBackTestingWithArbTickFile(self,mainPath, arbSymbol): + """运行套利回测(使用本地tick TXT csv数据) + 参数:套利代码 SP rb1610&rb1701 + added by IncenseLee + 原始的tick,分别存放在白天目录1和夜盘目录2中,每天都有各个合约的数据 + Z:\ticks\SHFE\201606\RB\0601\ + RB1610.txt + RB1701.txt + .... + Z:\ticks\SHFE_night\201606\RB\0601 + RB1610.txt + RB1701.txt + .... + + 夜盘目录为自然日,不是交易日。 + + 按照回测的开始日期,到结束日期,循环每一天。 + 每天优先读取日盘数据,再读取夜盘数据。 + 读取eg1(如RB1610),读取Leg2(如RB701),合并成价差tick,灌输到策略的onTick中。 + """ + self.capital = self.initCapital # 更新设置期初资金 + + if len(arbSymbol) < 1: + self.writeCtaLog(u'套利合约为空') + return + + if not (arbSymbol.upper().index("SP") == 0 and arbSymbol.index(" ") > 0 and arbSymbol.index("&") > 0): + self.writeCtaLog(u'套利合约格式不符合') + return + + # 获得Leg1,leg2 + legs = arbSymbol[arbSymbol.index(" "):] + leg1 = legs[1:legs.index("&")] + leg2 = legs[legs.index("&") + 1:] + self.writeCtaLog(u'Leg1:{0},Leg2:{1}'.format(leg1, leg2)) + + if not self.dataStartDate: + self.writeCtaLog(u'回测开始日期未设置。') + return + # RB + if len(self.symbol)<1: + self.writeCtaLog(u'回测对象未设置。') + return + + if not self.dataEndDate: + self.dataEndDate = datetime.today() + + #首先根据回测模式,确认要使用的数据类 + if self.mode == self.BAR_MODE: + self.writeCtaLog(u'本回测仅支持tick模式') + return + + testdays = (self.dataEndDate - self.dataStartDate).days + + if testdays < 1: + self.writeCtaLog(u'回测时间不足') + return + + for i in range(0, testdays): + + testday = self.dataStartDate + timedelta(days = i) + + self.output(u'回测日期:{0}'.format(testday)) + + # 白天数据 + self.__loadArbTicks(mainPath,testday,leg1,leg2) + + # 撤销所有之前的orders + if self.symbol: + self.cancelOrders(self.symbol) + # 更新持仓缓存 + self.update_pos_buffer() + # 夜盘数据 + self.__loadArbTicks(mainPath+'_night', testday, leg1, leg2) + + + # ---------------------------------------------------------------------- + def runBackTestingWithTickFile(self, mainPath, symbol): + """运行Tick回测(使用本地tick TXT csv数据) + 参数:代码 rb1610 + added by WenjianDu + 原始的tick,分别存放在白天目录1和夜盘目录2中,每天都有各个合约的数据 + Z:\ticks\SHFE\201606\RB\0601\ + RB1610.txt + RB1701.txt + .... + Z:\ticks\SHFE_night\201606\RB\0601 + RB1610.txt + RB1701.txt + .... + + 夜盘目录为自然日,不是交易日。 + + 按照回测的开始日期,到结束日期,循环每一天。 + 每天优先读取日盘数据,再读取夜盘数据。 + 读取tick(如RB1610),灌输到策略的onTick中。 + """ + self.capital = self.initCapital # 更新设置期初资金 + + if len(symbol) < 1: + self.writeCtaLog(u'合约为空') + return + + # 获得tick + self.writeCtaLog(u'arbSymbol:{0}'.format(symbol)) + + if not self.dataStartDate: + self.writeCtaLog(u'回测开始日期未设置。') + return + # RB + if len(self.symbol) < 1: + self.writeCtaLog(u'回测对象未设置。') + return + + if not self.dataEndDate: + self.dataEndDate = datetime.today() + + # 首先根据回测模式,确认要使用的数据类 + if self.mode == self.BAR_MODE: + self.writeCtaLog(u'本回测仅支持tick模式') + return + + testdays = (self.dataEndDate - self.dataStartDate).days + + if testdays < 1: + self.writeCtaLog(u'回测时间不足') + return + + for i in range(0, testdays): + testday = self.dataStartDate + timedelta(days=i) + self.output(u'回测日期:{0}'.format(testday)) + # 白天数据 + self.__loadTxtTicks(mainPath, testday, symbol) + # 撤销所有之前的orders + if self.symbol: + self.cancelOrders(self.symbol) + # 更新持仓缓存 + self.update_pos_buffer() + # # 夜盘数据 + # self.__loadTxtTicks(mainPath + '_night', testday, symbol) + self.savingDailyData(testday, self.capital, self.maxCapital, self.totalCommission) + + # ---------------------------------------------------------------------- + def runBackTestingWithArbTickFile2(self, leg1MainPath, leg2MainPath, arbSymbol): + """运行套利回测(使用本地tick csv数据) + 参数:套利代码 SP rb1610&rb1701 + added by IncenseLee + 原始的tick,存放在相应市场下每天的目录中,目录包含市场各个合约的数据 + E:\ticks\SQ\201606\20160601\ + RB10.csv + RB01.csv + .... + + 目录为交易日。 + 按照回测的开始日期,到结束日期,循环每一天。 + + 读取eg1(如RB1610),读取Leg2(如RB701),合并成价差tick,灌输到策略的onTick中。 + """ + self.capital = self.initCapital # 更新设置期初资金 + + if len(arbSymbol) < 1: + self.writeCtaLog(u'套利合约为空') + return + + if not (arbSymbol.upper().index("SP") == 0 and arbSymbol.index(" ") > 0 and arbSymbol.index("&") > 0): + self.writeCtaLog(u'套利合约格式不符合') + return + + # 获得Leg1,leg2 + legs = arbSymbol[arbSymbol.index(" "):] + leg1 = legs[1:legs.index("&")] + leg2 = legs[legs.index("&") + 1:] + self.writeCtaLog(u'Leg1:{0},Leg2:{1}'.format(leg1, leg2)) + + if not self.dataStartDate: + self.writeCtaLog(u'回测开始日期未设置。') + return + # RB + if len(self.symbol) < 1: + self.writeCtaLog(u'回测对象未设置。') + return + + if not self.dataEndDate: + self.dataEndDate = datetime.today() + + # 首先根据回测模式,确认要使用的数据类 + if self.mode == self.BAR_MODE: + self.writeCtaLog(u'本回测仅支持tick模式') + return + + testdays = (self.dataEndDate - self.dataStartDate).days + + if testdays < 1: + self.writeCtaLog(u'回测时间不足') + return + + for i in range(0, testdays): + testday = self.dataStartDate + timedelta(days=i) + + self.output(u'回测日期:{0}'.format(testday)) + + # 白天数据 + self.__loadArbTicks2(leg1MainPath, leg2MainPath, testday, leg1, leg2) + + + def runBackTestingWithNonStrArbTickFile(self, leg1MainPath, leg2MainPath, leg1Symbol,leg2Symbol): + """运行套利回测(使用本地tick txt数据) + 参数: + leg1MainPath: leg1合约所在的市场路径 + leg2MainPath: leg2合约所在的市场路径 + leg1Symbol: leg1合约 + Leg2Symbol:leg2合约 + added by IncenseLee + 原始的tick,分别存放在白天目录1和夜盘目录2中,每天都有各个合约的数据 + Z:\ticks\SHFE\201606\RB\0601\ + RB1610.txt + RB1701.txt + .... + Z:\ticks\SHFE_night\201606\RB\0601 + RB1610.txt + RB1701.txt + .... + + 夜盘目录为自然日,不是交易日。 + + 按照回测的开始日期,到结束日期,循环每一天。 + 每天优先读取日盘数据,再读取夜盘数据。 + 读取eg1(如RB1610),读取Leg2(如RB701),根据两者tick的时间优先顺序,逐一tick灌输到策略的onTick中。 + """ + self.capital = self.initCapital # 更新设置期初资金 + + if not self.dataStartDate: + self.writeCtaLog(u'回测开始日期未设置。') + return + # RB + if len(self.symbol)<1: + self.writeCtaLog(u'回测对象未设置。') + return + + if not self.dataEndDate: + self.dataEndDate = datetime.today() + + #首先根据回测模式,确认要使用的数据类 + if self.mode == self.BAR_MODE: + self.writeCtaLog(u'本回测仅支持tick模式') + return + + testdays = (self.dataEndDate - self.dataStartDate).days + + if testdays < 1: + self.writeCtaLog(u'回测时间不足') + return + + for i in range(0, testdays): + testday = self.dataStartDate + timedelta(days = i) + + self.output(u'回测日期:{0}'.format(testday)) + # 撤销所有之前的orders + if self.symbol: + self.cancelOrders(self.symbol) + if self.last_leg1_tick: + self.cancelOrders(self.last_leg1_tick.vtSymbol) + if self.last_leg2_tick: + self.cancelOrders(self.last_leg2_tick.vtSymbol) + # 更新持仓缓存 + self.update_pos_buffer() + # 加载运行白天数据 + self.__loadNotStdArbTicks(leg1MainPath, leg2MainPath, testday, leg1Symbol,leg2Symbol) + + self.savingDailyData(testday, self.capital, self.maxCapital,self.totalCommission) + + # 加载运行夜盘数据 + self.__loadNotStdArbTicks(leg1MainPath+'_night', leg2MainPath+'_night', testday, leg1Symbol, leg2Symbol) + + self.savingDailyData(self.dataEndDate, self.capital, self.maxCapital,self.totalCommission) + + def runBackTestingWithNonStrArbTickFile2(self, leg1MainPath, leg2MainPath, leg1Symbol, leg2Symbol): + """运行套利回测(使用本地tickcsv数据,数据从taobao标普购买) + 参数: + leg1MainPath: leg1合约所在的市场路径 + leg2MainPath: leg2合约所在的市场路径 + leg1Symbol: leg1合约 + Leg2Symbol:leg2合约 + added by IncenseLee + 原始的tick,存放在相应市场下每天的目录中,目录包含市场各个合约的数据 + E:\ticks\SQ\201606\20160601\ + RB10.csv + RB01.csv + .... + + 目录为交易日。 + 按照回测的开始日期,到结束日期,循环每一天。 + + 读取eg1(如RB1610),读取Leg2(如RB701),根据两者tick的时间优先顺序,逐一tick灌输到策略的onTick中。 + """ + self.capital = self.initCapital # 更新设置期初资金 + + if not self.dataStartDate: + self.writeCtaLog(u'回测开始日期未设置。') + return + # RB + if len(self.symbol) < 1: + self.writeCtaLog(u'回测对象未设置。') + return + + if not self.dataEndDate: + self.dataEndDate = datetime.today() + + # 首先根据回测模式,确认要使用的数据类 + if self.mode == self.BAR_MODE: + self.writeCtaLog(u'本回测仅支持tick模式') + return + + testdays = (self.dataEndDate - self.dataStartDate).days + + if testdays < 1: + self.writeCtaLog(u'回测时间不足') + return + + self.writeCtaLog(u'开始回测:{} ~ {}'.format(self.dataStartDate,self.dataEndDate)) + for i in range(0, testdays): + testday = self.dataStartDate + timedelta(days=i) + + self.output(u'回测日期:{0}'.format(testday)) + # 撤销所有之前的orders + if self.symbol: + self.cancelOrders(self.symbol) + if self.last_leg1_tick: + self.cancelOrders(self.last_leg1_tick.vtSymbol) + if self.last_leg2_tick: + self.cancelOrders(self.last_leg2_tick.vtSymbol) + # 更新持仓缓存 + self.update_pos_buffer() + # 加载运行每天得tick套利数据 + self.__run_arbitrage_ticks2(leg1MainPath, leg2MainPath, testday, leg1Symbol, leg2Symbol) + + self.savingDailyData(testday, self.capital, self.maxCapital,self.totalCommission) + + def runBackTestingWithNonStrArbTickFromMongoDB(self, leg1Symbol, leg2Symbol): + """运行套利回测(使用服务器数据,数据从taobao标普购买) + 参数: + leg1Symbol: leg1合约 + Leg2Symbol:leg2合约 + added by IncenseLee + + 目录为交易日。 + 按照回测的开始日期,到结束日期,循环每一天。 + + 读取eg1(如RB1610),读取Leg2(如RB1701),根据两者tick的时间优先顺序,逐一tick灌输到策略的onTick中。 + """ + + # 连接数据库 + host, port, log = loadMongoSetting() + self.dbClient = pymongo.MongoClient(host, port) + + self.capital = self.initCapital # 更新设置期初资金 + + if not self.dataStartDate: + self.writeCtaLog(u'回测开始日期未设置。') + return + # RB + if len(self.symbol) < 1: + self.writeCtaLog(u'回测对象未设置。') + return + + if not self.dataEndDate: + self.dataEndDate = datetime.today() + + # 首先根据回测模式,确认要使用的数据类 + if self.mode == self.BAR_MODE: + self.writeCtaLog(u'本回测仅支持tick模式') + return + + testdays = (self.dataEndDate - self.dataStartDate).days + + if testdays < 1: + self.writeCtaLog(u'回测时间不足') + return + + for i in range(0, testdays): + testday = self.dataStartDate + timedelta(days=i) + + self.output(u'回测日期:{0}'.format(testday)) + + # 撤销所有之前的orders + if self.symbol: + self.cancelOrders(self.symbol) + if self.last_leg1_tick: + self.cancelOrders(self.last_leg1_tick.vtSymbol) + if self.last_leg2_tick: + self.cancelOrders(self.last_leg2_tick.vtSymbol) + # 更新持仓缓存 + self.update_pos_buffer() + # 加载运行每天数据 + self.__loadNotStdArbTicksFromMongoDB(testday, leg1Symbol, leg2Symbol) + + self.savingDailyData(testday, self.capital, self.maxCapital, self.totalCommission) + + def runBackTestingWithRqTickFile(self,leg1_path,leg2_path,leg1_symbol, leg2_symbol): + """ + 运行套利回测,使用本地ricequant下载得tick数据 + :param leg1_path:腿1路径 + :param leg2_path:腿2路径 + :param leg1_symbol:腿1合约 + :param leg2_symbol:腿2合约 + :return: + """ + + if not self.dataStartDate: + self.writeCtaLog(u'回测开始日期未设置。') + return + # RB + if len(self.symbol) < 1: + self.writeCtaLog(u'回测对象未设置。') + return + + if not self.dataEndDate: + self.dataEndDate = datetime.today() + + # 首先根据回测模式,确认要使用的数据类 + if self.mode == self.BAR_MODE: + self.writeCtaLog(u'本回测仅支持tick模式') + return + + testdays = (self.dataEndDate - self.dataStartDate).days + + if testdays < 1: + self.writeCtaLog(u'回测时间不足') + return + + self.writeCtaLog(u'开始回测:{} ~ {}'.format(self.dataStartDate, self.dataEndDate)) + for i in range(0, testdays): + testday = self.dataStartDate + timedelta(days=i) + + self.output(u'回测日期:{0}'.format(testday)) + # 撤销所有之前的orders + if self.symbol: + self.cancelOrders(self.symbol) + if self.last_leg1_tick: + self.cancelOrders(self.last_leg1_tick.vtSymbol) + if self.last_leg2_tick: + self.cancelOrders(self.last_leg2_tick.vtSymbol) + # 更新持仓缓存 + self.update_pos_buffer() + # 加载运行每天数据 + print('{}'.format(testday)) + if self.__run_rq_ticks(leg1_path, leg2_path, testday, leg1_symbol, leg2_symbol): + self.savingDailyData(testday, self.capital, self.maxCapital, self.totalCommission) + + # ---------------------------------------------------------------------- + def runBackTestingWithBarFile(self, filename): + """运行回测(使用本地csv数据) + added by IncenseLee + """ + self.capital = self.initCapital # 更新设置期初资金 + if not filename: + self.writeCtaLog(u'请指定回测数据文件') + return + + if not self.strategyStartDate: + self.writeCtaLog(u'回测开始日期未设置。') + return + + if not self.dataEndDate: + self.dataEndDate = datetime.today() + + import os + if not os.path.isfile(filename): + self.writeCtaLog(u'{0}文件不存在'.format(filename)) + + if len(self.symbol) < 1: + self.writeCtaLog(u'回测对象未设置。') + return + + # 首先根据回测模式,确认要使用的数据类 + if not self.mode == self.BAR_MODE: + self.writeCtaLog(u'注意:文件仅支持bar模式,请修改mode') + return + + # self.output(u'开始回测') + + self.strategy.onInit() + self.strategy.trading = False + self.output(u'策略初始化完成') + + self.output(u'开始回放数据') + + import csv + csvfile = open(filename, 'r', encoding='utf8') + reader = csv.DictReader((line.replace('\0', '') for line in csvfile), delimiter=",") + last_tradingDay = None + for row in reader: + try: + bar = CtaBarData() + bar.symbol = self.symbol + bar.vtSymbol = self.symbol + + # 从tb导出的csv文件 + # bar.open = float(row['Open']) + # bar.high = float(row['High']) + # bar.low = float(row['Low']) + # bar.close = float(row['Close']) + # bar.volume = float(row['TotalVolume'])# + # barEndTime = datetime.strptime(row['Date']+' ' + row['Time'], '%Y/%m/%d %H:%M:%S') + if row.get('open', None) is None: + continue + if row.get('high', None) is None: + continue + if row.get('low', None) is None: + continue + if row.get('close', None) is None: + continue + + if len(row['open']) == 0 or len(row['high']) == 0 or len(row['low']) == 0 or len(row['close']) == 0: + continue + # 从ricequant导出的csv文件 + bar.open = self.roundToPriceTick(float(row['open'])) + bar.high = self.roundToPriceTick(float(row['high'])) + bar.low = self.roundToPriceTick(float(row['low'])) + bar.close = self.roundToPriceTick(float(row['close'])) + bar.volume = float(row['volume']) if len(row['volume']) > 0 else 0 + if '-' in row['index']: + barEndTime = datetime.strptime(row['index'], '%Y-%m-%d %H:%M:%S') + elif '-' in row['datetime']: + barEndTime = datetime.strptime(row['datetime'], '%Y-%m-%d %H:%M:%S') + else: + barEndTime = datetime.strptime(row['datetime'], '%Y%m%d%H%M%S') + + # 使用Bar的开始时间作为datetime + bar.datetime = barEndTime - timedelta(seconds=self.barTimeInterval) + + bar.date = bar.datetime.strftime('%Y-%m-%d') + bar.time = bar.datetime.strftime('%H:%M:%S') + if 'trading_date' in row: + if len(row['trading_date']) is 8: + bar.tradingDay = row['trading_date'][0:4] + '-' \ + + row['trading_date'][4:6] + '-' \ + + row['trading_date'][6:] + else: + bar.tradingDay = row['trading_date'] + else: + if bar.datetime.hour >= 21 and not self.is_7x24: + if bar.datetime.isoweekday() == 5: + # 星期五=》星期一 + bar.tradingDay = (barEndTime + timedelta(days=3)).strftime('%Y-%m-%d') + else: + # 第二天 + bar.tradingDay = (barEndTime + timedelta(days=1)).strftime('%Y-%m-%d') + elif bar.datetime.hour < 8 and bar.datetime.isoweekday() == 6 and not self.is_7x24: + # 星期六=>星期一 + bar.tradingDay = (barEndTime + timedelta(days=2)).strftime('%Y-%m-%d') + else: + bar.tradingDay = bar.date + + if self.strategyStartDate <= bar.datetime <= self.dataEndDate: + if last_tradingDay != bar.tradingDay: + if last_tradingDay is not None: + self.savingDailyData(datetime.strptime(last_tradingDay, '%Y-%m-%d'), self.capital, + self.maxCapital, self.totalCommission, benchmark=bar.close) + last_tradingDay = bar.tradingDay + + # 第二个交易日,撤单 + self.cancelOrders(self.symbol) + # 更新持仓缓存 + self.update_pos_buffer() + + if self.dataStartDate >= bar.datetime: + continue + + if bar.datetime > self.dataEndDate: + continue + + # Check the order triggers and deliver the bar to the Strategy + if self.useBreakoutMode is False: + self.newBar(bar) + else: + self.newBarForBreakout(bar) + + if not self.strategy.trading and self.strategyStartDate < bar.datetime: + self.strategy.trading = True + self.strategy.onStart() + self.output(u'策略启动交易') + + if self.netCapital < 0: + self.writeCtaError(u'净值低于0,回测停止') + return + + except Exception as ex: + self.writeCtaError(u'回测异常导致停止') + self.writeCtaError(u'{},{}'.format(str(ex), traceback.format_exc())) + return + + # ---------------------------------------------------------------------- + def runBackTestingWithDataSource(self, data_source_url=None): + """运行回测(使用数据源) + added by IncenseLee + """ + self.capital = self.initCapital # 更新设置期初资金 + if not self.strategyStartDate: + self.writeCtaLog(u'回测开始日期未设置。') + return + + if not self.dataEndDate: + self.dataEndDate = datetime.today() + + if len(self.symbol) < 1: + self.writeCtaLog(u'回测对象未设置。') + return + + # 首先根据回测模式,确认要使用的数据类 + if not self.mode == self.BAR_MODE: + self.writeCtaLog(u'文件仅支持bar模式,若扩展tick模式,需要修改本方法') + return + + # self.output(u'开始回测') + + # self.strategy.inited = True + self.strategy.onInit() + self.output(u'策略初始化完成') + + # self.strategy.trading = True + # self.strategy.onStart() + # self.output(u'策略启动完成') + + self.output(u'开始载入数据') + + # 载入回测数据 + testdays = (self.dataEndDate - self.dataStartDate).days + + rawBars = [] + # 看本地缓存是否存在 + cachefilename = u'{0}_{1}_{2}'.format(self.symbol, self.dataStartDate.strftime('%Y%m%d'), + self.dataEndDate.strftime('%Y%m%d')) + rawBars = self.__loadTicksFromLocalCache(cachefilename) + + if len(rawBars) < 1: + self.writeCtaLog(u'从数据库中读取数据') + + query_time = datetime.now() + ds = DataSource(data_source_url) + start_date = self.dataStartDate.strftime('%Y-%m-%d') + end_date = self.dataEndDate.strftime('%Y-%m-%d') + fields = ['open', 'close', 'high', 'low', 'volume', 'open_interest', 'limit_up', 'limit_down', + 'trading_date'] + last_bar_dt = None + + df = ds.get_price(order_book_id=self.strategy.symbol, start_date=start_date, + end_date=end_date, frequency='1m', fields=fields) + + if df is None: + self.writeCtaLog(u'ERROR data_source拿不到数据,回测结束') + return + process_time = datetime.now() + # 将数据从查询指针中读取出,并生成列表 + count_bars = 0 + self.writeCtaLog(u'一共获取{}条{}分钟数据'.format(len(df), '1m')) + for idx in df.index: + row = df.loc[idx] + # self.writeCtaLog('{}: {}, o={}, h={}, l={}, c={}'.format(count_bars, datetime.strptime(str(idx), '%Y-%m-%d %H:%M:00'), + # row['open'], row['high'], row['low'], row['close'])) + bar = CtaBarData() + bar.vtSymbol = self.symbol + bar.symbol = self.symbol + last_bar_dt = datetime.strptime(str(idx), '%Y-%m-%d %H:%M:00') + bar.datetime = last_bar_dt - timedelta(minutes=1) + bar.date = bar.datetime.strftime('%Y-%m-%d') + bar.time = bar.datetime.strftime('%H:%M:00') + bar.tradingDay = datetime.strptime(str(int(row['trading_date'])), '%Y%m%d') + bar.open = float(row['open']) + bar.high = float(row['high']) + bar.low = float(row['low']) + bar.close = float(row['close']) + bar.volume = int(row['volume']) + rawBars.append(bar) + count_bars += 1 + + self.writeCtaLog(u'回测日期{}-{},数据量:{},查询耗时:{},回测耗时:{}' + .format(self.dataStartDate.strftime('%Y-%m-%d'), self.dataEndDate.strftime('%Y%m%d'), + count_bars, str(datetime.now() - query_time), + str(datetime.now() - process_time))) + + # 保存本地cache文件 + if count_bars > 0: + self.__saveTicksToLocalCache(cachefilename, rawBars) + + if len(rawBars) < 1: + self.writeCtaLog(u'ERROR 拿不到指定日期的数据,结束') + return + + self.output(u'开始回放数据') + last_tradingDay = 0 + for bar in rawBars: + # self.writeCtaLog(u'{} o:{};h:{};l:{};c:{},v:{},tradingDay:{},H2_count:{}' + # .format(bar.date+' '+bar.time, bar.open, bar.high, + # bar.low, bar.close, bar.volume, bar.tradingDay, self.lineH2.m1_bars_count)) + + if self.strategyStartDate <= bar.datetime <= self.dataEndDate: + if last_tradingDay == 0: + last_tradingDay = bar.tradingDay + elif last_tradingDay != bar.tradingDay: + if last_tradingDay is not None: + self.savingDailyData(last_tradingDay, self.capital, self.maxCapital, self.totalCommission) + last_tradingDay = bar.tradingDay + + # 第二个交易日,撤单 + self.cancelOrders(self.symbol) + # 更新持仓缓存 + self.update_pos_buffer() + + # Simulate latest tick and send it to Strategy + # simTick = self.__barToTick(bar) + # self.tick = simTick + # self.strategy.curTick = simTick + + # Check the order triggers and deliver the bar to the Strategy + if self.useBreakoutMode is False: + self.newBar(bar) + else: + self.newBarForBreakout(bar) + + if not self.strategy.trading and self.strategyStartDate < bar.datetime: + self.strategy.trading = True + self.strategy.onStart() + self.output(u'策略启动完成') + + if self.netCapital < 0: + self.writeCtaError(u'净值低于0,回测停止') + return + + # ---------------------------------------------------------------------- + def runBacktestingWithMysql(self): + """运行回测(使用Mysql数据) + added by IncenseLee + """ + self.capital = self.initCapital # 更新设置期初资金 + + if not self.dataStartDate: + self.writeCtaLog(u'回测开始日期未设置。') + return + + if not self.dataEndDate: + self.dataEndDate = datetime.today() + + if len(self.symbol) < 1: + self.writeCtaLog(u'回测对象未设置。') + return + + # 首先根据回测模式,确认要使用的数据类 + if self.mode == self.BAR_MODE: + dataClass = CtaBarData + func = self.newBar + else: + dataClass = CtaTickData + func = self.newTick + + self.output(u'开始回测') + + # self.strategy.inited = True + self.strategy.onInit() + self.output(u'策略初始化完成') + + self.strategy.trading = True + self.strategy.onStart() + self.output(u'策略启动完成') + + self.output(u'开始回放数据') + + # 每次获取日期周期 + intervalDays = 10 + + for i in range(0, (self.dataEndDate - self.dataStartDate).days + 1, intervalDays): + d1 = self.dataStartDate + timedelta(days=i) + + if (self.dataEndDate - d1).days > intervalDays: + d2 = self.dataStartDate + timedelta(days=i + intervalDays - 1) + else: + d2 = self.dataEndDate + + # 提取历史数据 + self.loadDataHistoryFromMysql(self.symbol, d1, d2) + + self.output(u'数据日期:{0} => {1}'.format(d1, d2)) + # 将逐笔数据推送 + for data in self.historyData: + # 记录最新的TICK数据 + self.tick = self.__dataToTick(data) + self.dt = self.tick.datetime + + # 处理限价单 + self.crossLimitOrder() + self.crossStopOrder() + + # 推送到策略引擎中 + self.strategy.onTick(self.tick) + + # 清空历史数据 + self.historyData = [] + + self.output(u'数据回放结束') + + # ---------------------------------------------------------------------- + def runBacktesting(self): + """运行回测""" + + self.capital = self.initCapital # 更新设置期初资金 + + # 首先根据回测模式,确认要使用的数据类 + if self.mode == self.BAR_MODE: + dataClass = CtaBarData + func = self.newBar + else: + dataClass = CtaTickData + func = self.newTick + + self.output(u'开始回测') + + self.strategy.inited = True + self.strategy.onInit() + self.output(u'策略初始化完成') + + self.strategy.trading = True + self.strategy.onStart() + self.output(u'策略启动完成') + + self.output(u'开始回放数据') + + # 循环加载回放数据 + self.runHistoryDataFromMongo() + + self.output(u'数据回放结束') + def __loadDataHistoryFromLocalCache(self, symbol, startDate, endDate): """看本地缓存是否存在 added by IncenseLee """ # 运行路径下cache子目录 - cacheFolder = os.getcwd()+'/cache' + cacheFolder = self.get_logs_path()+'/cache' # cache文件 cacheFile = u'{0}/{1}_{2}_{3}.pickle'.\ @@ -495,7 +1414,7 @@ class BacktestingEngine(object): """ # 运行路径下cache子目录 - cacheFolder = os.getcwd()+'/cache' + cacheFolder = self.get_logs_path()+'/cache' # 创建cache子目录 if not os.path.isdir(cacheFolder): @@ -707,124 +1626,13 @@ class BacktestingEngine(object): return tick - #---------------------------------------------------------------------- - def getMysqlDeltaDate(self,symbol, startDate, decreaseDays): - """从mysql库中获取交易日前若干天 - added by IncenseLee - """ - try: - if self.__mysqlConnected: - - # 获取mysql指针 - cur = self.__mysqlConnection.cursor() - - sqlstring='select distinct ndate from TB_{0}MI where ndate < ' \ - 'cast(\'{1}\' as date) order by ndate desc limit {2},1'.format(symbol, startDate, decreaseDays-1) - - # self.writeCtaLog(sqlstring) - - count = cur.execute(sqlstring) - - if count > 0: - - # 提取第一条记录 - result = cur.fetchone() - - return result[0] - - else: - self.writeCtaLog(u'MysqlDB没有查询结果,请检查日期') - - else: - self.writeCtaLog(u'MysqlDB未连接,请检查') - - except MySQLdb.Error as e: - self.writeCtaLog(u'MysqlDB载入数据失败,请检查.Error {0}: {1}'.format(e.arg[0],e.arg[1])) - - # 出错后缺省返回 - return startDate-timedelta(days=3) - - # ---------------------------------------------------------------------- - def runBackTestingWithArbTickFile(self,mainPath, arbSymbol): - """运行套利回测(使用本地tick TXT csv数据) - 参数:套利代码 SP rb1610&rb1701 - added by IncenseLee - 原始的tick,分别存放在白天目录1和夜盘目录2中,每天都有各个合约的数据 - Z:\ticks\SHFE\201606\RB\0601\ - RB1610.txt - RB1701.txt - .... - Z:\ticks\SHFE_night\201606\RB\0601 - RB1610.txt - RB1701.txt - .... - - 夜盘目录为自然日,不是交易日。 - - 按照回测的开始日期,到结束日期,循环每一天。 - 每天优先读取日盘数据,再读取夜盘数据。 - 读取eg1(如RB1610),读取Leg2(如RB701),合并成价差tick,灌输到策略的onTick中。 - """ - self.capital = self.initCapital # 更新设置期初资金 - - if len(arbSymbol) < 1: - self.writeCtaLog(u'套利合约为空') - return - - if not (arbSymbol.upper().index("SP") == 0 and arbSymbol.index(" ") > 0 and arbSymbol.index("&") > 0): - self.writeCtaLog(u'套利合约格式不符合') - return - - # 获得Leg1,leg2 - legs = arbSymbol[arbSymbol.index(" "):] - leg1 = legs[1:legs.index("&")] - leg2 = legs[legs.index("&") + 1:] - self.writeCtaLog(u'Leg1:{0},Leg2:{1}'.format(leg1, leg2)) - - if not self.dataStartDate: - self.writeCtaLog(u'回测开始日期未设置。') - return - # RB - if len(self.symbol)<1: - self.writeCtaLog(u'回测对象未设置。') - return - - if not self.dataEndDate: - self.dataEndDate = datetime.today() - - #首先根据回测模式,确认要使用的数据类 - if self.mode == self.BAR_MODE: - self.writeCtaLog(u'本回测仅支持tick模式') - return - - testdays = (self.dataEndDate - self.dataStartDate).days - - if testdays < 1: - self.writeCtaLog(u'回测时间不足') - return - - for i in range(0, testdays): - - testday = self.dataStartDate + timedelta(days = i) - - self.output(u'回测日期:{0}'.format(testday)) - - # 白天数据 - self.__loadArbTicks(mainPath,testday,leg1,leg2) - - # 撤销所有之前的orders - if self.symbol: - self.cancelOrders(self.symbol) - - # 夜盘数据 - self.__loadArbTicks(mainPath+'_night', testday, leg1, leg2) - def __loadArbTicks(self,mainPath,testday,leg1,leg2): + def __loadArbTicks(self, mainPath, testday, leg1, leg2): self.writeCtaLog(u'加载回测日期:{0}\{1}的价差tick'.format(mainPath, testday)) - cachefilename = u'{0}_{1}_{2}_{3}_{4}'.format(self.symbol,leg1,leg2, mainPath, testday.strftime('%Y%m%d')) + cachefilename = u'{0}_{1}_{2}_{3}_{4}'.format(self.symbol, leg1, leg2, mainPath, testday.strftime('%Y%m%d')) arbTicks = self.__loadArbTicksFromLocalCache(cachefilename) @@ -848,7 +1656,7 @@ class BacktestingEngine(object): leg2Ticks = {} leg2CsvReadFile = open(leg2File, 'rb') - #reader = csv.DictReader((line.replace('\0',' ') for line in leg2CsvReadFile), delimiter=",") + # reader = csv.DictReader((line.replace('\0',' ') for line in leg2CsvReadFile), delimiter=",") reader = csv.DictReader(leg2CsvReadFile, delimiter=",") self.writeCtaLog(u'加载{0}'.format(leg2File)) for row in reader: @@ -868,9 +1676,9 @@ class BacktestingEngine(object): continue # 修正毫秒 - if tick.datetime.replace(microsecond = 0) == dt: + if tick.datetime.replace(microsecond=0) == dt: # 与上一个tick的时间(去除毫秒后)相同,修改为500毫秒 - tick.datetime=tick.datetime.replace(microsecond = 500) + tick.datetime = tick.datetime.replace(microsecond=500) tick.time = tick.datetime.strftime('%H:%M:%S.%f') else: @@ -888,18 +1696,18 @@ class BacktestingEngine(object): # 排除涨停/跌停的数据 if (tick.bidPrice1 == float('1.79769E308') and tick.bidVolume1 == 0) \ - or (tick.askPrice1 == float('1.79769E308') and tick.askVolume1 == 0): + or (tick.askPrice1 == float('1.79769E308') and tick.askVolume1 == 0): continue dtStr = tick.date + ' ' + tick.time if dtStr in leg2Ticks: pass - #self.writeCtaError(u'日内数据重复,异常,数据时间为:{0}'.format(dtStr)) + # self.writeCtaError(u'日内数据重复,异常,数据时间为:{0}'.format(dtStr)) else: leg2Ticks[dtStr] = tick leg1CsvReadFile = file(leg1File, 'rb') - #reader = csv.DictReader((line.replace('\0',' ') for line in leg1CsvReadFile), delimiter=",") + # reader = csv.DictReader((line.replace('\0',' ') for line in leg1CsvReadFile), delimiter=",") reader = csv.DictReader(leg1CsvReadFile, delimiter=",") self.writeCtaLog(u'加载{0}'.format(leg1File)) @@ -1011,71 +1819,6 @@ class BacktestingEngine(object): cache.close() return True - # ---------------------------------------------------------------------- - - def runBackTestingWithTickFile(self, mainPath, symbol): - """运行Tick回测(使用本地tick TXT csv数据) - 参数:代码 rb1610 - added by WenjianDu - 原始的tick,分别存放在白天目录1和夜盘目录2中,每天都有各个合约的数据 - Z:\ticks\SHFE\201606\RB\0601\ - RB1610.txt - RB1701.txt - .... - Z:\ticks\SHFE_night\201606\RB\0601 - RB1610.txt - RB1701.txt - .... - - 夜盘目录为自然日,不是交易日。 - - 按照回测的开始日期,到结束日期,循环每一天。 - 每天优先读取日盘数据,再读取夜盘数据。 - 读取tick(如RB1610),灌输到策略的onTick中。 - """ - self.capital = self.initCapital # 更新设置期初资金 - - if len(symbol) < 1: - self.writeCtaLog(u'合约为空') - return - - # 获得tick - self.writeCtaLog(u'arbSymbol:{0}'.format(symbol)) - - if not self.dataStartDate: - self.writeCtaLog(u'回测开始日期未设置。') - return - # RB - if len(self.symbol) < 1: - self.writeCtaLog(u'回测对象未设置。') - return - - if not self.dataEndDate: - self.dataEndDate = datetime.today() - - # 首先根据回测模式,确认要使用的数据类 - if self.mode == self.BAR_MODE: - self.writeCtaLog(u'本回测仅支持tick模式') - return - - testdays = (self.dataEndDate - self.dataStartDate).days - - if testdays < 1: - self.writeCtaLog(u'回测时间不足') - return - - for i in range(0, testdays): - testday = self.dataStartDate + timedelta(days=i) - self.output(u'回测日期:{0}'.format(testday)) - # 白天数据 - self.__loadTxtTicks(mainPath, testday, symbol) - # 撤销所有之前的orders - if self.symbol: - self.cancelOrders(self.symbol) - # # 夜盘数据 - # self.__loadTxtTicks(mainPath + '_night', testday, symbol) - self.savingDailyData(testday, self.capital, self.maxCapital, self.totalCommission) - def __loadTxtTicks(self, mainPath, testday, symbol): self.writeCtaLog(u'加载回测日期:{0}\{1}的tick'.format(mainPath, testday)) @@ -1169,7 +1912,7 @@ class BacktestingEngine(object): def __loadTicksFromLocalCache(self, filename): """从本地缓存中,加载数据""" # 运行路径下cache子目录 - cacheFolder = os.getcwd() + '/cache' + cacheFolder = self.get_logs_path() + '/cache' # cacheFolder = '/home/wenjiand/Workspaces/huafu-vnpy/vnpy/trader/app/ctaStrategy/strategy/cache' # cache文件 @@ -1188,7 +1931,7 @@ class BacktestingEngine(object): def __saveTicksToLocalCache(self, filename, arbticks): """保存价差tick到本地缓存目录""" # 运行路径下cache子目录 - cacheFolder = os.getcwd() + '/cache' + cacheFolder = self.get_logs_path() + '/cache' # cacheFolder = '/home/wenjiand/Workspaces/huafu-vnpy/vnpy/trader/app/ctaStrategy/strategy/cache' # 创建cache子目录 @@ -1210,67 +1953,7 @@ class BacktestingEngine(object): cache.close() return True - # ---------------------------------------------------------------------- - def runBackTestingWithArbTickFile2(self, leg1MainPath,leg2MainPath, arbSymbol): - """运行套利回测(使用本地tick csv数据) - 参数:套利代码 SP rb1610&rb1701 - added by IncenseLee - 原始的tick,存放在相应市场下每天的目录中,目录包含市场各个合约的数据 - E:\ticks\SQ\201606\20160601\ - RB10.csv - RB01.csv - .... - 目录为交易日。 - 按照回测的开始日期,到结束日期,循环每一天。 - - 读取eg1(如RB1610),读取Leg2(如RB701),合并成价差tick,灌输到策略的onTick中。 - """ - self.capital = self.initCapital # 更新设置期初资金 - - if len(arbSymbol) < 1: - self.writeCtaLog(u'套利合约为空') - return - - if not (arbSymbol.upper().index("SP") == 0 and arbSymbol.index(" ") > 0 and arbSymbol.index("&") > 0): - self.writeCtaLog(u'套利合约格式不符合') - return - - # 获得Leg1,leg2 - legs = arbSymbol[arbSymbol.index(" "):] - leg1 = legs[1:legs.index("&")] - leg2 = legs[legs.index("&") + 1:] - self.writeCtaLog(u'Leg1:{0},Leg2:{1}'.format(leg1, leg2)) - - if not self.dataStartDate: - self.writeCtaLog(u'回测开始日期未设置。') - return - # RB - if len(self.symbol) < 1: - self.writeCtaLog(u'回测对象未设置。') - return - - if not self.dataEndDate: - self.dataEndDate = datetime.today() - - # 首先根据回测模式,确认要使用的数据类 - if self.mode == self.BAR_MODE: - self.writeCtaLog(u'本回测仅支持tick模式') - return - - testdays = (self.dataEndDate - self.dataStartDate).days - - if testdays < 1: - self.writeCtaLog(u'回测时间不足') - return - - for i in range(0, testdays): - testday = self.dataStartDate + timedelta(days=i) - - self.output(u'回测日期:{0}'.format(testday)) - - # 白天数据 - self.__loadArbTicks2(leg1MainPath, leg2MainPath, testday, leg1, leg2) def __loadArbTicks2(self, leg1MainPath, leg2MainPath, testday, leg1Symbol, leg2Symbol): """加载taobao csv格式tick产生的价差合约""" @@ -1353,74 +2036,6 @@ class BacktestingEngine(object): # 推送到策略中 self.newTick(t) - def runBackTestingWithNonStrArbTickFile(self, leg1MainPath, leg2MainPath, leg1Symbol,leg2Symbol): - """运行套利回测(使用本地tick txt数据) - 参数: - leg1MainPath: leg1合约所在的市场路径 - leg2MainPath: leg2合约所在的市场路径 - leg1Symbol: leg1合约 - Leg2Symbol:leg2合约 - added by IncenseLee - 原始的tick,分别存放在白天目录1和夜盘目录2中,每天都有各个合约的数据 - Z:\ticks\SHFE\201606\RB\0601\ - RB1610.txt - RB1701.txt - .... - Z:\ticks\SHFE_night\201606\RB\0601 - RB1610.txt - RB1701.txt - .... - - 夜盘目录为自然日,不是交易日。 - - 按照回测的开始日期,到结束日期,循环每一天。 - 每天优先读取日盘数据,再读取夜盘数据。 - 读取eg1(如RB1610),读取Leg2(如RB701),根据两者tick的时间优先顺序,逐一tick灌输到策略的onTick中。 - """ - self.capital = self.initCapital # 更新设置期初资金 - - if not self.dataStartDate: - self.writeCtaLog(u'回测开始日期未设置。') - return - # RB - if len(self.symbol)<1: - self.writeCtaLog(u'回测对象未设置。') - return - - if not self.dataEndDate: - self.dataEndDate = datetime.today() - - #首先根据回测模式,确认要使用的数据类 - if self.mode == self.BAR_MODE: - self.writeCtaLog(u'本回测仅支持tick模式') - return - - testdays = (self.dataEndDate - self.dataStartDate).days - - if testdays < 1: - self.writeCtaLog(u'回测时间不足') - return - - for i in range(0, testdays): - testday = self.dataStartDate + timedelta(days = i) - - self.output(u'回测日期:{0}'.format(testday)) - # 撤销所有之前的orders - if self.symbol: - self.cancelOrders(self.symbol) - if self.last_leg1_tick: - self.cancelOrders(self.last_leg1_tick.vtSymbol) - if self.last_leg2_tick: - self.cancelOrders(self.last_leg2_tick.vtSymbol) - # 加载运行白天数据 - self.__loadNotStdArbTicks(leg1MainPath, leg2MainPath, testday, leg1Symbol,leg2Symbol) - - self.savingDailyData(testday, self.capital, self.maxCapital,self.totalCommission) - - # 加载运行夜盘数据 - self.__loadNotStdArbTicks(leg1MainPath+'_night', leg2MainPath+'_night', testday, leg1Symbol, leg2Symbol) - - self.savingDailyData(self.dataEndDate, self.capital, self.maxCapital,self.totalCommission) def __loadTicksFromTxtFile(self, filepath, tickDate, vtSymbol): """从文件中读取tick""" @@ -1554,66 +2169,6 @@ class BacktestingEngine(object): self.newTick(leg2) leg2_tick = None - def runBackTestingWithNonStrArbTickFile2(self, leg1MainPath, leg2MainPath, leg1Symbol, leg2Symbol): - """运行套利回测(使用本地tickcsv数据,数据从taobao标普购买) - 参数: - leg1MainPath: leg1合约所在的市场路径 - leg2MainPath: leg2合约所在的市场路径 - leg1Symbol: leg1合约 - Leg2Symbol:leg2合约 - added by IncenseLee - 原始的tick,存放在相应市场下每天的目录中,目录包含市场各个合约的数据 - E:\ticks\SQ\201606\20160601\ - RB10.csv - RB01.csv - .... - - 目录为交易日。 - 按照回测的开始日期,到结束日期,循环每一天。 - - 读取eg1(如RB1610),读取Leg2(如RB701),根据两者tick的时间优先顺序,逐一tick灌输到策略的onTick中。 - """ - self.capital = self.initCapital # 更新设置期初资金 - - if not self.dataStartDate: - self.writeCtaLog(u'回测开始日期未设置。') - return - # RB - if len(self.symbol) < 1: - self.writeCtaLog(u'回测对象未设置。') - return - - if not self.dataEndDate: - self.dataEndDate = datetime.today() - - # 首先根据回测模式,确认要使用的数据类 - if self.mode == self.BAR_MODE: - self.writeCtaLog(u'本回测仅支持tick模式') - return - - testdays = (self.dataEndDate - self.dataStartDate).days - - if testdays < 1: - self.writeCtaLog(u'回测时间不足') - return - - for i in range(0, testdays): - testday = self.dataStartDate + timedelta(days=i) - - self.output(u'回测日期:{0}'.format(testday)) - # 撤销所有之前的orders - if self.symbol: - self.cancelOrders(self.symbol) - if self.last_leg1_tick: - self.cancelOrders(self.last_leg1_tick.vtSymbol) - if self.last_leg2_tick: - self.cancelOrders(self.last_leg2_tick.vtSymbol) - - # 加载运行每天数据 - self.__loadNotStdArbTicks2(leg1MainPath, leg2MainPath, testday, leg1Symbol, leg2Symbol) - - self.savingDailyData(testday, self.capital, self.maxCapital,self.totalCommission) - def __loadTicksFromCsvFile(self, filepath, tickDate, vtSymbol): """从csv文件中UnicodeDictReader读取tick""" @@ -1689,8 +2244,80 @@ class BacktestingEngine(object): return ticks - def __loadNotStdArbTicks2(self, leg1MainPath, leg2MainPath, testday, leg1Symbol, leg2Symbol): + def __load_rq_ticks_from_csv(self, filepath, tickDate, vtSymbol): + """从rq csv文件中读取tick""" + # 先读取数据到Dict,以日期时间为key + ticks = OrderedDict() + trading_date = tickDate.strftime('%Y-%m-%d') + if not os.path.isfile(filepath): + self.writeCtaLog(u'{0}文件不存在'.format(filepath)) + return ticks + dt = None + csvReadFile = open(filepath, 'rb') + df = pd.read_csv(filepath,parse_dates=False) + if df is None: + return ticks + + self.writeCtaLog(u'加载{},共{}条数据'.format(filepath,len(df))) + dt1 = datetime.now() + + for row in df.itertuples(): + if trading_date!=row.trading_date: + continue + tick = CtaTickData() + tick.vtSymbol = vtSymbol + tick.symbol = vtSymbol + + dt_str = row.index + + if len(dt_str) == 0: + continue + tick.datetime = datetime.strptime(dt_str, '%Y-%m-%d %H:%M:%S.%f') + + tick.date = tick.datetime.strftime('%Y-%m-%d') + tick.tradingDay = row.trading_date + tick.time = tick.datetime.strftime('%H:%M:%S.%f') + + dt = tick.datetime + + tick.lastPrice = float(row.last) + tick.volume = int(float(row.volume)) + tick.bidPrice1 = float(row.b1) # 叫买价(价格低) + tick.bidVolume1 = int(float(row.b1_v)) + tick.askPrice1 = float(row.a1) # 叫卖价(价格高) + tick.askVolume1 = int(float(row.a1_v)) + tick.upperLimit = float(row.limit_up) + tick.lowerLimit = float(row.limit_down) + + # 排除涨停/跌停的数据 + if (tick.bidPrice1 == tick.upperLimit) \ + or (tick.askPrice1 == tick.lowerLimit): + self.writeCtaLog(u'排除日内涨跌停数据:{}'.format(row)) + continue + + dtStr = tick.date + ' ' + tick.time + if dtStr in ticks: + pass + + #self.writeCtaError(u'日内数据重复,异常,数据时间为:{0}'.format(dtStr)) + else: + ticks[dtStr] = tick + + load_seconds = (datetime.now()-dt1).total_seconds() + self.writeCtaLog(u'加载耗时:{}秒'.format(load_seconds)) + return ticks + + def __run_arbitrage_ticks2(self, leg1MainPath, leg2MainPath, testday, leg1Symbol, leg2Symbol): + """ + 运行tick套利 + :param leg1MainPath: 腿1得路径 + :param leg2MainPath: 腿2得路径 + :param testday: 测试日期 + :param leg1Symbol: 腿1合约 + :param leg2Symbol: 腿2合约 + :return: + """ self.writeCtaLog(u'加载回测日期:{0}的价差tick'.format(testday)) p = re.compile(r"([A-Z]+)[0-9]+",re.I) leg1_shortSymbol = p.match(leg1Symbol) @@ -1703,29 +2330,25 @@ class BacktestingEngine(object): leg1_shortSymbol = leg1_shortSymbol.group(1) leg2_shortSymbol = leg2_shortSymbol.group(1) - # E:\Ticks\SQ\2014\201401\20140102\ag01_20140102.csv #leg1File = u'e:\\ticks\\{0}\\{1}\\{2}\\{3}\\{4}{5}_{3}.csv' \ # .format(leg1MainPath, testday.strftime('%Y'), testday.strftime('%Y%m'), testday.strftime('%Y%m%d'), leg1_shortSymbol, leg1Symbol[-2:]) leg1File = os.path.abspath( os.path.join(leg1MainPath, testday.strftime('%Y'), testday.strftime('%Y%m'), testday.strftime('%Y%m%d'), - '{0}{1}_{2}.csv'.format(leg1_shortSymbol,leg1Symbol[-2:],testday.strftime('%Y%m%d')))) + '{0}{1}_{2}.csv'.format(leg1_shortSymbol.upper(),leg1Symbol[-2:],testday.strftime('%Y%m%d')))) if not os.path.isfile(leg1File): self.writeCtaLog(u'{0}文件不存在'.format(leg1File)) return - #leg2File = u'e:\\ticks\\{0}\\{1}\\{2}\\{3}\\{4}{5}_{3}.csv' \ - # .format(leg2MainPath,testday.strftime('%Y'), testday.strftime('%Y%m'), testday.strftime('%Y%m%d'), leg2_shortSymbol, leg2Symbol[-2:]) leg2File = os.path.abspath( os.path.join(leg1MainPath, testday.strftime('%Y'), testday.strftime('%Y%m'), testday.strftime('%Y%m%d'), - '{0}{1}_{2}.csv'.format(leg2_shortSymbol, leg2Symbol[-2:], testday.strftime('%Y%m%d')))) + '{0}{1}_{2}.csv'.format(leg2_shortSymbol.upper(), leg2Symbol[-2:], testday.strftime('%Y%m%d')))) if not os.path.isfile(leg2File): self.writeCtaLog(u'{0}文件不存在'.format(leg2File)) return - leg1Ticks = self.__loadTicksFromCsvFile(filepath=leg1File, tickDate=testday, vtSymbol=leg1Symbol) if len(leg1Ticks) == 0: self.writeCtaLog(u'{0}读取tick数为空'.format(leg1File)) @@ -1736,6 +2359,7 @@ class BacktestingEngine(object): self.writeCtaLog(u'{0}读取tick数为空'.format(leg1File)) return + leg1_tick = None leg2_tick = None @@ -1765,64 +2389,6 @@ class BacktestingEngine(object): self.newTick(leg2) leg2_tick = None - def runBackTestingWithNonStrArbTickFromMongoDB(self, leg1Symbol, leg2Symbol): - """运行套利回测(使用服务器数据,数据从taobao标普购买) - 参数: - leg1Symbol: leg1合约 - Leg2Symbol:leg2合约 - added by IncenseLee - - 目录为交易日。 - 按照回测的开始日期,到结束日期,循环每一天。 - - 读取eg1(如RB1610),读取Leg2(如RB1701),根据两者tick的时间优先顺序,逐一tick灌输到策略的onTick中。 - """ - - # 连接数据库 - host, port, log = loadMongoSetting() - self.dbClient = pymongo.MongoClient(host, port) - - self.capital = self.initCapital # 更新设置期初资金 - - if not self.dataStartDate: - self.writeCtaLog(u'回测开始日期未设置。') - return - # RB - if len(self.symbol) < 1: - self.writeCtaLog(u'回测对象未设置。') - return - - if not self.dataEndDate: - self.dataEndDate = datetime.today() - - # 首先根据回测模式,确认要使用的数据类 - if self.mode == self.BAR_MODE: - self.writeCtaLog(u'本回测仅支持tick模式') - return - - testdays = (self.dataEndDate - self.dataStartDate).days - - if testdays < 1: - self.writeCtaLog(u'回测时间不足') - return - - for i in range(0, testdays): - testday = self.dataStartDate + timedelta(days=i) - - self.output(u'回测日期:{0}'.format(testday)) - - # 撤销所有之前的orders - if self.symbol: - self.cancelOrders(self.symbol) - if self.last_leg1_tick: - self.cancelOrders(self.last_leg1_tick.vtSymbol) - if self.last_leg2_tick: - self.cancelOrders(self.last_leg2_tick.vtSymbol) - # 加载运行每天数据 - self.__loadNotStdArbTicksFromMongoDB( testday, leg1Symbol, leg2Symbol) - - self.savingDailyData(testday, self.capital, self.maxCapital,self.totalCommission) - def __loadNotStdArbTicksFromMongoDB(self,testday, leg1Symbol, leg2Symbol): self.writeCtaLog(u'从MongoDB加载回测日期:{0}的{1}-{2}价差tick'.format(testday,leg1Symbol, leg2Symbol)) @@ -1910,379 +2476,98 @@ class BacktestingEngine(object): return ticks - #---------------------------------------------------------------------- - def runBackTestingWithBarFile(self, filename): - """运行回测(使用本地csv数据) - added by IncenseLee + # ---------------------------------------------------------------------- + + def __run_rq_ticks(self, leg1_path, leg2_path, testday, leg1_symbol, leg2_symbol): """ - self.capital = self.initCapital # 更新设置期初资金 - if not filename: - self.writeCtaLog(u'请指定回测数据文件') - return - - if not self.strategyStartDate: - self.writeCtaLog(u'回测开始日期未设置。') - return - - if not self.dataEndDate: - self.dataEndDate = datetime.today() - - import os - if not os.path.isfile(filename): - self.writeCtaLog(u'{0}文件不存在'.format(filename)) - - if len(self.symbol) < 1: - self.writeCtaLog(u'回测对象未设置。') - return - - # 首先根据回测模式,确认要使用的数据类 - if not self.mode == self.BAR_MODE: - self.writeCtaLog(u'文件仅支持bar模式,若扩展tick模式,需要修改本方法') - return - - #self.output(u'开始回测') - - self.strategy.onInit() - self.strategy.trading = False - self.output(u'策略初始化完成') - - self.output(u'开始回放数据') - - import csv - csvfile = open(filename,'r',encoding='utf8') - reader = csv.DictReader((line.replace('\0', '') for line in csvfile), delimiter=",") - last_tradingDay = None - for row in reader: - try: - bar = CtaBarData() - bar.symbol = self.symbol - bar.vtSymbol = self.symbol - - # 从tb导出的csv文件 - #bar.open = float(row['Open']) - #bar.high = float(row['High']) - #bar.low = float(row['Low']) - #bar.close = float(row['Close']) - #bar.volume = float(row['TotalVolume'])# - #barEndTime = datetime.strptime(row['Date']+' ' + row['Time'], '%Y/%m/%d %H:%M:%S') - if row.get('open',None) is None: - continue - if row.get('high', None) is None: - continue - if row.get('low', None) is None: - continue - if row.get('close', None) is None: - continue - - if len(row['open'])==0 or len(row['high'])==0 or len(row['low'])==0 or len(row['close'])==0: - continue - # 从ricequant导出的csv文件 - bar.open = self.roundToPriceTick(float(row['open'])) - bar.high = self.roundToPriceTick(float(row['high'])) - bar.low = self.roundToPriceTick(float(row['low'])) - bar.close = self.roundToPriceTick(float(row['close'])) - bar.volume = float(row['volume']) if len(row['volume'])>0 else 0 - if '-' in row['index']: - barEndTime = datetime.strptime(row['index'], '%Y-%m-%d %H:%M:%S') - elif '-' in row['datetime']: - barEndTime = datetime.strptime(row['datetime'], '%Y-%m-%d %H:%M:%S') - else: - barEndTime = datetime.strptime(row['datetime'], '%Y%m%d%H%M%S') - - # 使用Bar的开始时间作为datetime - bar.datetime = barEndTime - timedelta(seconds=self.barTimeInterval) - - bar.date = bar.datetime.strftime('%Y-%m-%d') - bar.time = bar.datetime.strftime('%H:%M:%S') - if 'trading_date' in row: - if len(row['trading_date']) is 8: - bar.tradingDay = row['trading_date'][0:4] + '-' + row['trading_date'][4:6] + '-' + row['trading_date'][6:] - else: - bar.tradingDay = row['trading_date'] - else: - if bar.datetime.hour >=21 and not self.is_7x24: - if bar.datetime.isoweekday() == 5: - # 星期五=》星期一 - bar.tradingDay = (barEndTime + timedelta(days=3)).strftime('%Y-%m-%d') - else: - # 第二天 - bar.tradingDay = (barEndTime + timedelta(days=1)).strftime('%Y-%m-%d') - elif bar.datetime.hour < 8 and bar.datetime.isoweekday() == 6 and not self.is_7x24: - # 星期六=>星期一 - bar.tradingDay = (barEndTime + timedelta(days=2)).strftime('%Y-%m-%d') - else: - bar.tradingDay = bar.date - - if self.strategyStartDate <= bar.datetime <= self.dataEndDate: - if last_tradingDay != bar.tradingDay: - if last_tradingDay is not None: - self.savingDailyData(datetime.strptime(last_tradingDay, '%Y-%m-%d'), self.capital, - self.maxCapital,self.totalCommission,benchmark=bar.close) - last_tradingDay = bar.tradingDay - - if self.dataStartDate >= bar.datetime: - continue - - if bar.datetime > self.dataEndDate: - continue - - # Check the order triggers and deliver the bar to the Strategy - if self.useBreakoutMode is False: - self.newBar(bar) - else: - self.newBarForBreakout(bar) - - if not self.strategy.trading and self.strategyStartDate < bar.datetime: - self.strategy.trading = True - self.strategy.onStart() - self.output(u'策略启动完成') - - if self.netCapital < 0: - self.writeCtaError(u'净值低于0,回测停止') - return - - except Exception as ex: - self.writeCtaError(u'回测异常导致停止') - self.writeCtaError(u'{},{}'.format(str(ex),traceback.format_exc())) - return - - #---------------------------------------------------------------------- - def runBackTestingWithDataSource(self): - """运行回测(使用本地csv数据) - added by IncenseLee + 运行ricequant得本地tick + :param leg1_path: + :param leg2_path: + :param testday: + :param leg1_symbol: + :param leg2_symbol: + :return: """ - self.capital = self.initCapital # 更新设置期初资金 - if not self.strategyStartDate: - self.writeCtaLog(u'回测开始日期未设置。') - return + self.writeCtaLog(u'加载回测日期:{0}的价差tick'.format(testday)) + p = re.compile(r"([A-Z]+)[0-9]+", re.I) + leg1_shortSymbol = p.match(leg1_symbol) + leg2_shortSymbol = p.match(leg2_symbol) - if not self.dataEndDate: - self.dataEndDate = datetime.today() + if leg1_shortSymbol is None or leg2_shortSymbol is None: + self.writeCtaLog(u'{0},{1}不能正则分解'.format(leg1_symbol, leg2_symbol)) + return False - if len(self.symbol) < 1: - self.writeCtaLog(u'回测对象未设置。') - return + leg1_shortSymbol = leg1_shortSymbol.group(1) + leg2_shortSymbol = leg2_shortSymbol.group(1) - # 首先根据回测模式,确认要使用的数据类 - if not self.mode == self.BAR_MODE: - self.writeCtaLog(u'文件仅支持bar模式,若扩展tick模式,需要修改本方法') - return + # E:\Ticks\SQ\2014\201401\20140102\ag01_20140102.csv + # leg1File = u'e:\\ticks\\{0}\\{1}\\{2}\\{3}\\{4}{5}_{3}.csv' \ + # .format(leg1MainPath, testday.strftime('%Y'), testday.strftime('%Y%m'), testday.strftime('%Y%m%d'), leg1_shortSymbol, leg1Symbol[-2:]) - # self.output(u'开始回测') + leg1File = os.path.abspath( + os.path.join(leg1_path, testday.strftime('%Y'), testday.strftime('%Y%m'), testday.strftime('%Y%m%d'), + '{}_{}.csv'.format(leg1_symbol.upper(),testday.strftime('%Y%m%d')))) - # self.strategy.inited = True - self.strategy.onInit() - self.output(u'策略初始化完成') + if not os.path.isfile(leg1File): + self.writeCtaLog(u'{0}文件不存在'.format(leg1File)) + return False - #self.strategy.trading = True - #self.strategy.onStart() - #self.output(u'策略启动完成') + # leg2File = u'e:\\ticks\\{0}\\{1}\\{2}\\{3}\\{4}{5}_{3}.csv' \ + # .format(leg2MainPath,testday.strftime('%Y'), testday.strftime('%Y%m'), testday.strftime('%Y%m%d'), leg2_shortSymbol, leg2Symbol[-2:]) + leg2File = os.path.abspath( + os.path.join(leg2_path, testday.strftime('%Y'), testday.strftime('%Y%m'), testday.strftime('%Y%m%d'), + '{}_{}.csv'.format(leg2_symbol.upper(),testday.strftime('%Y%m%d')))) - self.output(u'开始载入数据') + if not os.path.isfile(leg2File): + self.writeCtaLog(u'{0}文件不存在'.format(leg2File)) + return False - # 载入回测数据 - testdays = (self.dataEndDate - self.dataStartDate).days + leg1Ticks = self.__load_rq_ticks_from_csv(filepath=leg1File, tickDate=testday, vtSymbol=leg1_symbol) + if len(leg1Ticks) == 0: + self.writeCtaLog(u'{0}读取tick数为空'.format(leg1File)) + return False - rawBars = [] - # 看本地缓存是否存在 - cachefilename = u'{0}_{1}_{2}'.format(self.symbol, self.dataStartDate.strftime('%Y%m%d'), - self.dataEndDate.strftime('%Y%m%d')) - rawBars = self.__loadTicksFromLocalCache(cachefilename) + leg2Ticks = self.__load_rq_ticks_from_csv(filepath=leg2File, tickDate=testday, vtSymbol=leg2_symbol) + if len(leg2Ticks) == 0: + self.writeCtaLog(u'{0}读取tick数为空'.format(leg1File)) + return False - if len(rawBars) < 1: - self.writeCtaLog(u'从数据库中读取数据') + leg1_tick = None + leg2_tick = None + dt1 = datetime.now() + while not (len(leg1Ticks) == 0 or len(leg2Ticks) == 0): + if leg1_tick is None and len(leg1Ticks) > 0: + leg1_tick = leg1Ticks.popitem(last=False) + if leg2_tick is None and len(leg2Ticks) > 0: + leg2_tick = leg2Ticks.popitem(last=False) - query_time = datetime.now() - ds = DataSource() - start_date = self.dataStartDate.strftime('%Y-%m-%d') - end_date = self.dataEndDate.strftime('%Y-%m-%d') - fields = ['open', 'close', 'high', 'low', 'volume', 'open_interest', 'limit_up', 'limit_down', - 'trading_date'] - last_bar_dt = None + if leg1_tick is None and leg2_tick is not None: + self.newTick(leg2_tick[1]) + self.last_leg2_tick = leg2_tick[1] + leg2_tick = None + elif leg1_tick is not None and leg2_tick is None: + self.newTick(leg1_tick[1]) + self.last_leg1_tick = leg1_tick[1] + leg1_tick = None + elif leg1_tick is not None and leg2_tick is not None: + leg1 = leg1_tick[1] + leg2 = leg2_tick[1] + self.last_leg1_tick = leg1_tick[1] + self.last_leg2_tick = leg2_tick[1] + if leg1.datetime <= leg2.datetime: + self.newTick(leg1) + leg1_tick = None + else: + self.newTick(leg2) + leg2_tick = None + process_seconds = (datetime.now()-dt1).total_seconds() + self.writeCtaLog(u'推送{}tick耗时:{}秒'.format(testday,process_seconds)) + return True - df = ds.get_price(order_book_id=self.strategy.symbol, start_date=start_date, - end_date=end_date, frequency='1m', fields=fields) - - process_time = datetime.now() - # 将数据从查询指针中读取出,并生成列表 - count_bars = 0 - self.writeCtaLog(u'一共获取{}条{}分钟数据'.format(len(df), '1m')) - for idx in df.index: - row = df.loc[idx] - # self.writeCtaLog('{}: {}, o={}, h={}, l={}, c={}'.format(count_bars, datetime.strptime(str(idx), '%Y-%m-%d %H:%M:00'), - # row['open'], row['high'], row['low'], row['close'])) - bar = CtaBarData() - bar.vtSymbol = self.symbol - bar.symbol = self.symbol - last_bar_dt = datetime.strptime(str(idx), '%Y-%m-%d %H:%M:00') - bar.datetime = last_bar_dt - timedelta(minutes=1) - bar.date = bar.datetime.strftime('%Y-%m-%d') - bar.time = bar.datetime.strftime('%H:%M:00') - bar.tradingDay = datetime.strptime(str(int(row['trading_date'])), '%Y%m%d') - bar.open = float(row['open']) - bar.high = float(row['high']) - bar.low = float(row['low']) - bar.close = float(row['close']) - bar.volume = int(row['volume']) - rawBars.append(bar) - count_bars += 1 - - self.writeCtaLog(u'回测日期{}-{},数据量:{},查询耗时:{},回测耗时:{}' - .format(self.dataStartDate.strftime('%Y-%m-%d'), self.dataEndDate.strftime('%Y%m%d'), - count_bars, str(datetime.now() - query_time), str(datetime.now() - process_time))) - - # 保存本地cache文件 - if count_bars > 0: - self.__saveTicksToLocalCache(cachefilename, rawBars) - - if len(rawBars) < 1: - self.writeCtaLog(u'ERROR 拿不到指定日期的数据,结束') - return - - self.output(u'开始回放数据') - last_tradingDay = 0 - for bar in rawBars: - # self.writeCtaLog(u'{} o:{};h:{};l:{};c:{},v:{},tradingDay:{},H2_count:{}' - # .format(bar.date+' '+bar.time, bar.open, bar.high, - # bar.low, bar.close, bar.volume, bar.tradingDay, self.lineH2.m1_bars_count)) - - if self.strategyStartDate <= bar.datetime <= self.dataEndDate: - if last_tradingDay == 0: - last_tradingDay = bar.tradingDay - elif last_tradingDay != bar.tradingDay: - if last_tradingDay is not None: - self.savingDailyData(last_tradingDay, self.capital, self.maxCapital, self.totalCommission) - last_tradingDay = bar.tradingDay - - # Simulate latest tick and send it to Strategy - #simTick = self.__barToTick(bar) - # self.tick = simTick - #self.strategy.curTick = simTick - - # Check the order triggers and deliver the bar to the Strategy - if self.useBreakoutMode is False: - self.newBar(bar) - else: - self.newBarForBreakout(bar) - - if not self.strategy.trading and self.strategyStartDate < bar.datetime: - self.strategy.trading = True - self.strategy.onStart() - self.output(u'策略启动完成') - - if self.netCapital < 0: - self.writeCtaError(u'净值低于0,回测停止') - return - - #---------------------------------------------------------------------- - def runBacktestingWithMysql(self): - """运行回测(使用Mysql数据) - added by IncenseLee - """ - self.capital = self.initCapital # 更新设置期初资金 - - if not self.dataStartDate: - self.writeCtaLog(u'回测开始日期未设置。') - return - - if not self.dataEndDate: - self.dataEndDate = datetime.today() - - if len(self.symbol)<1: - self.writeCtaLog(u'回测对象未设置。') - return - - # 首先根据回测模式,确认要使用的数据类 - if self.mode == self.BAR_MODE: - dataClass = CtaBarData - func = self.newBar - else: - dataClass = CtaTickData - func = self.newTick - - self.output(u'开始回测') - - #self.strategy.inited = True - self.strategy.onInit() - self.output(u'策略初始化完成') - - self.strategy.trading = True - self.strategy.onStart() - self.output(u'策略启动完成') - - self.output(u'开始回放数据') - - # 每次获取日期周期 - intervalDays = 10 - - for i in range (0,(self.dataEndDate - self.dataStartDate).days +1, intervalDays): - d1 = self.dataStartDate + timedelta(days = i ) - - if (self.dataEndDate - d1).days > intervalDays: - d2 = self.dataStartDate + timedelta(days = i + intervalDays -1 ) - else: - d2 = self.dataEndDate - - # 提取历史数据 - self.loadDataHistoryFromMysql(self.symbol, d1, d2) - - self.output(u'数据日期:{0} => {1}'.format(d1,d2)) - # 将逐笔数据推送 - for data in self.historyData: - - # 记录最新的TICK数据 - self.tick = self.__dataToTick(data) - self.dt = self.tick.datetime - - # 处理限价单 - self.crossLimitOrder() - self.crossStopOrder() - - # 推送到策略引擎中 - self.strategy.onTick(self.tick) - - # 清空历史数据 - self.historyData = [] - - self.output(u'数据回放结束') - - #---------------------------------------------------------------------- - def runBacktesting(self): - """运行回测""" - - self.capital = self.initCapital # 更新设置期初资金 - - # 首先根据回测模式,确认要使用的数据类 - if self.mode == self.BAR_MODE: - dataClass = CtaBarData - func = self.newBar - else: - dataClass = CtaTickData - func = self.newTick - - self.output(u'开始回测') - - self.strategy.inited = True - self.strategy.onInit() - self.output(u'策略初始化完成') - - self.strategy.trading = True - self.strategy.onStart() - self.output(u'策略启动完成') - - self.output(u'开始回放数据') - - # 循环加载回放数据 - self.runHistoryDataFromMongo() - - - self.output(u'数据回放结束') - - # ---------------------------------------------------------------------- def runHistoryDataFromMongo(self): """ 根据测试的每一天,从MongoDB载入历史数据,并推送Tick至回测函数 - :return: + :return: """ host, port, log = loadMongoSetting() @@ -2313,8 +2598,9 @@ class BacktestingEngine(object): # 循环每一天 for i in range(0, testdays): testday = self.dataStartDate + timedelta(days=i) - testday_monrning = testday #testday.replace(hour=0, minute=0, second=0, microsecond=0) - testday_midnight = testday + timedelta(days=1) #testday.replace(hour=23, minute=59, second=59, microsecond=999999) + testday_monrning = testday # testday.replace(hour=0, minute=0, second=0, microsecond=0) + testday_midnight = testday + timedelta( + days=1) # testday.replace(hour=23, minute=59, second=59, microsecond=999999) query_time = datetime.now() # 载入初始化需要用的数据 @@ -2337,7 +2623,7 @@ class BacktestingEngine(object): .format(testday.strftime('%Y-%m-%d'), count_ticks, str(datetime.now() - query_time), str(datetime.now() - process_time))) # 记录每日净值 - self.savingDailyData(testday, self.capital, self.maxCapital,self.totalCommission) + self.savingDailyData(testday, self.capital, self.maxCapital, self.totalCommission) def __sendOnBarEvent(self, bar): """发送Bar的事件""" @@ -2354,6 +2640,13 @@ class BacktestingEngine(object): self.dt = bar.datetime self.crossLimitOrder() # 先撮合限价单 self.crossStopOrder() # 再撮合停止单 + + self.setPrice(bar.vtSymbol,bar.close) + + # 更新资金曲线(只有持仓时,才更新) + if self.fund_kline and (len(self.longPosition)>0 or len(self.shortPosition) > 0) : + self.fund_kline.update_account(self.dt, self.netCapital) + self.strategy.onBar(bar) # 推送K线到策略中 self.__sendOnBarEvent(bar) # 推送K线到事件 self.last_bar = bar @@ -2363,6 +2656,12 @@ class BacktestingEngine(object): """新的K线""" self.bar = bar self.dt = bar.datetime + self.setPrice(bar.vtSymbol, bar.close) + + # 更新资金曲线(只有持仓时,才更新) + if self.fund_kline and (len(self.longPosition)>0 or len(self.shortPosition) > 0): + self.fund_kline.update_account(self.dt, self.netCapital) + self.strategy.onBar(bar) # 推送K线到策略中 self.crossLimitOrder() # 先撮合限价单 self.crossStopOrder() # 再撮合停止单 @@ -2376,6 +2675,12 @@ class BacktestingEngine(object): self.dt = tick.datetime self.crossLimitOrder() self.crossStopOrder() + self.setPrice(tick.vtSymbol, tick.lastPrice) + + # 更新资金曲线(只有持仓时,才更新) + if self.fund_kline and (len(self.longPosition)>0 or len(self.shortPosition) > 0): + self.fund_kline.update_account(self.dt, self.netCapital) + self.strategy.onTick(tick) # ---------------------------------------------------------------------- @@ -2384,10 +2689,13 @@ class BacktestingEngine(object): 初始化策略 setting是策略的参数设置,如果使用类中写好的默认设置则可以不传该参数 """ + self.fund_renko = setting.pop('use_renko',False) self.strategy = strategyClass(self, setting) if not self.strategy.name: self.strategy.name = self.strategy.className + self.create_fund_kline() + if setting.get('auto_init',False): self.strategy.onInit() if setting.get('auto_start',False): @@ -2456,7 +2764,9 @@ class BacktestingEngine(object): """撤销所有单""" # Symbol参数:指定合约的撤单; # OFFSET参数:指定Offset的撤单,缺省不填写时,为所有 - self.writeCtaLog(u'从所有订单中撤销{0}\{1}'.format(offset, symbol)) + if len(self.workingLimitOrderDict) > 0: + self.writeCtaLog(u'从所有订单中撤销{0}\{1}'.format(offset, symbol)) + for vtOrderID in self.workingLimitOrderDict.keys(): order = self.workingLimitOrderDict[vtOrderID] @@ -2584,7 +2894,7 @@ class BacktestingEngine(object): self.tradeDict[tradeID] = trade self.writeCtaLog(u'TradeId:{0}'.format(tradeID)) - # 更新持仓缓存数据 # TODO: do we need this? + # 更新持仓缓存数据 posBuffer = self.posBufferDict.get(trade.vtSymbol, None) if not posBuffer: posBuffer = PositionBuffer() @@ -2664,7 +2974,7 @@ class BacktestingEngine(object): self.tradeDict[tradeID] = trade - # 更新持仓缓存数据 # TODO: do we need this? + # 更新持仓缓存数据 posBuffer = self.posBufferDict.get(trade.vtSymbol, None) if not posBuffer: posBuffer = PositionBuffer() @@ -2702,6 +3012,19 @@ class BacktestingEngine(object): if self.calculateMode == self.REALTIME_MODE: self.realtimeCalculate() + def update_pos_buffer(self): + """更新持仓信息,把今仓=>昨仓""" + + for k,v in self.posBufferDict.items(): + if v.longToday > 0: + self.writeCtaLog(u'调整多单持仓:今仓{}=> 0 昨仓{} => 昨仓:{}'.format(v.longToday,v.longYd,v.longPosition)) + v.longToday = 0 + v.longYd = v.longPosition + + if v.shortToday > 0: + self.writeCtaLog(u'调整空单持仓:今仓{}=> 0 昨仓{} => 昨仓:{}'.format(v.shortToday, v.shortYd, v.shortPosition)) + v.shortToday = 0 + v.shortYd = v.shortPosition #---------------------------------------------------------------------- def insertData(self, dbName, collectionName, data): @@ -2723,9 +3046,13 @@ class BacktestingEngine(object): 获取数据保存目录 :return: """ - logs_folder = os.path.abspath(os.path.join(os.getcwd(), 'data')) - if os.path.exists(logs_folder): - return logs_folder + if self.data_path is not None: + data_folder = self.data_path + else: + data_folder = os.path.abspath(os.path.join(os.getcwd(), 'data')) + self.data_path = data_folder + if os.path.exists(data_folder): + return data_folder else: return os.path.abspath(os.path.join(cta_engine_path, 'data')) @@ -2734,7 +3061,12 @@ class BacktestingEngine(object): 获取日志保存目录 :return: """ - logs_folder = os.path.abspath(os.path.join(os.getcwd(), 'logs')) + if self.logs_path is not None: + logs_folder = self.logs_path + else: + logs_folder = os.path.abspath(os.path.join(os.getcwd(), 'logs')) + self.logs_path = logs_folder + if os.path.exists(logs_folder): return logs_folder else: @@ -2783,6 +3115,10 @@ class BacktestingEngine(object): self.output(u'Notify:{}'.format(content)) self.writeCtaLog(content) + def writeCtaCritical(self,content,strategy_name=None): + self.output(u'Notify:{}'.format(content)) + self.writeCtaLog(content) + #---------------------------------------------------------------------- def output(self, content): """输出内容""" @@ -3186,7 +3522,7 @@ class BacktestingEngine(object): self.netCapital = max(self.netCapital, self.capital) self.maxNetCapital = max(self.netCapital,self.maxNetCapital) #self.maxVolume = max(self.maxVolume, result.volume) - drawdown = self.capital - self.maxCapital + drawdown = self.netCapital - self.maxCapital drawdownRate = round(float(drawdown*100/self.maxCapital),4) self.pnlList.append(result.pnl) @@ -3298,6 +3634,8 @@ class BacktestingEngine(object): elif self.last_bar is not None: dict['lastPrice'] = self.last_bar.close else: + if len(self.dailyList) == 0: + return dict['lastPrice'] = self.dailyList[-1]['lastPrice'] self.dailyList.append(dict) @@ -3316,7 +3654,7 @@ class BacktestingEngine(object): self.writeCtaLog(u'DEBUG---: savingDailyData, {}: lastPrice={}, net={}, capital={} max={} margin={} commission={} longPos={} shortPos={}, {}'.format( dict['date'], dict['lastPrice'], dict['net'], c, m, today_margin, commission, len(long_list), len(short_list), positionMsg)) - + print(u'{} : {}'.format(dict['date'],dict['net'])) # ---------------------------------------------------------------------- def writeWenHuaSignal(self, filehandle, count, bardatetime, price, text, mask=52): """ @@ -3663,7 +4001,7 @@ class BacktestingEngine(object): self.pnlList.append(result.pnl*compounding) self.timeList.append(time) - self.capitalList.append(self.capningital) + self.capitalList.append(self.capital) self.drawdownList.append(drawdown) self.drawdownRateList.append(drawdownRate) @@ -3679,8 +4017,10 @@ class BacktestingEngine(object): 导出每日净值结果表 :return: """ - if not self.exportTradeList: + if len(self.exportTradeList)==0: + self.writeCtaLog('no traded records') return + s = EMPTY_STRING s = self.strategy_name.replace('&','') s = s.replace(' ', '') @@ -3724,66 +4064,6 @@ class BacktestingEngine(object): self.writeWenHuaSignal(wenhuaSignalFile, wenhuaSingalCount, datetime.strptime(t['CloseTime'], '%Y-%m-%d %H:%M:%S'), t['ClosePrice'], msg) wenhuaSingalCount += 1 wenhuaSignalFile.close() -# wh_records = OrderedDict() -# for t in self.exportTradeList: -# if t['Direction'] is 'Long': -# k = '{}_{}_{}'.format(t['OpenTime'], 'Buy', t['OpenPrice']) -# # 生成文华用的指标信号 -# v = {'time': datetime.strptime(t['OpenTime'], '%Y-%m-%d %H:%M:%S'), 'price':t['OpenPrice'], 'action': 'Buy', 'volume':t['Volume']} -# r = wh_records.get(k,None) -# if r is not None: -# r['volume'] += t['Volume'] -# else: -# wh_records[k] = v -# -# k = '{}_{}_{}'.format(t['CloseTime'], 'Sell', t['ClosePrice']) -# # 生成文华用的指标信号 -# v = {'time': datetime.strptime(t['CloseTime'], '%Y-%m-%d %H:%M:%S'), 'price': t['ClosePrice'], 'action': 'Sell', 'volume': t['Volume']} -# r = wh_records.get(k, None) -# if r is not None: -# r['volume'] += t['Volume'] -# else: -# wh_records[k] = v -# -# else: -# k = '{}_{}_{}'.format(t['OpenTime'], 'Short', t['OpenPrice']) -# # 生成文华用的指标信号 -# v = {'time': datetime.strptime(t['OpenTime'], '%Y-%m-%d %H:%M:%S'), 'price': t['OpenPrice'], 'action': 'Short', 'volume': t['Volume']} -# r = wh_records.get(k, None) -# if r is not None: -# r['volume'] += t['Volume'] -# else: -# wh_records[k] = v -# k = '{}_{}_{}'.format(t['CloseTime'], 'Cover', t['ClosePrice']) -# # 生成文华用的指标信号 -# v = {'time': datetime.strptime(t['CloseTime'], '%Y-%m-%d %H:%M:%S'), 'price': t['ClosePrice'], 'action': 'Cover', 'volume': t['Volume']} -# r = wh_records.get(k, None) -# if r is not None: -# r['volume'] += t['Volume'] -# else: -# wh_records[k] = v -# -# branchs = 0 -# count = 0 -# wh_signal_file = None -# for r in list(wh_records.values()): -# if count % 200 == 0: -# if wh_signal_file is not None: -# wh_signal_file.close() -# -# # 交易记录生成文华对应的公式 -# filename = os.path.abspath(os.path.join(self.get_logs_path(), -# '{}_WenHua_{}_{}.csv'.format(s, datetime.now().strftime('%Y%m%d_%H%M'), branchs))) -# branchs += 1 -# self.writeCtaLog(u'save trade records for WenHua:{}'.format(filename)) -# -# wh_signal_file = open(filename, mode='w') -# -# count += 1 -# if wh_signal_file is not None: -# self.writeWenHuaSignal(filehandle=wh_signal_file, count=count, bardatetime=r['time'],price=r['price'], text='{}({})'.format(r['action'],r['volume'])) -# if wh_signal_file is not None: -# wh_signal_file.close() # 导出每日净值记录表 if not self.dailyList: @@ -3864,7 +4144,7 @@ class BacktestingEngine(object): return d, capitalNetList, capitalList # ---------------------------------------------------------------------- - def showBacktestingResult(self): + def showBacktestingResult(self,is_plot_daily=False): """显示回测结果""" if self.calculateMode != self.REALTIME_MODE: self.calculateBacktestingResult() @@ -3873,138 +4153,181 @@ class BacktestingEngine(object): if len(d) == 0: self.output(u'无交易结果') - return + return {},'' # 导出交易清单 self.exportTradeResult() + result_info = OrderedDict() + # 输出 self.writeCtaNotification('-' * 30) + result_info.update({u'第一笔交易': str(d['timeList'][0])}) self.writeCtaNotification(u'第一笔交易:\t%s' % d['timeList'][0]) + + result_info.update({u'最后一笔交易': str(d['timeList'][-1])}) self.writeCtaNotification(u'最后一笔交易:\t%s' % d['timeList'][-1]) + result_info.update({u'总交易次数': d['totalResult']}) self.writeCtaNotification(u'总交易次数:\t%s' % formatNumber(d['totalResult'])) + + result_info.update({u'期初资金': d['initCapital']}) self.writeCtaNotification(u'期初资金:\t%s' % formatNumber(d['initCapital'])) + + result_info.update({u'总盈亏': d['capital']}) self.writeCtaNotification(u'总盈亏:\t%s' % formatNumber(d['capital'])) + + result_info.update({u'资金最高净值': d['maxCapital']}) self.writeCtaNotification(u'资金最高净值:\t%s' % formatNumber(d['maxCapital'])) + + result_info.update({u'资金最高净值时间': str(self.maxNetCapital_time)}) self.writeCtaNotification(u'资金最高净值时间:\t%s' % self.maxNetCapital_time) + result_info.update({u'每笔最大盈利': d['maxPnl']}) self.writeCtaNotification(u'每笔最大盈利:\t%s' % formatNumber(d['maxPnl'])) + + result_info.update({u'每笔最大亏损': d['minPnl']}) self.writeCtaNotification(u'每笔最大亏损:\t%s' % formatNumber(d['minPnl'])) + + result_info.update({u'净值最大回撤': min(d['drawdownList'])}) self.writeCtaNotification(u'净值最大回撤: \t%s' % formatNumber(min(d['drawdownList']))) + + result_info.update({u'净值最大回撤率': self.daily_max_drawdown_rate}) #self.writeCtaNotification(u'净值最大回撤率: \t%s' % formatNumber(max(d['drawdownRateList']))) self.writeCtaNotification(u'净值最大回撤率: \t%s' % formatNumber(self.daily_max_drawdown_rate)) + + result_info.update({u'净值最大回撤时间': str(self.max_drowdown_rate_time)}) self.writeCtaNotification(u'净值最大回撤时间:\t%s' % self.max_drowdown_rate_time) + + result_info.update({u'胜率': d['winningRate']}) self.writeCtaNotification(u'胜率:\t%s' % formatNumber(d['winningRate'])) + result_info.update({u'盈利交易平均值':d['averageWinning']}) self.writeCtaNotification(u'盈利交易平均值\t%s' % formatNumber(d['averageWinning'])) + + result_info.update({u'亏损交易平均值' : d['averageLosing']}) self.writeCtaNotification(u'亏损交易平均值\t%s' % formatNumber(d['averageLosing'])) + + result_info.update({u'盈亏比': d['profitLossRatio']}) self.writeCtaNotification(u'盈亏比:\t%s' % formatNumber(d['profitLossRatio'])) + result_info.update({u'最大持仓': d['maxVolume']}) self.writeCtaNotification(u'最大持仓:\t%s' % formatNumber(d['maxVolume'])) + result_info.update({u'平均每笔盈利' : d['capital']/d['totalResult']}) self.writeCtaNotification(u'平均每笔盈利:\t%s' %formatNumber(d['capital']/d['totalResult'])) + result_info.update({u'平均每笔滑点成本': d['totalSlippage']/d['totalResult']}) self.writeCtaNotification(u'平均每笔滑点成本:\t%s' %formatNumber(d['totalSlippage']/d['totalResult'])) + + result_info.update({u'平均每笔佣金': d['totalCommission']/d['totalResult']}) self.writeCtaNotification(u'平均每笔佣金:\t%s' %formatNumber(d['totalCommission']/d['totalResult'])) + + result_info.update({u'Sharpe Ratio': d['sharpe']}) self.writeCtaNotification(u'Sharpe Ratio:\t%s' % formatNumber(d['sharpe'])) # 绘图 - import matplotlib - import matplotlib.pyplot as plt - from matplotlib.ticker import MultipleLocator, FormatStrFormatter - import numpy as np - matplotlib.rcParams['figure.figsize'] = (20.0, 10.0) - + fig_file_name = EMPTY_STRING try: - import seaborn as sns # 如果安装了seaborn则设置为白色风格 - sns.set_style('whitegrid') - except ImportError: - pass + import matplotlib + matplotlib.use('Agg') + import matplotlib.pyplot as plt - # 是否显示每日资金曲线 - isPlotDaily = False # DEBUG - #isPlotDaily = True - capitalStr = '' - if isPlotDaily == True: - daily_df = pd.DataFrame(self.dailyList) - daily_df = daily_df.set_index('date') + from matplotlib.ticker import MultipleLocator, FormatStrFormatter + import numpy as np + matplotlib.rcParams['figure.figsize'] = (20.0, 10.0) - pCapital = plt.subplot(4, 1, 1) - pCapital.set_ylabel("trade capital") - pCapital.plot(d['capitalList'], color='r', lw=0.8) - plt.title(u'{}: {}~{}({}) NetCapital={}({}), #Trading={}({}/day), TotalCommission={}, MaxLots={}({}), MDD={}%'.format( - self.symbol, - self.startDate, self.endDate, len(dailyNetCapital), - dailyNetCapital[-1], min(d['drawdownList']), - d['totalResult'], int(d['totalResult']/len(dailyNetCapital)), - d['totalCommission'], - d['maxVolume'], max(daily_df['occupyMoney']), - self.daily_max_drawdown_rate)) - pCapital.grid() - capitalStr = '{}.{}'.format(round(dailyNetCapital[-1]), round(min(d['drawdownList']))) + try: + import seaborn as sns # 如果安装了seaborn则设置为白色风格 + sns.set_style('whitegrid') + except ImportError: + pass - pDailyCapital = plt.subplot(4, 1, 3) - pDailyCapital.set_ylabel("daily capital") - pDailyCapital.plot(dailyCapital, color='b', lw=0.8, label='Capital') - pDailyCapital.plot(dailyNetCapital, color='r', lw=1, label='NetCapital') - # Change the label of X-Axes to date - xt = pDailyCapital.get_xticks() - interval = len(dailyNetCapital) / 10 - interval = round(interval) -1 - xt3 = list(range(-10, len(dailyNetCapital), interval)) - xt2 = [daily_df.index[int(i)] for i in xt3[1:]] - xt2.insert(0,'') - xt2.append('') - pDailyCapital.set_xticks(xt3) - pDailyCapital.set_xticklabels(xt2) - pDailyCapital.grid() - pDailyCapital.legend() + # 是否显示每日资金曲线 + capitalStr = '' + if is_plot_daily: + daily_df = pd.DataFrame(self.dailyList) + daily_df = daily_df.set_index('date') - pLastPrice = plt.subplot(4, 1, 2) - pLastPrice.set_ylabel("daily lastprice") - pLastPrice.plot(daily_df['lastPrice'], color='y', lw=1, label='Price') - pLastPrice.set_xticks(xt3) - pLastPrice.set_xticklabels(xt2) - pLastPrice.grid() - pLastPrice.legend() + pCapital = plt.subplot(4, 1, 1) + pCapital.set_ylabel("trade capital") + pCapital.plot(d['capitalList'], color='r', lw=0.8) + plt.title(u'{}: {}~{}({}) NetCapital={}({}), #Trading={}({}/day), TotalCommission={}, MaxLots={}({}), MDD={}%'.format( + self.symbol, + self.startDate, self.endDate, len(dailyNetCapital), + dailyNetCapital[-1], min(d['drawdownList']), + d['totalResult'], int(d['totalResult']/len(dailyNetCapital)), + d['totalCommission'], + d['maxVolume'], max(daily_df['occupyMoney']), + self.daily_max_drawdown_rate)) + pCapital.grid() + capitalStr = '{}.{}'.format(round(dailyNetCapital[-1]), round(min(d['drawdownList']))) - pOccupyRate = plt.subplot(4, 1, 4) - pOccupyRate.set_ylabel("occupyMoney") - index = np.arange(len(daily_df['occupyMoney'])) - pOccupyRate.bar(index, daily_df['occupyMoney'], 0.4, color='b') - pOccupyRate.set_xticks(xt3) - pOccupyRate.set_xticklabels(xt2) - pOccupyRate.grid() + pDailyCapital = plt.subplot(4, 1, 3) + pDailyCapital.set_ylabel("daily capital") + pDailyCapital.plot(dailyCapital, color='b', lw=0.8, label='Capital') + pDailyCapital.plot(dailyNetCapital, color='r', lw=1, label='NetCapital') + # Change the label of X-Axes to date + xt = pDailyCapital.get_xticks() + interval = len(dailyNetCapital) / 10 + interval = round(interval) -1 + xt3 = list(range(-10, len(dailyNetCapital), interval)) + xt2 = [daily_df.index[int(i)] for i in xt3[1:]] + xt2.insert(0,'') + xt2.append('') + pDailyCapital.set_xticks(xt3) + pDailyCapital.set_xticklabels(xt2) + pDailyCapital.grid() + pDailyCapital.legend() - else: - pCapital = plt.subplot(4, 1, 1) - pCapital.set_ylabel("capital") - pCapital.plot(d['capitalList'], color='r', lw=0.8) + pLastPrice = plt.subplot(4, 1, 2) + pLastPrice.set_ylabel("daily lastprice") + pLastPrice.plot(daily_df['lastPrice'], color='y', lw=1, label='Price') + pLastPrice.set_xticks(xt3) + pLastPrice.set_xticklabels(xt2) + pLastPrice.grid() + pLastPrice.legend() - plt.title(u'{}~{},{} backtest result '.format(self.startDate, self.endDate, self.strategy_name)) + pOccupyRate = plt.subplot(4, 1, 4) + pOccupyRate.set_ylabel("occupyMoney") + index = np.arange(len(daily_df['occupyMoney'])) + pOccupyRate.bar(index, daily_df['occupyMoney'], 0.4, color='b') + pOccupyRate.set_xticks(xt3) + pOccupyRate.set_xticklabels(xt2) + pOccupyRate.grid() - pDD = plt.subplot(4, 1, 2) - pDD.set_ylabel("DD") - pDD.bar(range(len(d['drawdownList'])), d['drawdownList'], color='g') + else: + pCapital = plt.subplot(4, 1, 1) + pCapital.set_ylabel("capital") + pCapital.plot(d['capitalList'], color='r', lw=0.8) - pPnl = plt.subplot(4, 1, 3) - pPnl.set_ylabel("pnl") - pPnl.hist(d['pnlList'], bins=50, color='c') - - plt.tight_layout() - #plt.xticks(xindex, tradeTimeIndex, rotation=30) # 旋转15 + plt.title(u'{}~{},{} backtest result '.format(self.startDate, self.endDate, self.strategy_name)) - fig_file_name = os.path.abspath(os.path.join(self.get_logs_path(), - '{}_plot_{}_{}.png'.format(self.strategy_name, - datetime.now().strftime('%Y%m%d_%H%M'), capitalStr))) + pDD = plt.subplot(4, 1, 2) + pDD.set_ylabel("DD") + pDD.bar(range(len(d['drawdownList'])), d['drawdownList'], color='g') + + pPnl = plt.subplot(4, 1, 3) + pPnl.set_ylabel("pnl") + pPnl.hist(d['pnlList'], bins=50, color='c') + + plt.tight_layout() + #plt.xticks(xindex, tradeTimeIndex, rotation=30) # 旋转15 + + fig_file_name = os.path.abspath(os.path.join(self.get_logs_path(), + '{}_plot_{}_{}.png'.format(self.strategy_name, + datetime.now().strftime('%Y%m%d_%H%M'), capitalStr))) - fig = plt.gcf() - fig.savefig(fig_file_name) - self.output (u'图表保存至:{0}'.format(fig_file_name)) - #plt.show() - plt.close() + fig = plt.gcf() + fig.savefig(fig_file_name) + self.output (u'图表保存至:{0}'.format(fig_file_name)) + #plt.show() + plt.close() + except Exception as ex: + self.writeCtaError(u'保存图表异常') + + return result_info,fig_file_name #---------------------------------------------------------------------- def putStrategyEvent(self, name): diff --git a/vnpy/trader/app/ctaStrategy/ctaBase.py b/vnpy/trader/app/ctaStrategy/ctaBase.py index d45b3f2f..0c25626b 100644 --- a/vnpy/trader/app/ctaStrategy/ctaBase.py +++ b/vnpy/trader/app/ctaStrategy/ctaBase.py @@ -42,16 +42,17 @@ NIGHT_MARKET_SQ3 = {'RU': 0, 'RB': 0, 'HC': 0,'SP':0} # 郑商所夜盘,9:00~10:15, 10:30~11:30, 13:30~15:00, 21:00 ~23:30 NIGHT_MARKET_ZZ = {'TA': 0, 'JR': 0, 'OI': 0, 'RO': 0, 'PM': 0, 'WT': 0, 'WS': 0, 'WH': 0, 'CF': 0, 'SR': 0, 'FG': 0, 'ME': 0, 'MA': 0, 'RS': 0, 'RM': 0, 'TC': 0, 'RI': 0, 'ER': 0,'ZC':0} -# 郑商所夜盘,9:00~10:15, 10:30~11:30, 13:30~15:00, 21:00 ~23:30 +# 郑商所夜盘,9:00~10:15, 10:30~11:30, 13:30~15:00, 21:00 ~23:00 NIGHT_MARKET_DL = {'V': 0, 'L': 0, 'BB': 0, 'I': 0, 'FB': 0, 'C': 0, 'PP': 0, 'A': 0, 'B': 0, 'M': 0, 'Y': 0, 'P': 0, - 'JM': 0, 'J': 0} + 'JM': 0, 'J': 0,'EG':0} # 中金日盘,9:15 ~11:30, 13:00~15:15 MARKET_ZJ = {'IC': 0, 'IF': 0, 'IH': 0, 'T': 0, 'TF': 0} # 只有日盘得合约 MARKET_DAY_ONLY = {'IC': 0, 'IF': 0, 'IH': 0, 'T': 0, 'TF': 0,'PP':0,'JD':0,'BB':0,'C':0,'CS':0,'FB':0,'L':0,'V':0, - 'JR':0, 'LR':0,'PM':0,'RI':0,'RS':0,'SM':0,'WH':0,'AP':0} + 'JR':0, 'LR':0,'PM':0,'RI':0,'RS':0,'SM':0,'WH':0,'AP':0,'CJ':0} + # 数据库名称 SETTING_DB_NAME = 'VnTrader_Setting_Db' POSITION_DB_NAME = 'VnTrader_Position_Db' diff --git a/vnpy/trader/app/ctaStrategy/ctaEngine.py b/vnpy/trader/app/ctaStrategy/ctaEngine.py index ef68d682..df8f4540 100644 --- a/vnpy/trader/app/ctaStrategy/ctaEngine.py +++ b/vnpy/trader/app/ctaStrategy/ctaEngine.py @@ -37,6 +37,8 @@ from vnpy.trader.vtGateway import VtSubscribeReq, VtOrderReq, VtCancelOrderReq, from vnpy.trader.app.ctaStrategy.ctaBase import * from vnpy.trader.setup_logger import setup_logger from vnpy.trader.vtFunction import todayDate, getJsonPath +from vnpy.trader.vtObject import VtPositionData,PositionBuffer +from vnpy.trader.app.ctaStrategy.fundKline import FundKline from vnpy.trader.util_mail import sendmail from vnpy.trader.vtGlobal import globalSetting # 加载 strategy目录下所有的策略 @@ -97,6 +99,9 @@ class CtaEngine(object): # key为vtSymbol,value为PositionBuffer对象 self.posBufferDict = {} + # Strategy pos dict,key:strategy instance name, value: pos dict + self.strategy_pos_dict = {} + # 成交号集合,用来过滤已经收到过的成交推送 self.tradeSet = set() @@ -109,6 +114,9 @@ class CtaEngine(object): # 未能订阅的symbols self.pendingSubcribeSymbols = {} + # 订阅的bar与strategy映射关系 + self.barStrategyDict ={} + # 注册事件监听 self.registerEvent() @@ -120,6 +128,49 @@ class CtaEngine(object): self.strategy_loggers = {} self.createLogger() + self.fund_kline_dict = {} + + # 登记vtSymbol对应的最新价 + self.price_dict = {} + + def create_fund_kline(self, name,load_trade=False): + """ + 创建资金曲线 + :param: name 账号的ID,或者策略的实例name + :return: + """ + setting = {} + setting.update({'name':name}) + setting['inputMa1Len'] = 5 + setting['inputMa2Len'] = 10 + setting['inputMa3Len'] = 20 + setting['inputYb'] = True + setting['minDiff'] = 0.01 + setting['shortSymbol'] = 'fund' + + fund_kline = FundKline(cta_engine=self,setting=setting,load_trade=load_trade) + self.fund_kline_dict.update({name:fund_kline}) + return fund_kline + + def get_fund_kline(self, name=None): + """ 获取账号的资金k线对象""" + # 指定资金账号 + if name: + kline = self.fund_kline_dict.get(name, None) + return kline + + # 没有指定账号,并且存在一个或多个资金K线 + if len(self.fund_kline_dict)>0: + # 优先找vt_setting中,配置了strategy_groud的资金K线 + kline = self.fund_kline_dict.get(globalSetting.get('strategy_group'),None) + + # 找不到,返回第一个 + if kline is None: + kline = self.fund_kline_dict.values()[0] + return kline + else: + return None + def analysis_vtSymbol(self, vtSymbol): """ 分析合约 @@ -165,8 +216,8 @@ class CtaEngine(object): req.volume = self.roundToVolumeTick(volumeTick=contract.volumeTick,volume=volume) # 数量 if strategy: - req.productClass = strategy.productClass - req.currency = strategy.currency + req.productClass = getattr(strategy,'productClass','') + req.currency = getattr(strategy,'currency','') else: req.productClass = '' req.currency = '' @@ -183,7 +234,7 @@ class CtaEngine(object): req.direction = DIRECTION_SHORT # 只有上期所才要考虑平今平昨 - if contract.exchange != EXCHANGE_SHFE: + if contract.exchange not in [EXCHANGE_SHFE,EXCHANGE_SHFE]: req.offset = OFFSET_CLOSE else: # 获取持仓缓存数据 @@ -221,8 +272,8 @@ class CtaEngine(object): elif orderType == CTAORDER_COVER: req.direction = DIRECTION_LONG - # 只有上期所才要考虑平今平昨 - if contract.exchange != EXCHANGE_SHFE: + # 只有上期所,能源所才要考虑平今平昨 + if contract.exchange not in [EXCHANGE_SHFE,EXCHANGE_INE]: req.offset = OFFSET_CLOSE else: # 获取持仓缓存数据 @@ -251,16 +302,16 @@ class CtaEngine(object): # 其他情况使用平昨 # else: # req.offset = OFFSET_CLOSE - - vtOrderID = self.mainEngine.sendOrder(req, contract.gatewayName) # 发单 + strategy_name = getattr(strategy,'name') if strategy is not None else None + vtOrderID = self.mainEngine.sendOrder(req, contract.gatewayName, strategyName=strategy_name) # 发单 if vtOrderID is None or len(vtOrderID) == 0: - self.writeCtaError(u'{} 发送委托失败. {} {} {} {}'.format(strategy.name if strategy else 'CtaEngine', vtSymbol, req.offset, req.direction, volume, price)) + self.writeCtaError(u'{} 发送委托失败. {} {} {} {}'.format(getattr(strategy,'name','') if strategy else 'CtaEngine', vtSymbol, req.offset, req.direction, volume, price)) return '' if strategy: self.orderStrategyDict[vtOrderID] = strategy # 保存vtOrderID和策略的映射关系 - msg = u'策略%s发送委托,%s, %s,%s,%s@%s' % (strategy.name, vtSymbol, req.offset, req.direction, volume, price) + msg = u'策略%s发送委托,%s, %s,%s,%s@%s' % (getattr(strategy,'name',''), vtSymbol, req.offset, req.direction, volume, price) self.writeCtaLog(msg) else: msg = u'%s发送委托,%s, %s,%s,%s@%s' % ('CtaEngine', vtSymbol, req.offset, req.direction, volume, price) @@ -457,12 +508,13 @@ class CtaEngine(object): del self.workingStopOrderDict[so.stopOrderID] # ---------------------------------------------------------------------- - def procecssTickEvent(self, event): + def processTickEvent(self, event): """处理行情推送事件""" # 1. 获取事件的Tick数据 tick = event.dict_['data'] tick = copy.copy(tick) + # 移除待订阅的合约清单 if '.' in tick.vtSymbol: symbol = tick.vtSymbol.split('.')[0] @@ -478,6 +530,7 @@ class CtaEngine(object): # 缓存最新tick self.tickDict[tick.vtSymbol] = tick + self.setPrice(vtSymbol=tick.vtSymbol,price=tick.lastPrice) # 2.收到tick行情后,优先处理本地停止单(检查是否要立即发出) self.processStopOrder(tick) @@ -502,6 +555,18 @@ class CtaEngine(object): for strategy in l: self.callStrategyFunc(strategy, strategy.onTick, ctaTick) + def processBarEvent(self,event): + # 1. 获取事件的Tick数据 + + bar = event.dict_['data'] + # 3.推送tick到对应的策略对象进行处理 + if bar.vtSymbol in self.barStrategyDict: + # 逐个推送到策略实例中 + l = self.barStrategyDict[bar.vtSymbol] + for strategy in l: + self.writeCtaLog(u'推送{}bar到策略:{}'.format(bar.vtSymbol,strategy.name)) + self.callStrategyFunc(strategy, strategy.onBar, copy.copy(bar)) + # ---------------------------------------------------------------------- def processOrderEvent(self, event): """处理委托推送事件""" @@ -547,8 +612,10 @@ class CtaEngine(object): # else: # strategy.pos -= trade.volume + copy_trade = copy.copy(trade) + # 根据策略名称,写入 data\straetgy_name_trade.csv文件 - strategy_name = getattr(strategy,'name',None) + strategy_name = getattr(strategy,'name','no_strategy_name') trade_fields = ['symbol','exchange','vtSymbol','tradeID','vtTradeID','orderID','vtOrderID','direction','offset','price','volume','tradeTime'] trade_dict = OrderedDict() try: @@ -567,10 +634,14 @@ class CtaEngine(object): # 推送到策略onTrade事件 self.callStrategyFunc(strategy, strategy.onTrade, trade) - # 保存策略持仓到数据库 - self.saveSyncData(strategy) + if globalSetting.get('activate_strategy_fund_kline',False): + kline = self.get_fund_kline(strategy_name) + if kline is not None: + kline.update_trade(copy_trade) + else: + self.writeCtaError(u'未能推送成交记录到{}资金曲线'.format(strategy_name)) - # 更新持仓缓存数据 + # 更新持仓缓存数据 if trade.vtSymbol in self.tickStrategyDict: posBuffer = self.posBufferDict.get(trade.vtSymbol, None) if not posBuffer: @@ -580,26 +651,194 @@ class CtaEngine(object): posBuffer.updateTradeData(trade) # ---------------------------------------------------------------------- - def processPositionEvent(self, event): """处理持仓推送""" pos = event.dict_['data'] # 更新持仓缓存数据 - if True: # pos.vtSymbol in self.tickStrategyDict: - posBuffer = self.posBufferDict.get(pos.vtSymbol, None) - if not posBuffer: - posBuffer = PositionBuffer() - posBuffer.vtSymbol = pos.vtSymbol - self.posBufferDict[pos.vtSymbol] = posBuffer - posBuffer.updatePositionData(pos) + posBuffer = self.posBufferDict.get(pos.vtSymbol, None) + if not posBuffer: + posBuffer = PositionBuffer() + posBuffer.vtSymbol = pos.vtSymbol + self.posBufferDict[pos.vtSymbol] = posBuffer + posBuffer.updatePositionData(pos) + + if pos.vtSymbol.endswith('SPD'): + return + + if not hasattr(self.mainEngine,'dataEngine'): + return + + # 2、如果是普通合约持仓,则查询是否为自定义套利合约的更新部分 + if pos.vtSymbol not in self.mainEngine.dataEngine.contract_spd_mapping: + return + + # 找到对应的spd合约 + spd_list = self.mainEngine.dataEngine.contract_spd_mapping.get(pos.vtSymbol, []) + for spd_name in spd_list: + spd_setting = self.mainEngine.dataEngine.custom_contract_setting.get(spd_name, None) + if spd_setting is None: + continue + leg1_symbol = spd_setting.get('leg1_symbol') + leg2_symbol = spd_setting.get('leg2_symbol') + leg1_ratio = spd_setting.get('leg1_ratio',1) + leg2_ratio = spd_setting.get('leg2_ratio',1) + leg1_pos = self.posBufferDict.get(leg1_symbol,None) + leg2_pos = self.posBufferDict.get(leg2_symbol,None) + + # leg1或者leg2没有持仓 + if leg1_pos is None or leg2_pos is None: + continue + + spd_long_pos = 0 + spd_short_pos = 0 + if leg1_pos.longPosition > 0 and leg2_pos.shortPosition > 0 and leg2_pos.shortPrice >0: + # 正套持仓量 + leg1_volume = int(leg1_pos.longPosition/leg1_ratio) + leg2_volume = int(leg2_pos.shortPosition/leg2_ratio) + spd_long_pos = min(leg1_volume,leg2_volume) + if spd_long_pos > 0: + spd_pos = VtPositionData() + spd_pos.vtSymbol = spd_name + spd_pos.symbol = spd_name + spd_pos.exchange = pos.exchange + spd_pos.gatewayName = pos.gatewayName + spd_pos.direction = DIRECTION_LONG + spd_pos.position = spd_long_pos + # 开仓均价 + if spd_setting.get('is_ratio', False): + # 使用百分比 + spd_pos.price = 100 * leg1_pos.longPrice * leg1_ratio/(leg2_pos.shortPrice*leg2_ratio) + elif spd_setting.get('is_spread', False): + spd_pos.price = leg1_pos.longPrice * leg1_ratio - leg2_pos.shortPrice * leg2_ratio + + # 冻结仓位 + spd_pos.frozen = int(min(leg1_pos.longFrozen/leg1_ratio,leg2_pos.shortFrozen/leg2_ratio)) + + # 持仓盈亏 + leg1_profit = leg1_pos.longProfit * spd_long_pos * leg1_ratio / leg1_pos.longPosition + leg2_profit = leg2_pos.shortProfit * spd_long_pos * leg2_ratio / leg2_pos.shortPosition + spd_pos.positionProfit = leg1_profit + leg2_profit + spd_pos.vtPositionName = '.'.join([spd_pos.vtSymbol,spd_pos.direction,spd_pos.gatewayName]) + + # 通用事件 + event1 = Event(type_=EVENT_POSITION) + event1.dict_['data'] = spd_pos + self.eventEngine.put(event1) + elif (leg1_pos.longPosition == 0 or leg2_pos.shortPosition == 0) and self.posBufferDict.get(spd_name,None) is not None: + # 没有持有正套单,单有套利合约得记录信息 + spd_pos = VtPositionData() + spd_pos.vtSymbol = spd_name + spd_pos.symbol = spd_name + spd_pos.exchange = pos.exchange + spd_pos.gatewayName = pos.gatewayName + spd_pos.direction = DIRECTION_LONG + spd_pos.position = 0 + spd_pos.price = 0 + # 冻结仓位 + spd_pos.frozen = 0 + # 持仓盈亏 + spd_pos.positionProfit = 0 + spd_pos.vtPositionName = '.'.join([spd_pos.vtSymbol, spd_pos.direction, spd_pos.gatewayName]) + + # 通用事件 + event1 = Event(type_=EVENT_POSITION) + event1.dict_['data'] = spd_pos + self.eventEngine.put(event1) + + if leg1_pos.shortPosition > 0 and leg2_pos.longPosition > 0 and leg2_pos.longPrice > 0: + spd_short_pos = int(min(leg1_pos.shortPosition / leg1_ratio, leg2_pos.longPosition / leg2_ratio)) + if spd_short_pos > 0: + spd_pos = VtPositionData() + spd_pos.vtSymbol = spd_name + spd_pos.symbol = spd_name + spd_pos.exchange = pos.exchange + spd_pos.gatewayName = pos.gatewayName + spd_pos.direction = DIRECTION_SHORT + spd_pos.position = spd_short_pos + # 开仓均价 + if spd_setting.get('is_ratio', False): + # 使用百分比 + spd_pos.price = 100 * leg1_pos.shortPrice * leg1_ratio / (leg2_pos.longPrice * leg2_ratio) + elif spd_setting.get('is_spread', False): + spd_pos.price = leg1_pos.shortPrice * leg1_ratio - leg2_pos.longPrice * leg2_ratio + + # 冻结仓位 + spd_pos.frozen = int(min(leg1_pos.shortFrozen / leg1_ratio, leg2_pos.longFrozen / leg2_ratio)) + + # 持仓盈亏 + leg1_profit = leg1_pos.shortProfit * spd_short_pos * leg1_ratio / leg1_pos.shortPosition + leg2_profit = leg2_pos.longProfit * spd_short_pos * leg2_ratio / leg2_pos.longPosition + spd_pos.positionProfit = leg1_profit + leg2_profit + spd_pos.vtPositionName = '.'.join([spd_pos.vtSymbol, spd_pos.direction, spd_pos.gatewayName]) + + # 通用事件 + event1 = Event(type_=EVENT_POSITION) + event1.dict_['data'] = spd_pos + self.eventEngine.put(event1) + elif (leg1_pos.shortPosition == 0 or leg2_pos.longPosition == 0) and self.posBufferDict.get(spd_name,None) is not None: + # 没有持有反套单,但有套利合约得记录信息 + spd_pos = VtPositionData() + spd_pos.vtSymbol = spd_name + spd_pos.symbol = spd_name + spd_pos.exchange = pos.exchange + spd_pos.gatewayName = pos.gatewayName + spd_pos.direction = DIRECTION_SHORT + spd_pos.position = 0 + spd_pos.price = 0 + # 冻结仓位 + spd_pos.frozen = 0 + # 持仓盈亏 + spd_pos.positionProfit = 0 + spd_pos.vtPositionName = '.'.join([spd_pos.vtSymbol, spd_pos.direction, spd_pos.gatewayName]) + + # 通用事件 + event1 = Event(type_=EVENT_POSITION) + event1.dict_['data'] = spd_pos + self.eventEngine.put(event1) + + def processAccountEvent(self,event): + """ + 账号资金事件更新 + :param event: + :return: + """ + dt = datetime.now() + if dt.hour in [9, 10, 11, 13, 14, 15, 21, 22, 23, 0, 1, 2]: + account = event.dict_['data'] + account_id = account.accountID + + # 更新账号得资金曲线 + if globalSetting.get('activate_account_fund_kline',False): + fund_kline = self.fund_kline_dict.get(account_id,None) + if fund_kline is None: + fund_kline = self.create_fund_kline(account_id) + + if fund_kline: + balance = account.balance + fund_kline.update_account(dt,balance) + + # 更新各策略实例的资金曲线 + if len(self.tickDict) > 0 and globalSetting.get('activate_strategy_fund_kline',False): + for strategy_name in self.strategy_pos_dict.keys(): + fund_kline = self.fund_kline_dict.get(strategy_name,None) + if fund_kline is not None: + hold_pnl = fund_kline.get_hold_pnl() + if hold_pnl !=0: + fund_kline.update_strategy(dt=dt,hold_pnl=hold_pnl) + + # 检查未订阅信息 + self.checkUnsubscribedSymbols() # ---------------------------------------------------------------------- def registerEvent(self): """注册事件监听""" # 注册行情数据推送(Tick数据到达)的响应事件 - self.eventEngine.register(EVENT_TICK, self.procecssTickEvent) + self.eventEngine.register(EVENT_TICK, self.processTickEvent) + + # 注册bar行情数据推送(Bar数据到达)的响应事件 + self.eventEngine.register(EVENT_BAR, self.processBarEvent) # 注册订单推送的响应事件 self.eventEngine.register(EVENT_ORDER, self.processOrderEvent) @@ -611,7 +850,7 @@ class CtaEngine(object): self.eventEngine.register(EVENT_POSITION, self.processPositionEvent) # 账号更新事件(借用账号更新事件,来检查是否有未订阅的合约信息) - self.eventEngine.register(EVENT_ACCOUNT, self.checkUnsubscribedSymbols) + self.eventEngine.register(EVENT_ACCOUNT, self.processAccountEvent) # 注册定时器事件 self.eventEngine.register(EVENT_TIMER, self.processTimerEvent) @@ -878,8 +1117,10 @@ class CtaEngine(object): # ---------------------------------------------------------------------- # 订阅合约相关 - def subscribe(self, strategy, symbol): + def subscribe(self, strategy, symbol,is_bar=False): """订阅合约,不成功时,加入到待订阅列表""" + if len(symbol)==0: + return contract = self.mainEngine.getContract(symbol) if contract: @@ -887,7 +1128,7 @@ class CtaEngine(object): req = VtSubscribeReq() req.symbol = contract.symbol req.exchange = contract.exchange - + req.is_bar = is_bar # 对于IB接口订阅行情时所需的货币和产品类型,从策略属性中获取 if hasattr(strategy,'currency'): req.currency = strategy.currency @@ -900,9 +1141,11 @@ class CtaEngine(object): print(u'Warning, can not find {0} in contracts'.format(symbol)) self.writeCtaLog(u'交易合约{}无法找到,添加到待订阅列表'.format(symbol)) self.pendingSubcribeSymbols[symbol] = strategy + try: req = VtSubscribeReq() req.symbol = symbol + req.is_bar = is_bar self.mainEngine.subscribe(req,None) @@ -919,10 +1162,19 @@ class CtaEngine(object): strategies.append(strategy) self.tickStrategyDict.update({symbol: strategies}) - def checkUnsubscribedSymbols(self, event): - """持仓更新信息时,检查未提交的合约""" + if is_bar: + s_list = self.barStrategyDict.get(symbol,[]) + if symbol not in s_list: + self.writeCtaLog(u'添加:{} bar订阅映射到策略:{}'.format(symbol,strategy.name)) + s_list.append(strategy) + self.barStrategyDict.update({symbol:s_list}) + + def checkUnsubscribedSymbols(self): + """检查未提交的合约""" + for symbol in self.pendingSubcribeSymbols.keys(): contract = self.mainEngine.getContract(symbol) + is_bar = True if symbol in self.barStrategyDict else False if contract: # 获取合约的缩写号 s = self.getShortSymbol(symbol) @@ -935,11 +1187,12 @@ class CtaEngine(object): self.writeCtaLog(u'重新提交合约{0}订阅请求'.format(symbol)) strategy = self.pendingSubcribeSymbols[symbol] - self.subscribe(strategy=strategy, symbol=symbol) + self.subscribe(strategy=strategy, symbol=symbol,is_bar=is_bar) else: try: req = VtSubscribeReq() req.symbol = symbol + req.is_bar = is_bar self.mainEngine.subscribe(req, None) except Exception as ex: @@ -1054,6 +1307,18 @@ class CtaEngine(object): symbols.append(strategy.vtSymbol) else: symbols.append(s[0]) + elif strategy.vtSymbol.endswith('SPD'): + # 增加自定义套利合约 + if strategy.vtSymbol not in symbols: + symbols.append(strategy.vtSymbol) + symbol = strategy.vtSymbol[0:-4] + lists = symbol.split('-') + leg1_symbol = lists[0] + leg2_symbol = lists[2] + if leg1_symbol not in symbols: + symbols.append(leg1_symbol) + if leg2_symbol not in symbols: + symbols.append(leg2_symbol) else: symbols = strategy.vtSymbol.split(';') @@ -1070,6 +1335,19 @@ class CtaEngine(object): if hasattr(strategy, 'Leg2Symbol'): if strategy.Leg2Symbol not in symbols: symbols.append(strategy.Leg2Symbol) + if hasattr(strategy, 'miSymbol'): + if strategy.miSymbol not in symbols: + symbols.append(strategy.miSymbol) + if strategy.miSymbol.endswith('SPD'): + # 增加自定义套利合约 + symbol = strategy.miSymbol[0:-4] + lists = symbol.split('-') + leg1_symbol = lists[0] + leg2_symbol = lists[2] + if leg1_symbol not in symbols: + symbols.append(leg1_symbol) + if leg2_symbol not in symbols: + symbols.append(leg2_symbol) for symbol in symbols: self.writeCtaLog(u'添加合约{}与策略{}的匹配目录'.format(symbol,strategy.name)) @@ -1103,6 +1381,11 @@ class CtaEngine(object): except Exception as ex: self.writeCtaCritical(u'自动启动策略:{} 异常,{},{}'.format(name, str(ex), traceback.format_exc())) return False + + # 激活策略实例的资金曲线 + if globalSetting.get('activate_strategy_fund_kline',False): + self.create_fund_kline(name, load_trade=True) + return True def initStrategy(self, name, force=False): @@ -1114,7 +1397,6 @@ class CtaEngine(object): self.callStrategyFunc(strategy, strategy.onInit, force) # strategy.onInit(force=force) # strategy.inited = True - self.loadSyncData(strategy) # 初始化完成后加载同步数据 else: self.writeCtaLog(u'请勿重复初始化策略实例:%s' % name) return True @@ -1536,6 +1818,7 @@ class CtaEngine(object): 'volume': sell_longYd, 'action': 'clean', 'comment': 'sell_longYd', 'result': True if order_id else False, 'datetime': datetime.now()} + self.mainEngine.dbInsert(MATRIX_DB_NAME, POSITION_DISPATCH_HISTORY_COLL_NAME, h) if sell_longToday > 0: @@ -1906,10 +2189,11 @@ class CtaEngine(object): except Exception as ex: self.writeCtaCritical(u'加载策略配置{}:异常{},{}'.format(setting, str(ex), traceback.format_exc())) traceback.print_exc() - + self.loadPosition() except Exception as ex: self.writeCtaCritical(u'加载策略配置异常:{},{}'.format(str(ex),traceback.format_exc())) + # ---------------------------------------------------------------------- # 策略运行监控相关 def getStrategyVar(self, name): @@ -1944,11 +2228,11 @@ class CtaEngine(object): self.writeCtaLog(u'策略实例不存在:' + name) return None - def getStategyPos(self, name,strategy=None): + def getStategyPos(self, name, strategy=None): """ 获取策略的持仓字典 :param name:策略名 - :return: + :return: [ {},{}] """ # 兼容处理,如果strategy是None,通过name获取 if strategy is None: @@ -1961,13 +2245,14 @@ class CtaEngine(object): pos_list = [] if strategy.inited: + # 如果策略具有getPositions得方法,则调用该方法 if hasattr(strategy,'getPositions'): pos_list=strategy.getPositions() for pos in pos_list: pos.update({'symbol':pos.get('vtSymbol')}) - return pos_list - # 有 ctaPosition属性 - if hasattr(strategy, 'position'): + + # 如果策略有 ctaPosition属性 + elif hasattr(strategy, 'position'): # 多仓 long_pos = {} long_pos['symbol'] = strategy.vtSymbol @@ -1984,7 +2269,7 @@ class CtaEngine(object): if short_pos['volume'] > 0: pos_list.append(short_pos) - # 模板缺省pos属性 + # 获取模板缺省pos属性 elif hasattr(strategy, 'pos'): if strategy.pos > 0: long_pos = {} @@ -2003,8 +2288,70 @@ class CtaEngine(object): if short_pos['volume'] > 0: pos_list.append(short_pos) + # 新增处理SPD结尾得特殊自定义套利合约 + try: + if strategy.vtSymbol.endswith('SPD') and len(pos_list) > 0: + old_pos_list = copy.copy(pos_list) + pos_list = [] + for pos in old_pos_list: + # SPD合约 + spd_symbol = pos.get('vtSymbol', pos.get('symbol', None)) + if spd_symbol is not None and spd_symbol.endswith('SPD'): + spd_setting = self.mainEngine.dataEngine.custom_contract_setting.get(spd_symbol,None) + + if spd_setting is None: + self.writeCtaError(u'获取不到:{}得设置信息,检查自定义合约配置文件'.format(spd_symbol)) + pos_list.append(pos) + continue + + leg1_direction = 'long' if pos.get('direction') in [DIRECTION_LONG,'long'] else 'short' + leg2_direction = 'short' if leg1_direction == 'long' else 'long' + spd_volume = pos.get('volume') + + leg1_pos = {} + leg1_pos.update({'symbol': spd_setting.get('leg1_symbol')}) + leg1_pos.update({'vtSymbol': spd_setting.get('leg1_symbol')}) + leg1_pos.update({'direction': leg1_direction}) + leg1_pos.update({'volume': spd_setting.get('leg1_ratio',1)*spd_volume}) + + leg2_pos = {} + leg2_pos.update({'symbol': spd_setting.get('leg2_symbol')}) + leg2_pos.update({'vtSymbol': spd_setting.get('leg2_symbol')}) + leg2_pos.update({'direction': leg2_direction}) + leg2_pos.update({'volume': spd_setting.get('leg2_ratio', 1) * spd_volume}) + + pos_list.append(leg1_pos) + pos_list.append(leg2_pos) + #pos_list.append(pos) + else: + pos_list.append(pos) + + except Exception as ex: + self.writeCtaError(u'分解SPD失败') + + # update local pos dict + self.strategy_pos_dict.update({name:pos_list}) + return pos_list + def get_pos_open_price(self,vtSymbol,direction): + """ + get the open price from posbufferDict + :param vtSymbol: + :param direction: + :return: + """ + + pos = self.posBufferDict.get(vtSymbol,None) + if pos is None: + return 0 + + if direction in [DIRECTION_LONG, 'long']: + return pos.longPrice + else: + return pos.shortPrice + + def updateStrategySetting(self,strategy_name,setting_key,setting_value): """ 更新策略的某项设置 @@ -2065,17 +2412,23 @@ class CtaEngine(object): func(params) else: func() - except Exception: + except Exception as ex: # 停止策略,修改状态为未初始化 strategy.trading = False strategy.inited = False # 发出日志 - content =u'策略{}触发异常已停止.{}'.format(strategy.name,traceback.format_exc()) - self.writeCtaError(content) + content =u'策略{}触发异常已停止.{},{}'.format(strategy.name,str(ex),traceback.format_exc()) + self.writeCtaCritical(content) self.mainEngine.writeCritical(content) - self.sendAlertToWechat(content=content,target=globalSetting.get('gateway_name',None)) + self.sendAlertToWechat(content=content, target=globalSetting.get('gateway_name',None)) + if globalSetting.get('activate_wx_ft', False): + try: + from huafu.util.util_wx_ft import sendWxMsg + sendWxMsg(text=u'策略{}触发异常停止', desp=content) + except Exception: + pass # ---------------------------------------------------------------------- # 仓位持久化相关 @@ -2108,38 +2461,6 @@ class CtaEngine(object): except: self.writeCtaLog(u'loadPosition Exception from Mongodb') - # ---------------------------------------------------------------------- - def saveSyncData(self, strategy): - """保存策略的持仓情况到数据库""" - flt = {'name': strategy.name, - 'vtSymbol': strategy.vtSymbol} - - d = copy.copy(flt) - for key in strategy.syncList: - d[key] = strategy.__getattribute__(key) - - self.mainEngine.dbUpdate(POSITION_DB_NAME, strategy.className, - d, flt, True) - - content = u'策略%s同步数据保存成功,当前持仓%s' % (strategy.name, strategy.pos) - self.writeCtaLog(content) - - # ---------------------------------------------------------------------- - def loadSyncData(self, strategy): - """从数据库载入策略的持仓情况""" - flt = {'name': strategy.name, - 'vtSymbol': strategy.vtSymbol} - syncData = self.mainEngine.dbQuery(POSITION_DB_NAME, strategy.className, flt) - - if not syncData: - return - - d = syncData[0] - - for key in strategy.syncList: - if key in d: - strategy.__setattr__(key, d[key]) - # ---------------------------------------------------------------------- # 公共方法相关 def roundToPriceTick(self, priceTick, price): @@ -2240,6 +2561,9 @@ class CtaEngine(object): except: traceback.print_exc() + for kline in self.fund_kline_dict.values(): + kline.save() + def clearData(self): """清空运行数据""" self.writeCtaLog(u'ctaEngine.clearData()清空运行数据') @@ -2278,7 +2602,13 @@ class CtaEngine(object): else: return c.size - def qryMarginRate(self,vtSymbol): + def setPrice(self,vtSymbol, price): + self.price_dict.update({vtSymbol:price}) + + def qryPrice(self,vtSymbol): + return self.price_dict.get(vtSymbol, None) + + def qryMarginRate(self, vtSymbol): """ 提供给策略查询品种的保证金比率 :param vtSymbol: @@ -2365,7 +2695,7 @@ class CtaEngine(object): return False # 上期,RB/HC/RU :日盘,夜盘(21:00~23:00) - if short_symbol in NIGHT_MARKET_SQ3: + if short_symbol in NIGHT_MARKET_SQ3 or short_symbol in NIGHT_MARKET_DL: if morning_begin <= dt <= morning_break or \ morning_restart <= dt <= morning_close or \ afternoon_begin <= dt <= afternoon_close or \ @@ -2375,7 +2705,7 @@ class CtaEngine(object): return False # 郑商、大连 21:00 ~ 23:30 - if short_symbol in NIGHT_MARKET_ZZ or short_symbol in NIGHT_MARKET_DL: + if short_symbol in NIGHT_MARKET_ZZ : if morning_begin <= dt <= morning_break or \ morning_restart <= dt <= morning_close or \ afternoon_begin <= dt <= afternoon_close or \ @@ -2385,88 +2715,3 @@ class CtaEngine(object): return False return True - -######################################################################## -class PositionBuffer(object): - """持仓缓存信息(本地维护的持仓数据)""" - - # ---------------------------------------------------------------------- - def __init__(self): - """Constructor""" - self.vtSymbol = EMPTY_STRING - - # 多头 - self.longPosition = EMPTY_INT - self.longToday = EMPTY_INT - self.longYd = EMPTY_INT - - # 空头 - self.shortPosition = EMPTY_INT - self.shortToday = EMPTY_INT - self.shortYd = EMPTY_INT - - self.frozen = EMPTY_FLOAT - - # ---------------------------------------------------------------------- - def toStr(self): - """更新显示信息""" - str = u'long:{},yd:{},td:{}, short:{},yd:{},td:{}, fz:{};' \ - .format(self.longPosition, self.longYd, self.longToday, - self.shortPosition, self.shortYd, self.shortToday,self.frozen) - return str - #---------------------------------------------------------------------- - def updatePositionData(self, pos): - """更新持仓数据""" - if pos.direction == DIRECTION_SHORT: - self.shortPosition = pos.position # >=0 - self.shortYd = pos.ydPosition # >=0 - self.shortToday = self.shortPosition - self.shortYd # >=0 - self.frozen = pos.frozen - else: - self.longPosition = pos.position # >=0 - self.longYd = pos.ydPosition # >=0 - self.longToday = self.longPosition - self.longYd # >=0 - self.frozen = pos.frozen - - #---------------------------------------------------------------------- - def updateTradeData(self, trade): - """更新成交数据""" - - if trade.direction == DIRECTION_SHORT: - # 空头和多头相同 - if trade.offset == OFFSET_OPEN: - self.shortPosition += trade.volume - self.shortToday += trade.volume - elif trade.offset == OFFSET_CLOSETODAY: - self.longPosition -= trade.volume - self.longToday -= trade.volume - else: - self.longPosition -= trade.volume - self.longYd -= trade.volume - - if self.longPosition <= 0: - self.longPosition = 0 - if self.longToday <= 0: - self.longToday = 0 - if self.longYd <= 0: - self.longYd = 0 - else: - # 多方开仓,则对应多头的持仓和今仓增加 - if trade.offset == OFFSET_OPEN: - self.longPosition += trade.volume - self.longToday += trade.volume - # 多方平今,对应空头的持仓和今仓减少 - elif trade.offset == OFFSET_CLOSETODAY: - self.shortPosition -= trade.volume - self.shortToday -= trade.volume - else: - self.shortPosition -= trade.volume - self.shortYd -= trade.volume - - if self.shortPosition <= 0: - self.shortPosition = 0 - if self.shortToday <= 0: - self.shortToday = 0 - if self.shortYd <= 0: - self.shortYd = 0 - # 多方平昨,对应空头的持仓和昨仓减少 \ No newline at end of file diff --git a/vnpy/trader/app/ctaStrategy/ctaLineBar.py b/vnpy/trader/app/ctaStrategy/ctaLineBar.py index c8b2b25a..7e658d32 100644 --- a/vnpy/trader/app/ctaStrategy/ctaLineBar.py +++ b/vnpy/trader/app/ctaStrategy/ctaLineBar.py @@ -394,12 +394,12 @@ class CtaLineBar(object): self.lineUpperBandAtan = [] self.lineMiddleBandAtan = [] self.lineLowerBandAtan = [] - self._rt_Upper = None - self._rt_Middle = None - self._rt_Lower = None - self._rt_UpperBandAtan = None - self._rt_MiddleBandAtan = None - self._rt_LowerBandAtan = None + self._rt_Upper = 0 + self._rt_Middle = 0 + self._rt_Lower = 0 + self._rt_UpperBandAtan = 0 + self._rt_MiddleBandAtan = 0 + self._rt_LowerBandAtan = 0 self.lastBollUpper = EMPTY_FLOAT # 最后一根K的Boll上轨数值(与MinDiff取整) self.lastBollMiddle = EMPTY_FLOAT # 最后一根K的Boll中轨数值(与MinDiff取整) @@ -442,6 +442,10 @@ class CtaLineBar(object): self.lastD = EMPTY_FLOAT # bar内计算时,最后一个未关闭的bar的实时值 self.lastJ = EMPTY_FLOAT # bar内计算时,最后一个未关闭的bar的实时J值 + self.kd_count = 0 # > 0, 金叉, < 0 死叉 + self.kd_last_cross = 0 # 最近一次金叉/死叉的点位 + self.kd_cross_price = 0 # 最近一次发生金叉/死叉的价格 + # K线的MACD计算数据(26,12,9) self.inputMacdFastPeriodLen = EMPTY_INT self.inputMacdSlowPeriodLen = EMPTY_INT @@ -747,7 +751,7 @@ class CtaLineBar(object): 根据实时计算得要求,执行实时指标计算 :return: """ - for func in self.rt_funcs: + for func in list(self.rt_funcs): try: func() except Exception as ex: @@ -1032,10 +1036,10 @@ class CtaLineBar(object): endtick = True # 夜盘23:00收盘 - if self.shortSymbol in NIGHT_MARKET_SQ3 and tick.datetime.hour == 23 and tick.datetime.minute == 00: + if self.shortSymbol in [NIGHT_MARKET_SQ3,NIGHT_MARKET_DL] and tick.datetime.hour == 23 and tick.datetime.minute == 00: endtick = True # 夜盘23:30收盘 - if self.shortSymbol in NIGHT_MARKET_ZZ or self.shortSymbol in NIGHT_MARKET_DL: + if self.shortSymbol in NIGHT_MARKET_ZZ: if tick.datetime.hour == 23 and tick.datetime.minute == 30: endtick = True @@ -1123,7 +1127,7 @@ class CtaLineBar(object): lastBar.high = max(lastBar.high, tick.lastPrice) lastBar.low = min(lastBar.low, tick.lastPrice) lastBar.close = tick.lastPrice - + lastBar.openInterest = tick.openInterest # 更新日内总交易量,和bar内交易量 lastBar.dayVolume = tick.volume if l1 == 1: @@ -1330,10 +1334,10 @@ class CtaLineBar(object): return # 1、lineBar满足长度才执行计算 - if len(self.lineBar) < max(7, self.inputMa1Len, self.inputMa2Len, self.inputMa3Len) + 2: + if len(self.lineBar) < min(7, self.inputMa1Len, self.inputMa2Len, self.inputMa3Len) + 2: self.debugCtaLog(u'数据未充分,当前Bar数据数量:{0},计算MA需要:{1}'. format(len(self.lineBar), - max(7, self.inputMa1Len, self.inputMa2Len, self.inputMa3Len) + 2)) + min(7, self.inputMa1Len, self.inputMa2Len, self.inputMa3Len) + 2)) return # 计算第一条MA均线 @@ -1821,9 +1825,12 @@ class CtaLineBar(object): if maxAtrLen <= 0: # 不计算 return - if len(self.lineBar) < maxAtrLen + 1: + data_need_len = min(7, maxAtrLen) + line_bar_len = len(self.lineBar) + + if line_bar_len < data_need_len: self.debugCtaLog(u'数据未充分,当前Bar数据数量:{0},计算ATR需要:{1}'. - format(len(self.lineBar), maxAtrLen + 1)) + format(line_bar_len,data_need_len)) return if self.mode == self.TICK_MODE: @@ -1843,7 +1850,7 @@ class CtaLineBar(object): j = 0 - for i in range(len(self.lineBar) - idx, len(self.lineBar) - idx - maxAtrLen, -1): # 周期 inputP + for i in range(len(self.lineBar) - idx, len(self.lineBar) - idx - data_need_len, -1): # 周期 inputP # 3.1、计算TR # 当前周期最高与最低的价差 @@ -1880,10 +1887,11 @@ class CtaLineBar(object): # 计算 ATR if self.inputAtr1Len > 0: + data_4_atr1 = min(line_bar_len,self.inputAtr1Len) if len(self.lineAtr1) < 1: - self.barAtr1 = round(barTr1 / self.inputAtr1Len, self.round_n) + self.barAtr1 = round(barTr1 / data_4_atr1, self.round_n) else: - self.barAtr1 = round((self.lineAtr1[-1] * (self.inputAtr1Len - 1) + barTr1) / self.inputAtr1Len, + self.barAtr1 = round((self.lineAtr1[-1] * (data_4_atr1 - 1) + barTr1) / data_4_atr1, self.round_n) if len(self.lineAtr1) > self.inputAtr1Len + 1: @@ -1891,10 +1899,11 @@ class CtaLineBar(object): self.lineAtr1.append(self.barAtr1) if self.inputAtr2Len > 0: + data_4_atr2 = min(line_bar_len, self.inputAtr2Len) if len(self.lineAtr2) < 1: - self.barAtr2 = round(barTr2 / self.inputAtr2Len, self.round_n) + self.barAtr2 = round(barTr2 / data_4_atr2, self.round_n) else: - self.barAtr2 = round((self.lineAtr2[-1] * (self.inputAtr2Len - 1) + barTr2) / self.inputAtr2Len, + self.barAtr2 = round((self.lineAtr2[-1] * (data_4_atr2 - 1) + barTr2) / data_4_atr2, self.round_n) if len(self.lineAtr2) > self.inputAtr2Len + 1: @@ -1902,10 +1911,11 @@ class CtaLineBar(object): self.lineAtr2.append(self.barAtr2) if self.inputAtr3Len > 0: + data_4_atr3 = min(line_bar_len, self.inputAtr3Len) if len(self.lineAtr3) < 1: - self.barAtr3 = round(barTr3 / self.inputAtr3Len, self.round_n) + self.barAtr3 = round(barTr3 / data_4_atr3, self.round_n) else: - self.barAtr3 = round((self.lineAtr3[-1] * (self.inputAtr3Len - 1) + barTr3) / self.inputAtr3Len, + self.barAtr3 = round((self.lineAtr3[-1] * (data_4_atr3 - 1) + barTr3) / data_4_atr3, self.round_n) if len(self.lineAtr3) > self.inputAtr3Len + 1: @@ -2064,12 +2074,12 @@ class CtaLineBar(object): l = len(self.lineBar) if self.inputBollLen > EMPTY_INT: - if l < min(14, self.inputBollLen) + 1: + if l < min(7, self.inputBollLen) : self.debugCtaLog(u'数据未充分,当前Bar数据数量:{0},计算Boll需要:{1}'. format(len(self.lineBar), min(14, self.inputBollLen) + 1)) else: - if l < self.inputBollLen + 2: - bollLen = l - 1 + if l < self.inputBollLen : + bollLen = l else: bollLen = self.inputBollLen @@ -2600,6 +2610,8 @@ class CtaLineBar(object): self.lineKdjButtom.append(b) self.lastKdjTopButtom = self.lineKdjButtom[-1] + self.update_kd_cross() + def __recountKdj_TB(self, countInBar=False): """KDJ指标""" """ @@ -2622,7 +2634,7 @@ class CtaLineBar(object): if self.inputKdjTBLen <= EMPTY_INT: return if self.inputKdjTBLen + self.inputKdjSmoothLen > self.max_hold_bars: - self.max_hold_bars = self.inputKdjTBLen + self.inputKdjSmoothLen1 + 1 + self.max_hold_bars = self.inputKdjTBLen + self.inputKdjSmoothLen + 1 # if len(self.lineBar) < self.inputKdjTBLen + 1: # if not countInBar: @@ -2742,6 +2754,35 @@ class CtaLineBar(object): self.lineKdjButtom.append(b) self.lastKdjTopButtom = self.lineKdjButtom[-1] + self.update_kd_cross() + + def update_kd_cross(self): + """更新KDJ金叉死叉""" + if len(self.lineK) < 2 or len(self.lineD) < 2: + return + + # K值大于D值 + if self.lineK[-1] > self.lineD[-1]: + if self.lineK[-2] > self.lineD[-2]: + # 延续金叉 + self.kd_count = max(1,self.kd_count) + 1 + else: + # 发生金叉 + self.kd_count = 1 + self.kd_last_cross = round((self.lineK[-1] + self.lineK[-2])/2,2) + self.kd_cross_price = self.lineBar[-1].close + + # K值小于D值 + else: + if self.lineK[-2] < self.lineD[-2]: + # 延续死叉 + self.kd_count = min(-1, self.kd_count) - 1 + else: + # 发生死叉 + self.kd_count = -1 + self.kd_last_cross = round((self.lineK[-1] + self.lineK[-2]) / 2, 2) + self.kd_cross_price = self.lineBar[-1].close + def __recountMacd(self): """ Macd计算方法: @@ -3174,7 +3215,12 @@ class CtaLineBar(object): if ma5 <= 0 or ma5_ref1 <= 0: self.writeCtaLog(u'boll中轨计算均线异常') return - self.atan = math.atan((ma5 / ma5_ref1 - 1) * 100) * 180 / math.pi + if self.inputKF: + self.atan = math.atan((ma5 / ma5_ref1 - 1) * 100) * 180 / math.pi + else: + # 当前均值,与前5均值得价差,除以标准差 + self.atan = math.atan((ma5 - ma5_ref1)/self.lineBollStd[-1]) * 180 / math.pi + # atan2 = math.atan((ma5 / ma5_ref1 - 1) * 100) * 180 / math.pi # atan3 = math.atan(ma5 / ma5_ref1 - 1)* 100 self.atan = round(self.atan, 3) @@ -4340,11 +4386,11 @@ class CtaLineBar(object): return True # 夜盘23:00收盘 - if self.shortSymbol in NIGHT_MARKET_SQ3 and tick_dt.hour == 23 and tick_dt.minute == 00: + if self.shortSymbol in [NIGHT_MARKET_SQ3,NIGHT_MARKET_DL] and tick_dt.hour == 23 and tick_dt.minute == 00: return True # 夜盘23:30收盘 - if self.shortSymbol in NIGHT_MARKET_ZZ or self.shortSymbol in NIGHT_MARKET_DL: + if self.shortSymbol in NIGHT_MARKET_ZZ : if tick_dt.hour == 23 and tick_dt.minute == 30: return True @@ -4584,10 +4630,10 @@ class CtaMinuteBar(CtaLineBar): endtick = True # 夜盘23:00收盘 - if self.shortSymbol in NIGHT_MARKET_SQ3 and tick.datetime.hour == 23 and tick.datetime.minute == 00: + if self.shortSymbol in [NIGHT_MARKET_SQ3,NIGHT_MARKET_DL] and tick.datetime.hour == 23 and tick.datetime.minute == 00: endtick = True # 夜盘23:30收盘 - if self.shortSymbol in NIGHT_MARKET_ZZ or self.shortSymbol in NIGHT_MARKET_DL: + if self.shortSymbol in NIGHT_MARKET_ZZ : if tick.datetime.hour == 23 and tick.datetime.minute == 30: endtick = True @@ -4639,7 +4685,7 @@ class CtaMinuteBar(CtaLineBar): lastBar.high = max(lastBar.high, tick.lastPrice) lastBar.low = min(lastBar.low, tick.lastPrice) lastBar.close = tick.lastPrice - + lastBar.openInterest = tick.openInterest # 更新日内总交易量,和bar内交易量 lastBar.dayVolume = tick.volume if l1 == 1: @@ -4848,10 +4894,10 @@ class CtaHourBar(CtaLineBar): endtick = True # 夜盘23:00收盘 - if self.shortSymbol in NIGHT_MARKET_SQ3 and tick.datetime.hour == 23 and tick.datetime.minute == 00: + if self.shortSymbol in [NIGHT_MARKET_SQ3,NIGHT_MARKET_DL] and tick.datetime.hour == 23 and tick.datetime.minute == 00: endtick = True # 夜盘23:30收盘 - if self.shortSymbol in NIGHT_MARKET_ZZ or self.shortSymbol in NIGHT_MARKET_DL: + if self.shortSymbol in NIGHT_MARKET_ZZ : if tick.datetime.hour == 23 and tick.datetime.minute == 30: endtick = True @@ -4924,7 +4970,7 @@ class CtaHourBar(CtaLineBar): lastBar.high = max(lastBar.high, tick.lastPrice) lastBar.low = min(lastBar.low, tick.lastPrice) lastBar.close = tick.lastPrice - + lastBar.openInterest = tick.openInterest # 更新日内总交易量,和bar内交易量 lastBar.dayVolume = tick.volume if l1 == 1: @@ -5136,7 +5182,7 @@ class CtaDayBar(CtaLineBar): lastBar.high = max(lastBar.high, tick.lastPrice) lastBar.low = min(lastBar.low, tick.lastPrice) lastBar.close = tick.lastPrice - + lastBar.openInterest = tick.openInterest # 更新日内总交易量,和bar内交易量 lastBar.dayVolume = tick.volume if l1 == 1: @@ -5407,7 +5453,7 @@ class CtaWeekBar(CtaLineBar): lastBar.high = max(lastBar.high, tick.lastPrice) lastBar.low = min(lastBar.low, tick.lastPrice) lastBar.close = tick.lastPrice - + lastBar.openInterest = tick.openInterest # 更新日内总交易量,和bar内交易量 lastBar.dayVolume = tick.volume if l1 == 1: diff --git a/vnpy/trader/app/ctaStrategy/ctaPolicy.py b/vnpy/trader/app/ctaStrategy/ctaPolicy.py index b6a44e4f..01e177f9 100644 --- a/vnpy/trader/app/ctaStrategy/ctaPolicy.py +++ b/vnpy/trader/app/ctaStrategy/ctaPolicy.py @@ -66,14 +66,14 @@ class CtaPolicy(object): self.create_time = datetime.now() create_time = json_data.get('create_time',None) - if len(create_time) > 0: + if create_time is not None or len(create_time) > 0: try: self.create_time = datetime.strptime(create_time, '%Y-%m-%d %H:%M:%S') except Exception as ex: self.writeCtaError(u'解释create_time异常:{}'.format(str(ex))) self.create_time = datetime.now() - self.create_time = datetime.now() + save_time = json_data.get('save_time',None) if len(save_time)> 0: try: @@ -253,13 +253,9 @@ class TurtlePolicy(CtaPolicy): def __init__(self, strategy): super(TurtlePolicy, self).__init__(strategy) - self.allow_add_pos = False # 是否加仓 - self.add_pos_on_pips = EMPTY_INT # 价格超过开仓价多少点时加仓 - - self.tns_open_price = 0 # 首次开仓价格 - self.last_open_price = 0 # 最后一次加仓价格 - self.stop_price = 0 # 止损价 - self.exit_on_last_rtn_pips = 0 # 最高价/最低价回撤多少跳动 + self.tns_open_price = 0 # 首次开仓价格 + self.last_open_price = 0 # 最后一次加仓价格 + self.stop_price = 0 # 止损价 self.high_price_in_long = 0 # 多趋势时,最高价 self.low_price_in_short = 0 # 空趋势时,最低价 self.last_under_open_price = 0 # 低于首次开仓价的补仓价格 @@ -283,15 +279,12 @@ class TurtlePolicy(CtaPolicy): j['tns_open_date'] = self.tns_open_date j['tns_open_price'] = self.tns_open_price if self.tns_open_price is not None else 0 - j['allow_add_pos'] = self.allow_add_pos - j['add_pos_on_pips'] = self.add_pos_on_pips - j['exit_on_last_rtn_pips'] = self.exit_on_last_rtn_pips - j['last_open_price'] = self.last_open_price if self.last_open_price is not None else 0 j['stop_price'] = self.stop_price if self.stop_price is not None else 0 j['high_price_in_long'] = self.high_price_in_long if self.high_price_in_long is not None else 0 j['low_price_in_short'] = self.low_price_in_short if self.low_price_in_short is not None else 0 - j['add_pos_count_under_first_price'] = self.add_pos_count_under_first_price if self.add_pos_count_under_first_price is not None else 0 + j[ + 'add_pos_count_under_first_price'] = self.add_pos_count_under_first_price if self.add_pos_count_under_first_price is not None else 0 j['last_under_open_price'] = self.last_under_open_price if self.last_under_open_price is not None else 0 j['max_pos'] = self.max_pos if self.max_pos is not None else 0 @@ -419,9 +412,6 @@ class TurtlePolicy(CtaPolicy): self.writeCtaError(u'解释last_risk_level异常:{}'.format(str(ex))) self.last_risk_level = 0 - self.allow_add_pos=json_data.get('allow_add_pos',False) - self.add_pos_on_pips = json_data.get('add_pos_on_pips',1) - self.exit_on_last_rtn_pips = json_data.get('exit_on_last_rtn_pips',0) def clean(self): """ @@ -442,9 +432,6 @@ class TurtlePolicy(CtaPolicy): self.tns_has_opened = False self.last_risk_level = 0 self.tns_count = 0 - self.allow_add_pos = False - self.add_pos_on_pips = 1 - self.exit_on_last_rtn_pips = 0 class TrendPolicy(CtaPolicy): """ diff --git a/vnpy/trader/app/ctaStrategy/ctaPosition.py b/vnpy/trader/app/ctaStrategy/ctaPosition.py index 00e80177..2f613e68 100644 --- a/vnpy/trader/app/ctaStrategy/ctaPosition.py +++ b/vnpy/trader/app/ctaStrategy/ctaPosition.py @@ -16,8 +16,8 @@ class CtaPosition: def __init__(self, strategy): self.strategy = strategy - self.longPos = 0 # 多仓持仓 - self.shortPos = 0 # 空仓持仓 + self.longPos = 0 # 多仓持仓(正数) + self.shortPos = 0 # 空仓持仓(负数) self.pos = 0 # 持仓状态 0:空仓/对空平等; >=1 净多仓 ;<=-1 净空仓 self.maxPos = 1 # 最大持仓量(多仓+空仓总量) self.step = 1 # 增仓数量 @@ -45,7 +45,7 @@ class CtaPosition: if direction == DIRECTION_LONG: # 加多仓 if (max(self.pos, self.longPos) + vol) > self.maxPos: - self.writeCtaError(u'异常,超出仓位。净:{},多:{},加多:{},最大:{}' + self.writeCtaError(u'异常,超出仓位。净:{0},多:{1},加多:{2},最大:{3}' .format(self.pos, self.longPos, vol, self.maxPos)) # 只告警 @@ -62,7 +62,7 @@ class CtaPosition: if direction == DIRECTION_SHORT: # 加空仓 if (min(self.pos, self.shortPos) - vol) < (0 - self.maxPos): - self.writeCtaError(u'异常,超出仓位。净:{},空:{},加空:{},最大:{}' + self.writeCtaError(u'异常,超出仓位。净:{0},空:{1},加空:{2},最大:{3}' .format(self.pos, self.shortPos, vol, self.maxPos)) #return False @@ -91,7 +91,7 @@ class CtaPosition: if direction == DIRECTION_LONG: # 平空仓 Cover if self.shortPos + vol > 0: - self.writeCtaError(u'异常,超出仓位。净:{},空:{},平仓:{}'.format(self.pos, self.shortPos, vol)) + self.writeCtaError(u'异常,超出仓位。净:{0},空:{1},平仓:{2}'.format(self.pos, self.shortPos, vol)) #self.strategy.pos = self.pos #return False @@ -105,7 +105,7 @@ class CtaPosition: if direction == DIRECTION_SHORT: # 平多仓 if self.longPos - vol < 0: - self.writeCtaError(u'异常,超出仓位。净:{},多:{},平仓:{}'.format(self.pos, self.longPos, vol)) + self.writeCtaError(u'异常,超出仓位。净:{0},多:{1},平仓:{2}'.format(self.pos, self.longPos, vol)) #self.strategy.pos = self.pos #return False diff --git a/vnpy/trader/app/ctaStrategy/ctaTemplate.py b/vnpy/trader/app/ctaStrategy/ctaTemplate.py index 96ca1091..de808326 100644 --- a/vnpy/trader/app/ctaStrategy/ctaTemplate.py +++ b/vnpy/trader/app/ctaStrategy/ctaTemplate.py @@ -268,7 +268,6 @@ class CtaTemplate(object): content = self.name + ':' + content self.ctaEngine.writeCtaLog(content, strategy_name=self.name) except Exception as ex: - self.ctaEngine.writeCtaLog(content) # ---------------------------------------------------------------------- @@ -333,6 +332,8 @@ class CtaTemplate(object): def getFullSymbol(self, symbol): """获取全路径得合约名称""" + if symbol.endswith('SPD'): + return symbol short_symbol = self.ctaEngine.getShortSymbol(symbol) if short_symbol == symbol: return symbol @@ -353,7 +354,7 @@ class CtaTemplate(object): if dt is None: dt = datetime.now() - if dt.hour >= 21: + if dt.hour >= 20: if dt.isoweekday() == 5: # 星期五=》星期一 return (dt + timedelta(days=3)).strftime('%Y-%m-%d') @@ -456,7 +457,7 @@ class MatrixTemplate(CtaTemplate): def getPositions(self): """ 获取策略当前持仓 - :return: [{'vtSymbol':symbol,'direction':direction,'volume':volume] + :return: [{'vtSymbol':symbol,'direction':direction,'volume':volume,'price':price] """ if not self.position: return [] @@ -515,7 +516,7 @@ class MatrixTemplate(CtaTemplate): return if dt.hour == 14: - if dt.minute <= 59: + if dt.minute <= 55: self.tradeWindow = True return @@ -556,7 +557,7 @@ class MatrixTemplate(CtaTemplate): return # 上期 天然橡胶 23:00 - if self.shortSymbol in NIGHT_MARKET_SQ3: + if self.shortSymbol in [NIGHT_MARKET_SQ3,NIGHT_MARKET_DL]: if dt.hour == 22: if dt.minute <= 59: # 收市前29分钟 @@ -566,8 +567,9 @@ class MatrixTemplate(CtaTemplate): if dt.minute > 54: # 夜盘平仓 self.closeWindow = True return + # 郑商、大连 23:30 - if self.shortSymbol in NIGHT_MARKET_ZZ or self.shortSymbol in NIGHT_MARKET_DL: + if self.shortSymbol in NIGHT_MARKET_ZZ: if dt.hour == 22: self.tradeWindow = True return diff --git a/vnpy/trader/app/ctaStrategy/uiCtaWidget.py b/vnpy/trader/app/ctaStrategy/uiCtaWidget.py index bc1377a3..23f2a707 100644 --- a/vnpy/trader/app/ctaStrategy/uiCtaWidget.py +++ b/vnpy/trader/app/ctaStrategy/uiCtaWidget.py @@ -289,12 +289,9 @@ class CtaEngineManager(QtWidgets.QWidget): #---------------------------------------------------------------------- def updateCtaLog(self, event): """更新CTA相关日志""" - try: - log = event.dict_['data'] - content = '{}\t{}'.format(log.logTime, log.logContent) - self.ctaLogMonitor.append(content) - except Exception as ex: - print(u'update exception:{},{}'.format(str(ex),traceback.format_exc())) + log = event.dict_['data'] + content = '\t'.join([log.logTime, log.logContent]) + self.ctaLogMonitor.append(content) #---------------------------------------------------------------------- def registerEvent(self):