修改回测模块和实盘模块,增加策略中,获取当前资金权益,支持按照比例方式计算开仓数量,支持加仓减仓模式。

This commit is contained in:
msincenselee 2016-10-18 21:51:01 +08:00
parent 5a3ad183a0
commit e23b84e212

View File

@ -39,6 +39,9 @@ class BacktestingEngine(object):
TICK_MODE = 'tick' TICK_MODE = 'tick'
BAR_MODE = 'bar' BAR_MODE = 'bar'
REALTIME_MODE ='RealTime' # 逐笔交易计算资金,供策略获取资金容量,计算开仓数量
FINAL_MODE = 'Final' # 最后才统计交易,不适合按照百分比等开仓数量计算
#---------------------------------------------------------------------- #----------------------------------------------------------------------
def __init__(self): def __init__(self):
"""Constructor""" """Constructor"""
@ -54,7 +57,7 @@ class BacktestingEngine(object):
# 回测相关 # 回测相关
self.strategy = None # 回测策略 self.strategy = None # 回测策略
self.mode = self.BAR_MODE # 回测模式默认为K线 self.mode = self.BAR_MODE # 回测模式默认为K线
self.slippage = 0 # 回测时假设的滑点 self.slippage = 0 # 回测时假设的滑点
self.rate = 0 # 回测时假设的佣金比例(适用于百分比佣金) self.rate = 0 # 回测时假设的佣金比例(适用于百分比佣金)
self.size = 1 # 合约大小默认为1 self.size = 1 # 合约大小默认为1
@ -88,7 +91,44 @@ class BacktestingEngine(object):
self.dt = None # 最新的时间 self.dt = None # 最新的时间
self.gatewayName = u'BackTest' self.gatewayName = u'BackTest'
self.usageCompounding = False # 是否使用简单复利
# 费用情况
self.avaliable = EMPTY_FLOAT
self.percent = EMPTY_FLOAT
self.percentLimit = 30 # 投资仓位比例上限
# 回测计算相关
self.calculateMode = self.FINAL_MODE
self.usageCompounding = False # 是否使用简单复利 只针对FINAL_MODE有效
self.initCapital = 10000 # 期初资金
self.capital = self.initCapital # 资金 相当于Balance
self.maxCapital = self.initCapital # 资金最高净值
self.maxPnl = 0 # 最高盈利
self.minPnl = 0 # 最大亏损
self.maxVolume = 1 # 最大仓位数
self.wins = 0 # 盈利次数
self.totalResult = 0 # 总成交数量
self.totalTurnover = 0 # 总成交金额(合约面值)
self.totalCommission = 0 # 总手续费
self.totalSlippage = 0 # 总滑点
self.timeList = [] # 时间序列
self.pnlList = [] # 每笔盈亏序列
self.capitalList = [] # 盈亏汇总的时间序列
self.drawdownList = [] # 回撤的时间序列
self.drawdownRateList = [] # 最大回撤比例的时间序列
def getAccountInfo(self):
"""返回账号的实时权益,可用资金,仓位比例,投资仓位比例上限"""
if self.capital == EMPTY_FLOAT:
self.percent = EMPTY_FLOAT
return self.capital, self.avaliable, self.percent, self.percentLimit
#---------------------------------------------------------------------- #----------------------------------------------------------------------
def setStartDate(self, startDate='20100416', initDays=10): def setStartDate(self, startDate='20100416', initDays=10):
@ -452,6 +492,9 @@ class BacktestingEngine(object):
"""运行回测使用本地csv数据) """运行回测使用本地csv数据)
added by IncenseLee added by IncenseLee
""" """
self.capital = self.initCapital # 更新设置期初资金
if not filename: if not filename:
self.writeCtaLog(u'请指定回测数据文件') self.writeCtaLog(u'请指定回测数据文件')
return return
@ -517,6 +560,7 @@ class BacktestingEngine(object):
"""运行回测(使用Mysql数据 """运行回测(使用Mysql数据
added by IncenseLee added by IncenseLee
""" """
self.capital = self.initCapital # 更新设置期初资金
if not self.dataStartDate: if not self.dataStartDate:
self.writeCtaLog(u'回测开始日期未设置。') self.writeCtaLog(u'回测开始日期未设置。')
@ -587,6 +631,9 @@ class BacktestingEngine(object):
#---------------------------------------------------------------------- #----------------------------------------------------------------------
def runBacktesting(self): def runBacktesting(self):
"""运行回测""" """运行回测"""
self.capital = self.initCapital # 更新设置期初资金
# 载入历史数据 # 载入历史数据
self.loadHistoryData() self.loadHistoryData()
@ -800,6 +847,9 @@ class BacktestingEngine(object):
# 从字典中删除该限价单 # 从字典中删除该限价单
del self.workingLimitOrderDict[orderID] del self.workingLimitOrderDict[orderID]
if self.calculateMode == self.REALTIME_MODE:
self.realtimeCalculate()
#---------------------------------------------------------------------- #----------------------------------------------------------------------
def crossStopOrder(self): def crossStopOrder(self):
@ -871,7 +921,11 @@ class BacktestingEngine(object):
self.limitOrderDict[orderID] = order self.limitOrderDict[orderID] = order
# 从字典中删除该限价单 # 从字典中删除该限价单
del self.workingStopOrderDict[stopOrderID] del self.workingStopOrderDict[stopOrderID]
if self.calculateMode == self.REALTIME_MODE:
self.realtimeCalculate()
#---------------------------------------------------------------------- #----------------------------------------------------------------------
def insertData(self, dbName, collectionName, data): def insertData(self, dbName, collectionName, data):
@ -902,6 +956,268 @@ class BacktestingEngine(object):
"""输出内容""" """输出内容"""
print str(datetime.now()) + "\t" + content print str(datetime.now()) + "\t" + content
def realtimeCalculate(self):
"""实时计算交易结果"""
resultDict = OrderedDict() # 交易结果记录
longTrade = [] # 未平仓的多头交易
shortTrade = [] # 未平仓的空头交易
longid = EMPTY_STRING
shortid = EMPTY_STRING
for tradeid in self.tradeDict.keys():
trade = self.tradeDict[tradeid]
# 多头交易
if trade.direction == DIRECTION_LONG:
# 如果尚无空头交易
if not shortTrade:
longTrade.append(trade)
longid = tradeid
# 当前多头交易为平空
else:
gId = tradeid # 交易组(多个平仓数为一组)
gr = None # 组合的交易结果
coverVolume = trade.volume
while coverVolume > 0:
if len(shortTrade)==0:
self.writeCtaLog(u'异常,没有开空仓的数据')
break
# 从未平仓的空头交易
entryTrade = shortTrade.pop(0)
# 开空volume不大于平仓volume
if coverVolume >= entryTrade.volume:
coverVolume = coverVolume - entryTrade.volume
result = TradingResult(entryTrade.price, trade.price, -entryTrade.volume,
self.rate, self.slippage, self.size, groupId=gId)
self.writeCtaLog(u'{6} [{7}:开空{0},short:{1}]-[{8}:平空{2},cover:{3},vol:{4}],净盈亏:{5}'
.format(entryTrade.tradeTime, entryTrade.price,
trade.tradeTime, trade.price, entryTrade.volume, result.pnl,
gId, shortid, tradeid))
if type(gr) == type(None):
if coverVolume > 0:
# 属于组合
gr = copy.deepcopy(result)
# 删除开空交易单
del self.tradeDict[entryTrade.tradeID]
else:
# 不属于组合
resultDict[entryTrade.dt] = result
# 删除平空交易单,
del self.tradeDict[trade.tradeID]
# 删除开空交易单
del self.tradeDict[entryTrade.tradeID]
else:
# 更新组合的数据
gr.turnover = gr.turnover + result.turnover
gr.commission = gr.commission + result.commission
gr.slippage = gr.slippage + result.slippage
gr.pnl = gr.pnl + result.pnl
# 删除开空交易单
del self.tradeDict[entryTrade.tradeID]
# 所有仓位平完
if coverVolume == 0:
gr.volume = trade.volume
resultDict[entryTrade.dt] = gr
# 删除平空交易单,
del self.tradeDict[trade.tradeID]
# 开空volume,大于平仓volume需要更新减少tradeDict的数量。
else:
shortVolume = entryTrade.volume - coverVolume
result = TradingResult(entryTrade.price, trade.price, -coverVolume,
self.rate, self.slippage, self.size, groupId=gId)
self.writeCtaLog(u'{6} [{7}:开空{0},short:{1}]-[{8}:平空{2},cover:{3},vol:{4}],净盈亏:{5}'
.format(entryTrade.tradeTime, entryTrade.price,
trade.tradeTime, trade.price, coverVolume, result.pnl,
gId, shortid, tradeid))
# 更新减少开仓单的volume
entryTrade.volume = shortVolume
coverVolume = 0
if type(gr) == type(None):
resultDict[entryTrade.dt] = result
else:
# 更新组合的数据
gr.turnover = gr.turnover + result.turnover
gr.commission = gr.commission + result.commission
gr.slippage = gr.slippage + result.slippage
gr.pnl = gr.pnl + result.pnl
gr.volume = trade.volume
resultDict[entryTrade.dt] = gr
# 删除平空交易单,
del self.tradeDict[trade.tradeID]
if type(gr) != type(None):
self.writeCtaLog(u'组合净盈亏:{0}'.format(gr.pnl))
self.writeCtaLog(u'-------------')
# 空头交易
else:
# 如果尚无多头交易
if not longTrade:
shortTrade.append(trade)
shortid = tradeid
# 当前空头交易为平多
else:
gId = tradeid # 交易组(多个平仓数为一组)
gr = None # 组合的交易结果
sellVolume = trade.volume
while sellVolume > 0:
if len(longTrade)==0:
self.writeCtaLog(u'异常,没有开多单')
break
entryTrade = longTrade.pop(0)
# 开多volume不大于平仓volume
if sellVolume >= entryTrade.volume:
sellVolume = sellVolume - entryTrade.volume
result = TradingResult(entryTrade.price, trade.price, entryTrade.volume,
self.rate, self.slippage, self.size, groupId= gId)
self.writeCtaLog(u'{6} [{7}:开多{0},buy:{1}]-[{8}.平多{2},sell:{3},vol:{4}],净盈亏:{5}'
.format(entryTrade.tradeTime, entryTrade.price,
trade.tradeTime,trade.price, entryTrade.volume, result.pnl,
gId, longid, tradeid))
if type(gr) == type(None):
if sellVolume > 0:
# 属于组合
gr = copy.deepcopy(result)
# 删除开多交易单
del self.tradeDict[entryTrade.tradeID]
else:
# 不属于组合
resultDict[entryTrade.dt] = result
# 删除平多交易单,
del self.tradeDict[trade.tradeID]
# 删除开多交易单
del self.tradeDict[entryTrade.tradeID]
else:
# 更新组合的数据
gr.turnover = gr.turnover + result.turnover
gr.commission = gr.commission + result.commission
gr.slippage = gr.slippage + result.slippage
gr.pnl = gr.pnl + result.pnl
# 删除开多交易单
del self.tradeDict[entryTrade.tradeID]
if sellVolume == 0:
gr.volume = trade.volume
resultDict[entryTrade.dt] = gr
# 删除平多交易单,
del self.tradeDict[trade.tradeID]
# 开多volume,大于平仓volume需要更新减少tradeDict的数量。
else:
longVolume = entryTrade.volume -sellVolume
result = TradingResult(entryTrade.price, trade.price, sellVolume,
self.rate, self.slippage, self.size, groupId= gId)
self.writeCtaLog(u'{6} [{7}:开多{0},buy:{1}]-[{8}.平多{2},sell:{3},vol:{4}],净盈亏:{5}'
.format(entryTrade.tradeTime, entryTrade.price,
trade.tradeTime,trade.price, sellVolume, result.pnl,
gId, longid, tradeid))
# 减少开多volume
entryTrade.volume = longVolume
sellVolume = 0
if type(gr) == type(None):
resultDict[entryTrade.dt] = result
else:
# 更新组合的数据
gr.turnover = gr.turnover + result.turnover
gr.commission = gr.commission + result.commission
gr.slippage = gr.slippage + result.slippage
gr.pnl = gr.pnl + result.pnl
gr.volume = trade.volume
resultDict[entryTrade.dt] = gr
# 删除平多交易单,
del self.tradeDict[trade.tradeID]
if type(gr) != type(None):
self.writeCtaLog(u'组合净盈亏:{0}'.format(gr.pnl))
self.writeCtaLog(u'-------------')
# 检查是否有平交易
if not resultDict:
# Todo:计算仓位比例
occupyMoney = EMPTY_FLOAT
if len(longTrade) > 0:
for t in longTrade:
occupyMoney += t.price * abs(t.volume) * self.size*0.11
if len(shortTrade)>0:
for t in longTrade:
occupyMoney += t.price * abs(t.volume) * self.size*0.11
self.avaliable = self.capital - occupyMoney
self.percent = round(float(self.avaliable*100/self.capital),2)
return
for time, result in resultDict.items():
if result.pnl > 0:
self.wins += 1
self.capital += result.pnl
self.maxCapital = max(self.capital, self.maxCapital)
self.maxVolume = max(self.maxVolume, result.volume)
drawdown = self.capital - self.maxCapital
drawdownRate = round(float(drawdown*100/self.maxCapital),4)
self.pnlList.append(result.pnl)
self.timeList.append(time)
self.capitalList.append(self.capital)
self.drawdownList.append(drawdown)
self.drawdownRateList.append(drawdownRate)
self.totalResult += 1
self.totalTurnover += result.turnover
self.totalCommission += result.commission
self.totalSlippage += result.slippage
self.output(u'[{5}]Vol:{0},盈亏:{1},回撤:{2}/{3},权益:{4}'.
format(abs(result.volume), result.pnl, drawdown, drawdownRate, self.capital, result.groupId))
#---------------------------------------------------------------------- #----------------------------------------------------------------------
def calculateBacktestingResult(self): def calculateBacktestingResult(self):
""" """
@ -911,7 +1227,10 @@ class BacktestingEngine(object):
例如前面共有6次开仓1手开仓+5次加仓每次1手平仓只有1次六手那么交易次数是6次开仓+平仓 例如前面共有6次开仓1手开仓+5次加仓每次1手平仓只有1次六手那么交易次数是6次开仓+平仓
暂不支持每次加仓数目不一致的核对因为比较复杂 暂不支持每次加仓数目不一致的核对因为比较复杂
增加组合的支持 增加组合的支持组合中仍然按照1手逐步加仓和多手平仓的方法即使启用了复利模式也仍然按照这个规则只是在计算收益时才乘以系数
增加期初权益每次交易后的权益可用资金仓位比例
""" """
self.output(u'计算回测结果') self.output(u'计算回测结果')
@ -1036,77 +1355,86 @@ class BacktestingEngine(object):
return {} return {}
# 然后基于每笔交易的结果,我们可以计算具体的盈亏曲线和最大回撤等 # 然后基于每笔交易的结果,我们可以计算具体的盈亏曲线和最大回撤等
"""
initCapital = 40000 # 期初资金 initCapital = 40000 # 期初资金
capital = initCapital # 资金 capital = initCapital # 资金
maxCapital = initCapital # 资金最高净值 maxCapital = initCapital # 资金最高净值
drawdown = 0 # 回撤
maxPnl = 0 # 最高盈利 maxPnl = 0 # 最高盈利
minPnl = 0 # 最大亏损 minPnl = 0 # 最大亏损
maxVolume = 1 # 最大仓位数 maxVolume = 1 # 最大仓位数
compounding = 1 # 简单的复利基数如果资金是期初资金的x倍就扩大开仓比例,例如3w开1手6w开2手12w开4手)
wins = 0 wins = 0
totalResult = 0 # 总成交数量 totalResult = 0 # 总成交数量
totalTurnover = 0 # 总成交金额(合约面值) totalTurnover = 0 # 总成交金额(合约面值)
totalCommission = 0 # 总手续费 totalCommission = 0 # 总手续费
totalSlippage = 0 # 总滑点 totalSlippage = 0 # 总滑点
timeList = [] # 时间序列 timeList = [] # 时间序列
pnlList = [] # 每笔盈亏序列 pnlList = [] # 每笔盈亏序列
capitalList = [] # 盈亏汇总的时间序列 capitalList = [] # 盈亏汇总的时间序列
drawdownList = [] # 回撤的时间序列 drawdownList = [] # 回撤的时间序列
drawdownRateList = [] # 最大回撤比例的时间序列 drawdownRateList = [] # 最大回撤比例的时间序列
"""
drawdown = 0 # 回撤
compounding = 1 # 简单的复利基数如果资金是期初资金的x倍就扩大开仓比例,例如3w开1手6w开2手12w开4手)
for time, result in resultDict.items(): for time, result in resultDict.items():
# 是否使用简单复利 # 是否使用简单复利
if self.usageCompounding: if self.usageCompounding:
compounding = int(capital/initCapital) compounding = int(self.capital/self.initCapital)
if result.pnl > 0: if result.pnl > 0:
wins += 1 self.wins += 1
capital += result.pnl*compounding self.capital += result.pnl*compounding
maxCapital = max(capital, maxCapital) self.maxCapital = max(self.capital, self.maxCapital)
maxVolume = max(maxVolume, result.volume*compounding) self.maxVolume = max(self.maxVolume, result.volume*compounding)
drawdown = capital - maxCapital drawdown = self.capital - self.maxCapital
drawdownRate = round(float(drawdown*100/maxCapital),4) drawdownRate = round(float(drawdown*100/self.maxCapital),4)
pnlList.append(result.pnl*compounding)
timeList.append(time)
capitalList.append(capital)
drawdownList.append(drawdown)
drawdownRateList.append(drawdownRate)
totalResult += 1
totalTurnover += result.turnover*compounding
totalCommission += result.commission*compounding
totalSlippage += result.slippage*compounding
self.pnlList.append(result.pnl*compounding)
self.timeList.append(time)
self.capitalList.append(self.capital)
self.drawdownList.append(drawdown)
self.drawdownRateList.append(drawdownRate)
self.totalResult += 1
self.totalTurnover += result.turnover*compounding
self.totalCommission += result.commission*compounding
self.totalSlippage += result.slippage*compounding
def getResult(self):
# 返回回测结果 # 返回回测结果
d = {} d = {}
d['initCapital'] = initCapital d['initCapital'] = self.initCapital
d['capital'] = capital - initCapital d['capital'] = self.capital - self.initCapital
d['maxCapital'] = maxCapital d['maxCapital'] = self.maxCapital
d['drawdown'] = drawdown
d['maxPnl'] = max(pnlList) d['maxPnl'] = max(self.pnlList)
d['minPnl'] = min(pnlList) d['minPnl'] = min(self.pnlList)
d['maxVolume'] = maxVolume d['maxVolume'] = self.maxVolume
d['totalResult'] = totalResult d['totalResult'] = self.totalResult
d['totalTurnover'] = totalTurnover d['totalTurnover'] = self.totalTurnover
d['totalCommission'] = totalCommission d['totalCommission'] = self.totalCommission
d['totalSlippage'] = totalSlippage d['totalSlippage'] = self.totalSlippage
d['timeList'] = timeList d['timeList'] = self.timeList
d['pnlList'] = pnlList d['pnlList'] = self.pnlList
d['capitalList'] = capitalList d['capitalList'] = self.capitalList
d['drawdownList'] = drawdownList d['drawdownList'] = self.drawdownList
d['drawdownRateList'] = drawdownRateList d['drawdownRateList'] = self.drawdownRateList
d['winRate'] = round(100*wins/len(pnlList),4) d['winRate'] = round(100*self.wins/len(self.pnlList),4)
return d return d
#---------------------------------------------------------------------- #----------------------------------------------------------------------
def showBacktestingResult(self): def showBacktestingResult(self):
"""显示回测结果""" """显示回测结果"""
d = self.calculateBacktestingResult() if self.calculateMode != self.REALTIME_MODE :
self.calculateBacktestingResult()
d = self.getResult()
if len(d)== 0: if len(d)== 0:
self.output(u'无交易结果') self.output(u'无交易结果')