diff --git a/vnpy/data/binance/__init__.py b/vnpy/data/binance/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/vnpy/data/binance/binance_data.py b/vnpy/data/binance/binance_data.py new file mode 100644 index 00000000..8821f4e3 --- /dev/null +++ b/vnpy/data/binance/binance_data.py @@ -0,0 +1,119 @@ +# encoding: UTF-8 + +# 从binance下载数据 +from datetime import datetime, timezone +import sys +import requests +import execjs +import traceback +from vnpy.trader.app.ctaStrategy.ctaBase import CtaBarData +from vnpy.api.binance.client import Client +from vnpy.trader.vtFunction import getJsonPath +from vnpy.trader.vtGlobal import globalSetting +import json + +PERIOD_MAPPING = {} +PERIOD_MAPPING['1min'] = '1m' +PERIOD_MAPPING['3min'] = '3m' +PERIOD_MAPPING['5min'] = '5m' +PERIOD_MAPPING['15min'] = '15m' +PERIOD_MAPPING['30min'] = '30m' +PERIOD_MAPPING['1hour'] = '1h' +PERIOD_MAPPING['2hour'] = '2h' +PERIOD_MAPPING['4hour'] = '4h' +PERIOD_MAPPING['6hour'] = '6h' +PERIOD_MAPPING['8hour'] = '8h' +PERIOD_MAPPING['12hour'] = '12h' +PERIOD_MAPPING['1day'] = '1d' +PERIOD_MAPPING['3day'] = '3d' +PERIOD_MAPPING['1week'] = '1w' +PERIOD_MAPPING['1month'] = '1M' + +SYMBOL_LIST = ['ltc_btc', 'eth_btc', 'etc_btc', 'bch_btc', 'btc_usdt', 'eth_usdt', 'ltc_usdt', 'etc_usdt', 'bch_usdt', + 'etc_eth','bt1_btc','bt2_btc','btg_btc','qtum_btc','hsr_btc','neo_btc','gas_btc', + 'qtum_usdt','hsr_usdt','neo_usdt','gas_usdt'] + + +class BinanceData(object): + + # ---------------------------------------------------------------------- + def __init__(self, strategy): + """ + 构造函数 + :param strategy: 上层策略,主要用与使用strategy.writeCtaLog() + """ + self.strategy = strategy + self.client = None + self.init_client() + + def init_client(self): + fileName = globalSetting.get('gateway_name', '') + '_connect.json' + filePath = getJsonPath(fileName, __file__) + try: + with open(filePath, 'r') as f: + # 解析json文件 + setting = json.load(f) + apiKey = setting.get('accessKey',None) + secretKey = setting.get('secretKey',None) + + if apiKey is not None and secretKey is not None: + self.client = Client(apiKey, secretKey) + + except IOError: + self.strategy.writeCtaError(u'BINANCE读取连接配置{}出错,请检查'.format(filePath)) + return + + def get_bars(self, symbol, period, callback, bar_is_completed=False,bar_freq=1, start_dt=None): + """ + 返回k线数据 + symbol:合约 + period: 周期: 1min,3min,5min,15min,30min,1day,3day,1hour,2hour,4hour,6hour,12hour + """ + if symbol not in SYMBOL_LIST: + self.strategy.writeCtaError(u'{} 合约{}不在下载清单中'.format(datetime.now(), symbol)) + return False + if period not in PERIOD_MAPPING: + self.strategy.writeCtaError(u'{} 周期{}不在下载清单中'.format(datetime.now(), period)) + return False + if self.client is None: + return False + + binance_symbol = symbol.upper().replace('_' , '') + binance_period = PERIOD_MAPPING.get(period) + + self.strategy.writeCtaLog('{}开始下载binance:{} {}数据.'.format(datetime.now(), binance_symbol, binance_period)) + + bars = [] + try: + bars = self.client.get_klines(symbol=binance_symbol, interval=binance_period) + for i, bar in enumerate(bars): + add_bar = CtaBarData() + try: + add_bar.vtSymbol = symbol + add_bar.symbol = symbol + add_bar.datetime = datetime.fromtimestamp(bar[0] / 1000) + add_bar.date = add_bar.datetime.strftime('%Y-%m-%d') + add_bar.time = add_bar.datetime.strftime('%H:%M:%S') + add_bar.tradingDay = add_bar.date + add_bar.open = float(bar[1]) + add_bar.high = float(bar[2]) + add_bar.low = float(bar[3]) + add_bar.close = float(bar[4]) + add_bar.volume = float(bar[5]) + except Exception as ex: + self.strategy.writeCtaError( + 'error when convert bar:{},ex:{},t:{}'.format(bar, str(ex), traceback.format_exc())) + return False + + if start_dt is not None and bar.datetime < start_dt: + continue + + if callback is not None: + callback(add_bar, bar_is_completed, bar_freq) + + return True + except Exception as ex: + self.strategy.writeCtaError('exception in get:{},{},{}'.format(binance_symbol,str(ex), traceback.format_exc())) + return False + + diff --git a/vnpy/data/okex/__init__.py b/vnpy/data/okex/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/vnpy/data/okex/okex_data.py b/vnpy/data/okex/okex_data.py new file mode 100644 index 00000000..ba27d25d --- /dev/null +++ b/vnpy/data/okex/okex_data.py @@ -0,0 +1,87 @@ +# encoding: UTF-8 + +# 从okex下载数据 +from datetime import datetime, timezone + +import requests +import execjs +import traceback +from vnpy.trader.app.ctaStrategy.ctaBase import CtaBarData, CtaTickData + +period_list = ['1min','3min','5min','15min','30min','1day','1week','1hour','2hour','4hour','6hour','12hour'] +symbol_list = ['ltc_btc','eth_btc','etc_btc','bch_btc','btc_usdt','eth_usdt','ltc_usdt','etc_usdt','bch_usdt', + 'etc_eth','bt1_btc','bt2_btc','btg_btc','qtum_btc','hsr_btc','neo_btc','gas_btc', + 'qtum_usdt','hsr_usdt','neo_usdt','gas_usdt'] + + +class OkexData(object): + + # ---------------------------------------------------------------------- + def __init__(self, strategy): + """ + 构造函数 + :param strategy: 上层策略,主要用与使用strategy.writeCtaLog() + """ + self.strategy = strategy + + # 设置HTTP请求的尝试次数,建立连接session + requests.adapters.DEFAULT_RETRIES = 5 + self.session = requests.session() + self.session.keep_alive = False + + def get_bars(self, symbol, period, callback, bar_is_completed=False,bar_freq=1, start_dt=None): + """ + 返回k线数据 + symbol:合约 + period: 周期: 1min,3min,5min,15min,30min,1day,3day,1hour,2hour,4hour,6hour,12hour + """ + if symbol not in symbol_list: + self.strategy.writeCtaError(u'{} {}不在下载清单中'.format(datetime.now(), symbol)) + return + + url = u'https://www.okex.com/api/v1/kline.do?symbol={}&type={}'.format(symbol, period) + self.strategy.writeCtaLog('{}开始下载:{} {}数据.URL:{}'.format(datetime.now(), symbol, period,url)) + + content = None + try: + content = self.session.get(url).content.decode('gbk') + except Exception as ex: + self.strategy.writeCtaError('exception in get:{},{},{}'.format(url,str(ex), traceback.format_exc())) + return + + bars = execjs.eval(content) + + for i, bar in enumerate(bars): + if len(bar) < 5: + self.strategy.writeCtaError('error when import bar:{}'.format(bar)) + return False + if i == 0: + continue + add_bar = CtaBarData() + try: + add_bar.vtSymbol = symbol + add_bar.symbol = symbol + add_bar.datetime = datetime.fromtimestamp(bar[0] / 1000) + add_bar.date = add_bar.datetime.strftime('%Y-%m-%d') + add_bar.time = add_bar.datetime.strftime('%H:%M:%S') + add_bar.tradingDay = add_bar.date + add_bar.open = float(bar[1]) + add_bar.high = float(bar[2]) + add_bar.low = float(bar[3]) + add_bar.close = float(bar[4]) + add_bar.volume = float(bar[5]) + except Exception as ex: + self.strategy.writeCtaError('error when convert bar:{},ex:{},t:{}'.format(bar, str(ex), traceback.format_exc())) + return False + + if start_dt is not None and bar.datetime < start_dt: + continue + + if callback is not None: + callback(add_bar, bar_is_completed, bar_freq) + + return True + + + + diff --git a/vnpy/trader/app/ctaStrategy/ctaBacktesting.py b/vnpy/trader/app/ctaStrategy/ctaBacktesting.py index 72bd78fd..6f4e9e86 100644 --- a/vnpy/trader/app/ctaStrategy/ctaBacktesting.py +++ b/vnpy/trader/app/ctaStrategy/ctaBacktesting.py @@ -3,6 +3,8 @@ ''' 本文件中包含的是CTA模块的回测引擎,回测引擎的API和CTA引擎一致, 可以使用和实盘相同的代码进行回测。 + +修改者: 华富资产/李来佳/28888502 ''' from __future__ import division @@ -32,7 +34,6 @@ from vnpy.trader.vtFunction import loadMongoSetting from vnpy.trader.vtEvent import * from vnpy.trader.setup_logger import setup_logger - ######################################################################## class BacktestingEngine(object): """ @@ -125,12 +126,13 @@ class BacktestingEngine(object): self.last_leg1_tick = None self.last_leg2_tick = None self.last_bar = None + self.is_7x24 = False # csvFile相关 self.barTimeInterval = 60 # csv文件,属于K线类型,K线的周期(秒数),缺省是1分钟 # 费用情况 - self.avaliable = EMPTY_FLOAT + self.percent = EMPTY_FLOAT self.percentLimit = 30 # 投资仓位比例上限 @@ -143,6 +145,7 @@ class BacktestingEngine(object): self.netCapital = self.initCapital # 实时资金净值(每日根据capital和持仓浮盈计算) self.maxCapital = self.initCapital # 资金最高净值 self.maxNetCapital = self.initCapital + self.avaliable = self.initCapital self.maxPnl = 0 # 最高盈利 self.minPnl = 0 # 最大亏损 @@ -168,8 +171,10 @@ class BacktestingEngine(object): self.daily_max_drawdown_rate = 0 # 按照日结算价计算 self.dailyList = [] - self.exportTradeList = [] # 导出交易记录列表 + self.daily_first_benchmark = None + self.exportTradeList = [] # 导出交易记录列表 + self.export_wenhua_signal = False self.fixCommission = EMPTY_FLOAT # 固定交易费用 self.logger = None @@ -1569,10 +1574,6 @@ class BacktestingEngine(object): self.strategy.onInit() self.output(u'策略初始化完成') - self.strategy.trading = True - self.strategy.onStart() - self.output(u'策略启动完成') - self.output(u'开始回放数据') import csv @@ -1609,14 +1610,14 @@ class BacktestingEngine(object): if 'trading_date' in row: bar.tradingDay = row['trading_date'] else: - if bar.datetime.hour >=21: + 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: + 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: @@ -1626,11 +1627,16 @@ class BacktestingEngine(object): 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) + self.maxCapital,self.totalCommission,benchmark=bar.close) last_tradingDay = bar.tradingDay self.newBar(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 @@ -1844,7 +1850,7 @@ class BacktestingEngine(object): self.strategy.name = self.strategy.className self.strategy.onInit() - self.strategy.onStart() + #self.strategy.onStart() # --------------------------------------------------------------------- def saveStrategyData(self): @@ -2167,7 +2173,7 @@ class BacktestingEngine(object): self.logger = setup_logger(filename=filename, name=self.strategy_name if len(self.strategy_name) > 0 else 'strategy', debug=debug,backtesing=True) #---------------------------------------------------------------------- - def writeCtaLog(self, content): + def writeCtaLog(self, content,strategy_name=None): """记录日志""" #log = str(self.dt) + ' ' + content #self.logList.append(log) @@ -2178,17 +2184,17 @@ class BacktestingEngine(object): else: self.createLogger() - def writeCtaError(self, content): + def writeCtaError(self, content,strategy_name=None): """记录异常""" self.output(u'Error:{}'.format(content)) self.writeCtaLog(content) - def writeCtaWarning(self, content): + def writeCtaWarning(self, content,strategy_name=None): """记录告警""" self.output(u'Warning:{}'.format(content)) self.writeCtaLog(content) - def writeCtaNotification(self,content): + def writeCtaNotification(self,content,strategy_name=None): """记录通知""" #print content self.output(u'Notify:{}'.format(content)) @@ -2613,7 +2619,7 @@ class BacktestingEngine(object): self.avaliable = self.netCapital - occupyMoney self.percent = round(float(occupyMoney * 100 / self.netCapital), 2) - def savingDailyData(self, d, c, m, commission): + def savingDailyData(self, d, c, m, commission, benchmark=0): """保存每日数据""" dict = {} dict['date'] = d.strftime('%Y/%m/%d') @@ -2624,6 +2630,14 @@ class BacktestingEngine(object): long_pos_occupy_money = 0 short_pos_occupy_money = 0 + if self.daily_first_benchmark is None and benchmark >0: + self.daily_first_benchmark = benchmark + + if benchmark > 0 and self.daily_first_benchmark is not None and self.daily_first_benchmark > 0: + benchmark = benchmark / self.daily_first_benchmark + else: + benchmark = 1 + for longpos in self.longPosition: symbol = '-' if longpos.vtSymbol == EMPTY_STRING else longpos.vtSymbol # 计算持仓浮盈浮亏/占用保证金 @@ -2673,7 +2687,7 @@ class BacktestingEngine(object): dict['occupyMoney'] = max(long_pos_occupy_money, short_pos_occupy_money) dict['occupyRate'] = dict['occupyMoney'] / dict['capital'] dict['commission'] = commission - + dict['benchmark'] = benchmark self.dailyList.append(dict) # 更新每日浮动净值 @@ -2688,6 +2702,45 @@ class BacktestingEngine(object): self.daily_max_drawdown_rate = drawdown_rate self.max_drowdown_rate_time = dict['date'] + # ---------------------------------------------------------------------- + def writeWenHuaSignal(self, filehandle, count, bardatetime, price, text): + """ + 输出到文华信号 + :param filehandle: + :param count: + :param bardatetime: + :param text: + :return: + """ + # 文华信号 + barDate = bardatetime.strftime('%Y%m%d') + barTime = bardatetime.strftime('%H%M') + outputMsg = 'AA{}:=DATE={};\n'.format(count, barDate[2:]) + filehandle.write(outputMsg) + outputMsg = 'BB{}:=PERIOD=1&&TIME={};\n'.format(count, barDate[2:], barTime) + #filehandle.write(outputMsg) + outputMsg = 'CC{}:=PERIOD=2&&TIME>={}&&TIME<={}+3;\n'.format(count, barTime, barTime) + #filehandle.write(outputMsg) + outputMsg = 'DD{}:=PERIOD=3&&TIME>={}&&TIME<={}+5;\n'.format(count, barTime, barTime) + #filehandle.write(outputMsg) + outputMsg = 'EE{}:=PERIOD=4&&TIME>={}&&TIME<={}+10;\n'.format(count, barTime, barTime) + #filehandle.write(outputMsg) + outputMsg = 'FF{}:=PERIOD=5&&TIME>={}&&TIME<={}+30;\n'.format(count, barTime, barTime) + filehandle.write(outputMsg) + outputMsg = 'GG{}:=PERIOD=6&&TIME>={}&&TIME<={}+60;\n'.format(count, barTime, barTime) + filehandle.write(outputMsg) + outputMsg = 'HH{}:=PERIOD=7&&TIME>={}&&TIME<={}+120;\n'.format(count, barTime, barTime) + filehandle.write(outputMsg) + outputMsg = 'II{}:=PERIOD=8;\n'.format(count) + filehandle.write(outputMsg) + outputMsg = 'DRAWICON(AA{} AND ( FF{} OR GG{} OR HH{} OR II{}), L, \'ICO14\');\n'.format( + count, count, count, count, count) + filehandle.write(outputMsg) + outputMsg = 'DRAWTEXT(AA{} AND (FF{} OR GG{} OR HH{} OR II{}), {}, \'{}\');\n'.format( + count, count, count, count, count, price, text) + filehandle.write(outputMsg) + filehandle.flush() + # ---------------------------------------------------------------------- def calculateBacktestingResult(self): """ @@ -2947,6 +3000,68 @@ class BacktestingEngine(object): for row in self.exportTradeList: writer.writerow(row) + if self.export_wenhua_signal: + 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: return @@ -2959,7 +3074,7 @@ class BacktestingEngine(object): self.writeCtaLog(u'save daily records to:{}'.format(csvOutputFile2)) csvWriteFile2 = open(csvOutputFile2, 'w', encoding='utf8',newline='') - fieldnames = ['date', 'capital','net', 'maxCapital','rate', 'commission', 'longMoney','shortMoney','occupyMoney','occupyRate','longPos','shortPos'] + fieldnames = ['date', 'capital','net', 'maxCapital','rate', 'commission', 'longMoney','shortMoney','occupyMoney','occupyRate','longPos','shortPos','benchmark'] writer2 = csv.DictWriter(f=csvWriteFile2, fieldnames=fieldnames, dialect='excel') writer2.writeheader() @@ -3332,38 +3447,5 @@ def optimize(strategyClass, setting, targetName, return (str(setting), targetValue) -if __name__ == '__main__': - # 以下内容是一段回测脚本的演示,用户可以根据自己的需求修改 - # 建议使用ipython notebook或者spyder来做回测 - # 同样可以在命令模式下进行回测(一行一行输入运行) - from strategy.strategyEmaDemo import * - - # 创建回测引擎 - engine = BacktestingEngine() - - # 设置引擎的回测模式为K线 - engine.setBacktestingMode(engine.BAR_MODE) - - # 设置回测用的数据起始日期 - engine.setStartDate('20110101') - - # 载入历史数据到引擎中 - engine.setDatabase(MINUTE_DB_NAME, 'IF0000') - - # 设置产品相关参数 - engine.setSlippage(0.2) # 股指1跳 - engine.setRate(0.3/10000) # 万0.3 - engine.setSize(300) # 股指合约大小 - - # 在引擎中创建策略对象 - engine.initStrategy(EmaDemoStrategy, {}) - - # 开始跑回测 - engine.runBacktesting() - - # 显示回测结果 - # spyder或者ipython notebook中运行时,会弹出盈亏曲线图 - # 直接在cmd中回测则只会打印一些回测数值 - engine.showBacktestingResult() \ No newline at end of file diff --git a/vnpy/trader/app/ctaStrategy/ctaEngine.py b/vnpy/trader/app/ctaStrategy/ctaEngine.py index adcae542..5ba25431 100644 --- a/vnpy/trader/app/ctaStrategy/ctaEngine.py +++ b/vnpy/trader/app/ctaStrategy/ctaEngine.py @@ -112,7 +112,7 @@ class CtaEngine(object): self.strategy_group = EMPTY_STRING self.logger = None - + self.strategy_loggers = {} self.createLogger() # ---------------------------------------------------------------------- @@ -128,6 +128,7 @@ class CtaEngine(object): req = VtOrderReq() req.symbol = contract.symbol # 合约代码 req.exchange = contract.exchange # 交易所 + req.vtSymbol = contract.vtSymbol req.price = self.roundToPriceTick(contract.priceTick, price) # 价格 req.volume = volume # 数量 @@ -410,9 +411,9 @@ class CtaEngine(object): for key in d.keys(): d[key] = tick.__getattribute__(key) - if ctaTick.datetime: + if not ctaTick.datetime: # 添加datetime字段 - ctaTick.datetime = datetime.strptime(' '.join([tick.date, tick.time]), '%Y%m%d %H:%M:%S.%f') + ctaTick.datetime = datetime.strptime(' '.join([tick.date, tick.time]), '%Y-%m-%d %H:%M:%S.%f') # 逐个推送到策略实例中 l = self.tickStrategyDict[tick.vtSymbol] @@ -471,7 +472,7 @@ class CtaEngine(object): self.posBufferDict[trade.vtSymbol] = posBuffer posBuffer.updateTradeData(trade) - # ---------------------------------------------------------------------- + # ---------------------------------------------------------------------- def processPositionEvent(self, event): """处理持仓推送""" @@ -615,7 +616,7 @@ class CtaEngine(object): # ---------------------------------------------------------------------- # 日志相关 - def writeCtaLog(self, content): + def writeCtaLog(self, content, strategy_name=None): """快速发出CTA模块日志事件""" log = VtLogData() log.logContent = content @@ -623,13 +624,19 @@ class CtaEngine(object): event.dict_['data'] = log self.eventEngine.put(event) - # 写入本地log日志 - if self.logger: - self.logger.info(content) + if strategy_name is None: + # 写入本地log日志 + if self.logger: + self.logger.info(content) + else: + self.createLogger() else: - self.createLogger() + if strategy_name in self.strategy_loggers: + self.strategy_loggers[strategy_name].info(content) + else: + self.createLogger(strategy_name=strategy_name) - def createLogger(self): + def createLogger(self, strategy_name=None): """ 创建日志记录 :return: @@ -642,25 +649,58 @@ class CtaEngine(object): # 否则,使用缺省保存目录 vnpy/trader/app/ctaStrategy/data path = os.path.abspath(os.path.join(os.path.dirname(__file__), '..', '..', 'logs')) - filename = os.path.abspath(os.path.join(path, 'ctaEngine')) + if strategy_name is None: + filename = os.path.abspath(os.path.join(path, 'ctaEngine')) - print(u'create logger:{}'.format(filename)) - self.logger = setup_logger(filename=filename, name='ctaEngine', debug=True) + print(u'create logger:{}'.format(filename)) + self.logger = setup_logger(filename=filename, name='ctaEngine', debug=True) + else: + filename = os.path.abspath(os.path.join(path, str(strategy_name))) + print(u'create logger:{}'.format(filename)) + self.strategy_loggers[strategy_name] = setup_logger(filename=filename, name=str(strategy_name), debug=True) - def writeCtaError(self, content): + def writeCtaError(self, content, strategy_name=None): """快速发出CTA模块错误日志事件""" + if strategy_name is not None: + if strategy_name in self.strategy_loggers: + self.strategy_loggers[strategy_name].error(content) + else: + self.createLogger(strategy_name=strategy_name) + try: + self.strategy_loggers[strategy_name].error(content) + except Exception as ex: + pass + self.mainEngine.writeError(content) - def writeCtaWarning(self, content): + def writeCtaWarning(self, content, strategy_name=None): """快速发出CTA模块告警日志事件""" + if strategy_name is not None: + if strategy_name in self.strategy_loggers: + self.strategy_loggers[strategy_name].warning(content) + else: + self.createLogger(strategy_name=strategy_name) + try: + self.strategy_loggers[strategy_name].warning(content) + except Exception as ex: + pass self.mainEngine.writeWarning(content) - def writeCtaNotification(self, content): + def writeCtaNotification(self, content, strategy_name=None): """快速发出CTA模块通知事件""" self.mainEngine.writeNotification(content) - def writeCtaCritical(self, content): + def writeCtaCritical(self, content, strategy_name=None): """快速发出CTA模块异常日志事件""" + if strategy_name is not None: + if strategy_name in self.strategy_loggers: + self.strategy_loggers[strategy_name].critical(content) + else: + self.createLogger(strategy_name=strategy_name) + try: + self.strategy_loggers[strategy_name].critical(content) + except Exception as ex: + pass self.mainEngine.writeCritical(content) def sendCtaSignal(self, source, symbol, direction, price, level): @@ -830,7 +870,7 @@ class CtaEngine(object): symbols.append(strategy.Leg2Symbol) for symbol in symbols: - self.writeCtaLog(u'添加合约{0}与策略的匹配目录'.format(symbol)) + self.writeCtaLog(u'添加合约{}与策略{}的匹配目录'.format(symbol,strategy.name)) if symbol in self.tickStrategyDict: l = self.tickStrategyDict[symbol] else: @@ -840,6 +880,7 @@ class CtaEngine(object): # 3.订阅合约 self.writeCtaLog(u'向gateway订阅合约{0}'.format(symbol)) + self.pendingSubcribeSymbols[symbol] = strategy self.subscribe(strategy=strategy, symbol=symbol) # 自动初始化 @@ -1950,23 +1991,43 @@ class PositionBuffer(object): self.shortPosition = EMPTY_INT self.shortToday = EMPTY_INT self.shortYd = EMPTY_INT + #---------------------------------------------------------------------- def updatePositionData(self, pos): """更新持仓数据""" - if pos.direction == DIRECTION_LONG: - self.longPosition = pos.position # >=0 - self.longYd = pos.ydPosition # >=0 - self.longToday = self.longPosition - self.longYd # >=0 + if pos.direction == DIRECTION_SHORT: + self.shortPosition = pos.position # >=0 + self.shortYd = pos.ydPosition # >=0 + self.shortToday = self.shortPosition - self.shortYd # >=0 else: - self.shortPosition = pos.position # >=0 - self.shortYd = pos.ydPosition # >=0 - self.shortToday = self.shortPosition - self.shortYd # >=0 - + self.longPosition = pos.position # >=0 + self.longYd = pos.ydPosition # >=0 + self.longToday = self.longPosition - self.longYd # >=0 + #---------------------------------------------------------------------- def updateTradeData(self, trade): """更新成交数据""" - if trade.direction == DIRECTION_LONG: + + 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 @@ -1985,22 +2046,4 @@ class PositionBuffer(object): self.shortToday = 0 if self.shortYd <= 0: self.shortYd = 0 - # 多方平昨,对应空头的持仓和昨仓减少 - else: - # 空头和多头相同 - 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 + # 多方平昨,对应空头的持仓和昨仓减少 \ No newline at end of file diff --git a/vnpy/trader/app/ctaStrategy/ctaLineBar.py b/vnpy/trader/app/ctaStrategy/ctaLineBar.py index 130cd01a..6d7690bc 100644 --- a/vnpy/trader/app/ctaStrategy/ctaLineBar.py +++ b/vnpy/trader/app/ctaStrategy/ctaLineBar.py @@ -103,6 +103,8 @@ class CtaLineBar(object): self.round_n = 4 # round() 小数点的截断数量 self.activeDayJump = False # 隔夜跳空 + self.is_7x24 = False + # 当前的Tick self.curTick = None self.lastTick = None @@ -173,6 +175,7 @@ class CtaLineBar(object): self.paramList.append('inputYb') self.paramList.append('inputYbLen') self.paramList.append('inputYbRef') + self.paramList.append('is_7x24') self.paramList.append('minDiff') self.paramList.append('shortSymbol') @@ -389,14 +392,14 @@ class CtaLineBar(object): # self.writeCtaLog(u'无效的tick时间:{0}'.format(tick.datetime)) # return - if tick.datetime.hour == 8 or tick.datetime.hour == 20: + if not self.is_7x24 and (tick.datetime.hour == 8 or tick.datetime.hour == 20 ): self.writeCtaLog(u'竞价排名tick时间:{0}'.format(tick.datetime)) return self.curTick = tick # 3.生成x K线,若形成新Bar,则触发OnBar事件 - self.__drawLineBar(tick) + self.drawLineBar(tick) # 更新curPeriod的High,low if self.curPeriod is not None: @@ -613,7 +616,7 @@ class CtaLineBar(object): return msg - def __firstTick(self, tick): + def firstTick(self, tick): """ K线的第一个Tick数据""" self.bar = CtaBarData() # 创建新的K线 @@ -661,14 +664,14 @@ class CtaLineBar(object): self.lineBar.append(self.bar) # 推入到lineBar队列 # ---------------------------------------------------------------------- - def __drawLineBar(self, tick): + def drawLineBar(self, tick): """生成 line Bar """ l1 = len(self.lineBar) # 保存第一个K线数据 if l1 == 0: - self.__firstTick(tick) + self.firstTick(tick) return # 清除480周期前的数据, @@ -680,24 +683,25 @@ class CtaLineBar(object): # 处理日内的间隔时段最后一个tick,如10:15分,11:30分,15:00 和 2:30分 endtick = False - if (tick.datetime.hour == 10 and tick.datetime.minute == 15) \ - or (tick.datetime.hour == 11 and tick.datetime.minute == 30) \ - or (tick.datetime.hour == 15 and tick.datetime.minute == 00) \ - or (tick.datetime.hour == 2 and tick.datetime.minute == 30): - endtick = True - - # 夜盘1:30收盘 - if self.shortSymbol in NIGHT_MARKET_SQ2 and tick.datetime.hour == 1 and tick.datetime.minute == 00: - endtick = True - - # 夜盘23:00收盘 - if self.shortSymbol in NIGHT_MARKET_SQ3 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 tick.datetime.hour == 23 and tick.datetime.minute == 30: + if not self.is_7x24: + if (tick.datetime.hour == 10 and tick.datetime.minute == 15) \ + or (tick.datetime.hour == 11 and tick.datetime.minute == 30) \ + or (tick.datetime.hour == 15 and tick.datetime.minute == 00) \ + or (tick.datetime.hour == 2 and tick.datetime.minute == 30): endtick = True + # 夜盘1:30收盘 + if self.shortSymbol in NIGHT_MARKET_SQ2 and tick.datetime.hour == 1 and tick.datetime.minute == 00: + endtick = True + + # 夜盘23:00收盘 + if self.shortSymbol in NIGHT_MARKET_SQ3 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 tick.datetime.hour == 23 and tick.datetime.minute == 30: + endtick = True + # 满足时间要求 # 1,秒周期,tick的时间,距离最后一个bar的开始时间,已经超出bar的时间周期(barTimeInterval) # 2,分钟、小时周期,取整=0 @@ -730,7 +734,7 @@ class CtaLineBar(object): ) and not endtick: # 创建并推入新的Bar - self.__firstTick(tick) + self.firstTick(tick) # 触发OnBar事件 self.onBar(lastBar) @@ -750,7 +754,7 @@ class CtaLineBar(object): lastBar.volume = tick.volume else: # 针对新交易日的第一个bar:于上一个bar的时间在14,当前bar的时间不在14点,初始化为tick.volume - if self.lineBar[-2].datetime.hour == 14 and tick.datetime.hour != 14 and not endtick: + if self.lineBar[-2].datetime.hour == 14 and tick.datetime.hour != 14 and not endtick and not self.is_7x24: lastBar.volume = tick.volume else: # 其他情况,bar为同一交易日,将tick的volume,减去上一bar的dayVolume @@ -809,7 +813,7 @@ class CtaLineBar(object): # ---------------------------------------------------------------------- def __recountSar(self): - """计算K线的MA1 和MA2""" + """计算K线的SAR""" l = len(self.lineBar) if l < 5: return @@ -2716,7 +2720,7 @@ class CtaMinuteBar(CtaLineBar): self.m1_bars_count += bar_freq # ---------------------------------------------------------------------- - def __drawLineBar(self, tick): + def drawLineBar(self, tick): """ 生成 line Bar :param tick: @@ -2727,7 +2731,7 @@ class CtaMinuteBar(CtaLineBar): # 保存第一个K线数据 if l1 == 0: - self.__firstTick(tick) + self.firstTick(tick) return # 清除480周期前的数据, @@ -2738,13 +2742,33 @@ class CtaMinuteBar(CtaLineBar): lastBar = self.lineBar[-1] is_new_bar = False + endtick = False + if not self.is_7x24: + # 处理日内的间隔时段最后一个tick,如10:15分,11:30分,15:00 和 2:30分 + if (tick.datetime.hour == 10 and tick.datetime.minute == 15) \ + or (tick.datetime.hour == 11 and tick.datetime.minute == 30) \ + or (tick.datetime.hour == 15 and tick.datetime.minute == 00) \ + or (tick.datetime.hour == 2 and tick.datetime.minute == 30): + endtick = True + + # 夜盘1:30收盘 + if self.shortSymbol in NIGHT_MARKET_SQ2 and tick.datetime.hour == 1 and tick.datetime.minute == 00: + endtick = True + + # 夜盘23:00收盘 + if self.shortSymbol in NIGHT_MARKET_SQ3 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 tick.datetime.hour == 23 and tick.datetime.minute == 30: + endtick = True # 不在同一交易日,推入新bar if self.curTradingDay != tick.tradingDay: is_new_bar = True self.curTradingDay = tick.tradingDay else: # 同一交易日,看分钟是否一致 - if tick.datetime.minute != self.last_minute: + if tick.datetime.minute != self.last_minute and not endtick: self.m1_bars_count += 1 self.last_minute = tick.datetime.minute @@ -2753,7 +2777,7 @@ class CtaMinuteBar(CtaLineBar): if is_new_bar: # 创建并推入新的Bar - self.__firstTick(tick) + self.firstTick(tick) self.m1_bars_count = 1 # 触发OnBar事件 self.onBar(lastBar) @@ -2913,7 +2937,7 @@ class CtaHourBar(CtaLineBar): self.__rt_countSkd() # ---------------------------------------------------------------------- - def __drawLineBar(self, tick): + def drawLineBar(self, tick): """ 生成 line Bar :param tick: @@ -2924,33 +2948,34 @@ class CtaHourBar(CtaLineBar): # 保存第一个K线数据 if l1 == 0: - self.__firstTick(tick) + self.firstTick(tick) return # 清除480周期前的数据, if l1 > 60 * 8: del self.lineBar[0] - - # 处理日内的间隔时段最后一个tick,如10:15分,11:30分,15:00 和 2:30分 endtick = False - if (tick.datetime.hour == 10 and tick.datetime.minute == 15) \ - or (tick.datetime.hour == 11 and tick.datetime.minute == 30) \ - or (tick.datetime.hour == 15 and tick.datetime.minute == 00) \ - or (tick.datetime.hour == 2 and tick.datetime.minute == 30): - endtick = True + if not self.is_7x24: + # 处理日内的间隔时段最后一个tick,如10:15分,11:30分,15:00 和 2:30分 - # 夜盘1:30收盘 - if self.shortSymbol in NIGHT_MARKET_SQ2 and tick.datetime.hour == 1 and tick.datetime.minute == 00: - endtick = True - - # 夜盘23:00收盘 - if self.shortSymbol in NIGHT_MARKET_SQ3 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 tick.datetime.hour == 23 and tick.datetime.minute == 30: + if (tick.datetime.hour == 10 and tick.datetime.minute == 15) \ + or (tick.datetime.hour == 11 and tick.datetime.minute == 30) \ + or (tick.datetime.hour == 15 and tick.datetime.minute == 00) \ + or (tick.datetime.hour == 2 and tick.datetime.minute == 30): endtick = True + # 夜盘1:30收盘 + if self.shortSymbol in NIGHT_MARKET_SQ2 and tick.datetime.hour == 1 and tick.datetime.minute == 00: + endtick = True + + # 夜盘23:00收盘 + if self.shortSymbol in NIGHT_MARKET_SQ3 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 tick.datetime.hour == 23 and tick.datetime.minute == 30: + endtick = True + # 与最后一个BAR的时间比对,判断是否超过K线周期 lastBar = self.lineBar[-1] is_new_bar = False @@ -2970,7 +2995,7 @@ class CtaHourBar(CtaLineBar): if is_new_bar: # 创建并推入新的Bar - self.__firstTick(tick) + self.firstTick(tick) self.m1_bars_count = 1 # 触发OnBar事件 self.onBar(lastBar) @@ -3148,7 +3173,7 @@ class CtaDayBar(CtaLineBar): self.__rt_countSkd() # ---------------------------------------------------------------------- - def __drawLineBar(self, tick): + def drawLineBar(self, tick): """ 生成 line Bar :param tick: @@ -3159,7 +3184,7 @@ class CtaDayBar(CtaLineBar): # 保存第一个K线数据 if l1 == 0: - self.__firstTick(tick) + self.firstTick(tick) return # 清除480周期前的数据, @@ -3176,7 +3201,7 @@ class CtaDayBar(CtaLineBar): if is_new_bar: # 创建并推入新的Bar - self.__firstTick(tick) + self.firstTick(tick) # 触发OnBar事件 self.onBar(lastBar) @@ -3217,6 +3242,7 @@ class CtaDayBar(CtaLineBar): if self.inputSkd: self.skd_is_high_dead_cross(runtime=True, high_skd=0) self.skd_is_low_golden_cross(runtime=True,low_skd=100) + class TestStrategy(object): def __init__(self): @@ -3224,9 +3250,53 @@ class TestStrategy(object): self.minDiff = 1 self.shortSymbol = 'I' self.vtSymbol = 'I99' + + self.lineM30 = None + self.lineH1 = None self.lineH2 = None self.lineD = None + self.TMinuteInterval = 1 + + self.save_m30_bars = [] + self.save_h1_bars = [] + self.save_h2_bars = [] + self.save_d_bars = [] + + def createLineM30(self): + # 创建M30 K线 + lineM30Setting = {} + lineM30Setting['name'] = u'M30' + lineM30Setting['period'] = PERIOD_MINUTE + lineM30Setting['barTimeInterval'] = 30 + lineM30Setting['inputPreLen'] = 5 + lineM30Setting['inputMa1Len'] = 5 + lineM30Setting['inputMa2Len'] = 10 + lineM30Setting['inputMa3Len'] = 18 + lineM30Setting['inputYb'] = True + lineM30Setting['inputSkd'] = True + lineM30Setting['mode'] = CtaLineBar.TICK_MODE + lineM30Setting['minDiff'] = self.minDiff + lineM30Setting['shortSymbol'] = self.shortSymbol + self.lineM30 = CtaLineBar(self, self.onBarM30, lineM30Setting) + + def createLineH1(self): + # 创建2小时K线 + lineH2Setting = {} + lineH2Setting['name'] = u'H1' + lineH2Setting['period'] = PERIOD_HOUR + lineH2Setting['barTimeInterval'] = 1 + lineH2Setting['inputPreLen'] = 5 + lineH2Setting['inputMa1Len'] = 5 + lineH2Setting['inputMa2Len'] = 10 + lineH2Setting['inputMa3Len'] = 18 + lineH2Setting['inputYb'] = True + lineH2Setting['inputSkd'] = True + lineH2Setting['mode'] = CtaLineBar.TICK_MODE + lineH2Setting['minDiff'] = self.minDiff + lineH2Setting['shortSymbol'] = self.shortSymbol + self.lineH1 = CtaHourBar(self, self.onBarH1, lineH2Setting) + def createLineH2(self): # 创建2小时K线 lineH2Setting = {} @@ -3234,10 +3304,9 @@ class TestStrategy(object): lineH2Setting['period'] = PERIOD_HOUR lineH2Setting['barTimeInterval'] = 2 lineH2Setting['inputPreLen'] = 5 - lineH2Setting['inputMa1Len'] = 10 - lineH2Setting['inputRsi1Len'] = 7 - lineH2Setting['inputRsi2Len'] = 14 - + lineH2Setting['inputMa1Len'] = 5 + lineH2Setting['inputMa2Len'] = 10 + lineH2Setting['inputMa3Len'] = 18 lineH2Setting['inputYb'] = True lineH2Setting['inputSkd'] = True lineH2Setting['mode'] = CtaLineBar.TICK_MODE @@ -3256,6 +3325,7 @@ class TestStrategy(object): lineDaySetting['inputMa2Len'] = 10 lineDaySetting['inputMa3Len'] = 18 lineDaySetting['inputYb'] = True + lineDaySetting['inputSkd'] = True lineDaySetting['mode'] = CtaDayBar.TICK_MODE lineDaySetting['minDiff'] = self.minDiff lineDaySetting['shortSymbol'] = self.shortSymbol @@ -3264,31 +3334,152 @@ class TestStrategy(object): def onBar(self, bar): #print(u'tradingDay:{},dt:{},o:{},h:{},l:{},c:{},v:{}'.format(bar.tradingDay,bar.datetime, bar.open, bar.high, bar.low, bar.close, bar.volume)) if self.lineD: - self.lineD.addBar(bar) + self.lineD.addBar(bar, bar_freq=self.TMinuteInterval) if self.lineH2: - self.lineH2.addBar(bar) + self.lineH2.addBar(bar, bar_freq=self.TMinuteInterval) + + if self.lineH1: + self.lineH1.addBar(bar, bar_freq=self.TMinuteInterval) + + if self.lineM30: + self.lineM30.addBar(bar, bar_freq=self.TMinuteInterval) + + def onBarM30(self, bar): + self.writeCtaLog(self.lineM30.displayLastBar()) + + self.save_m30_bars.append({ + 'datetime': bar.datetime, + 'open': bar.open, + 'high': bar.high, + 'low': bar.low, + 'close': bar.close, + 'turnover':0, + 'volume': bar.volume, + 'openInterest': 0, + 'ma5': self.lineM30.lineMa1[-1] if len(self.lineM30.lineMa1) > 0 else bar.close, + 'ma10': self.lineM30.lineMa2[-1] if len(self.lineM30.lineMa2) > 0 else bar.close, + 'ma18': self.lineM30.lineMa3[-1] if len(self.lineM30.lineMa3) > 0 else bar.close, + 'sk': self.lineM30.lineSK[-1] if len(self.lineM30.lineSK) > 0 else 0, + 'sd': self.lineM30.lineSD[-1] if len(self.lineM30.lineSD) > 0 else 0 + }) + + def onBarH1(self, bar): + self.writeCtaLog(self.lineH1.displayLastBar()) + + self.save_h1_bars.append({ + 'datetime': bar.datetime, + 'open': bar.open, + 'high': bar.high, + 'low': bar.low, + 'close': bar.close, + 'turnover':0, + 'volume': bar.volume, + 'openInterest': 0, + 'ma5': self.lineH1.lineMa1[-1] if len(self.lineH1.lineMa1) > 0 else bar.close, + 'ma10': self.lineH1.lineMa2[-1] if len(self.lineH1.lineMa2) > 0 else bar.close, + 'ma18': self.lineH1.lineMa3[-1] if len(self.lineH1.lineMa3) > 0 else bar.close, + 'sk': self.lineH1.lineSK[-1] if len(self.lineH1.lineSK) > 0 else 0, + 'sd': self.lineH1.lineSD[-1] if len(self.lineH1.lineSD) > 0 else 0 + }) def onBarH2(self, bar): self.writeCtaLog(self.lineH2.displayLastBar()) + self.save_h2_bars.append({ + 'datetime': bar.datetime, + 'open': bar.open, + 'high': bar.high, + 'low': bar.low, + 'close': bar.close, + 'turnover':0, + 'volume': bar.volume, + 'openInterest': 0, + 'ma5': self.lineH2.lineMa1[-1] if len(self.lineH2.lineMa1) > 0 else bar.close, + 'ma10': self.lineH2.lineMa2[-1] if len(self.lineH2.lineMa2) > 0 else bar.close, + 'ma18': self.lineH2.lineMa3[-1] if len(self.lineH2.lineMa3) > 0 else bar.close, + 'sk': self.lineH2.lineSK[-1] if len(self.lineH2.lineSK) > 0 else 0, + 'sd': self.lineH2.lineSD[-1] if len(self.lineH2.lineSD) > 0 else 0 + }) + def onBarD(self, bar): self.writeCtaLog(self.lineD.displayLastBar()) - + self.save_d_bars.append({ + 'datetime': bar.datetime, + 'open': bar.open, + 'high': bar.high, + 'low': bar.low, + 'close': bar.close, + 'turnover': 0, + 'volume': bar.volume, + 'openInterest': 0, + 'ma5': self.lineD.lineMa1[-1] if len(self.lineD.lineMa1) > 0 else bar.close, + 'ma10': self.lineD.lineMa2[-1] if len(self.lineD.lineMa2) > 0 else bar.close, + 'ma18': self.lineD.lineMa3[-1] if len(self.lineD.lineMa3) > 0 else bar.close, + 'sk': self.lineD.lineSK[-1] if len(self.lineD.lineSK) > 0 else 0, + 'sd': self.lineD.lineSD[-1] if len(self.lineD.lineSD) > 0 else 0 + }) def onTick(self, tick): print(u'{0},{1},ap:{2},av:{3},bp:{4},bv:{5}'.format(tick.datetime, tick.lastPrice, tick.askPrice1, tick.askVolume1, tick.bidPrice1, tick.bidVolume1)) def writeCtaLog(self, content): print(content) + def saveData(self): + + if len(self.save_m30_bars) > 0: + outputFile = '{}_m30.csv'.format(self.vtSymbol) + with open(outputFile, 'w', encoding='utf8', newline='') as f: + fieldnames = ['datetime', 'open', 'price', 'high','low','close','turnover','volume','openInterest','ma5','ma10','ma18','sk','sd'] + writer = csv.DictWriter(f=f, fieldnames=fieldnames, dialect='excel') + writer.writeheader() + for row in self.save_m30_bars: + writer.writerow(row) + + if len(self.save_h1_bars) > 0: + outputFile = '{}_h1.csv'.format(self.vtSymbol) + with open(outputFile, 'w', encoding='utf8', newline='') as f: + fieldnames = ['datetime', 'open', 'price', 'high','low','close','turnover','volume','openInterest','ma5','ma10','ma18','sk','sd'] + writer = csv.DictWriter(f=f, fieldnames=fieldnames, dialect='excel') + writer.writeheader() + for row in self.save_h1_bars: + writer.writerow(row) + + if len(self.save_h2_bars) > 0: + outputFile = '{}_h2.csv'.format(self.vtSymbol) + with open(outputFile, 'w', encoding='utf8', newline='') as f: + fieldnames = ['datetime', 'open', 'price', 'high','low','close','turnover','volume','openInterest','ma5','ma10','ma18','sk','sd'] + writer = csv.DictWriter(f=f, fieldnames=fieldnames, dialect='excel') + writer.writeheader() + for row in self.save_h2_bars: + writer.writerow(row) + + if len(self.save_d_bars) > 0: + outputFile = '{}_d.csv'.format(self.vtSymbol) + with open(outputFile, 'w', encoding='utf8', newline='') as f: + fieldnames = ['datetime', 'open', 'price', 'high','low','close','turnover','volume','openInterest','ma5','ma10','ma18','sk','sd'] + writer = csv.DictWriter(f=f, fieldnames=fieldnames, dialect='excel') + writer.writeheader() + for row in self.save_d_bars: + writer.writerow(row) + if __name__ == '__main__': t = TestStrategy() + t.minDiff = 1 + t.shortSymbol = 'J' + t.vtSymbol = 'J99' + # 创建M30线 + t.createLineM30() + + # 回测1小时线 + t.createLineH1() + # 回测2小时线 t.createLineH2() # 回测日线 - #t.createLineD() + t.createLineD() - filename = 'cache/I99_20141201_20171231_1m.csv' + filename = 'cache/{}_20141201_20171231_1m.csv'.format(t.vtSymbol) barTimeInterval = 60 # 60秒 minDiff = 0.5 #回测数据的最小跳动 @@ -3350,4 +3541,6 @@ if __name__ == '__main__': except Exception as ex: t.writeCtaLog(u'{0}:{1}'.format(Exception, ex)) traceback.print_exc() - break \ No newline at end of file + break + + t.saveData() \ No newline at end of file diff --git a/vnpy/trader/app/ctaStrategy/ctaPolicy.py b/vnpy/trader/app/ctaStrategy/ctaPolicy.py index 064c1aca..30283316 100644 --- a/vnpy/trader/app/ctaStrategy/ctaPolicy.py +++ b/vnpy/trader/app/ctaStrategy/ctaPolicy.py @@ -1,13 +1,174 @@ # encoding: UTF-8 +import os +import sys +import json +from datetime import datetime +from collections import OrderedDict + from vnpy.trader.app.ctaStrategy.ctaBase import * from vnpy.trader.vtConstant import * -DEBUGCTALOG = True - class CtaPolicy(object): - """CTA的策略规则类 + """ + 策略的持久化Policy + """ + + def __init__(self, strategy=None): + """ + 构造 + :param strategy: + """ + self.strategy = strategy + + self.create_time = None + self.save_time = None + + def writeCtaLog(self, log): + """ + 写入日志 + :param log: + :return: + """ + if self.strategy: + self.strategy.writeCtaLog(log) + + def writeCtaError(self, log): + """ + 写入错误日志 + :param log: + :return: + """ + if self.strategy: + self.strategy.writeCtaError(log) + + def toJson(self): + """ + 将数据转换成dict + datetime =》 string + object =》 string + :return: + """ + j = OrderedDict() + j['create_time'] = self.create_time.strftime('%Y-%m-%d %H:%M:%S') if self.create_time is not None else EMPTY_STRING + j['save_time'] = self.save_time.strftime('%Y-%m-%d %H:%M:%S') if self.save_time is not None else EMPTY_STRING + + return j + + def fromJson(self, json_data): + """ + 将数据从json_data中恢复 + :param json_data: + :return: + """ + self.writeCtaLog(u'将数据从json_data中恢复') + + if 'create_time' in json_data: + try: + self.create_time = datetime.strptime(json_data['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() + + if 'save_time' in json_data: + try: + self.save_time = datetime.strptime(json_data['save_time'], '%Y-%m-%d %H:%M:%S') + except Exception as ex: + self.writeCtaError(u'解释save_time异常:{}'.format(str(ex))) + self.save_time = datetime.now() + + def load(self): + """ + 从持久化文件中获取 + :return: + """ + json_file = os.path.abspath(os.path.join(self.get_data_folder(), u'{}_Policy.json'.format(self.strategy.name))) + + json_data = {} + if os.path.exists(json_file): + try: + with open(json_file, 'r', encoding='utf8') as f: + # 解析json文件 + json_data = json.load(f) + except IOError as ex: + self.writeCtaError(u'读取Policy文件{}出错,ex:{}'.format(json_file,str(ex))) + json_data = {} + + # 从持久化文件恢复数据 + self.fromJson(json_data) + + def save(self): + """ + 保存至持久化文件 + :return: + """ + json_file = os.path.abspath( + os.path.join(self.get_data_folder(), u'{}_Policy.json'.format(self.strategy.name))) + + try: + json_data = self.toJson() + + if self.strategy and self.strategy.backtesting: + json_data['save_time'] = self.strategy.curDateTime.strftime('%Y-%m-%d %H:%M:%S') + else: + json_data['save_time'] = datetime.now().strftime('%Y-%m-%d %H:%M:%S') + + with open(json_file, 'w') as f: + data = json.dumps(json_data, indent=4) + f.write(data) + #self.writeCtaLog(u'写入Policy文件:{}成功'.format(json_file)) + except IOError as ex: + self.writeCtaError(u'写入Policy文件{}出错,ex:{}'.format(json_file, str(ex))) + + def export_history(self): + """ + 导出历史 + :return: + """ + export_dir = os.path.abspath(os.path.join( + self.get_data_folder(), + 'export_csv', + '{}'.format(self.strategy.curDateTime.strftime('%Y%m%d')))) + if not os.path.exists(export_dir): + try: + os.mkdir(export_dir) + except Exception as ex: + self.writeCtaError(u'创建Policy切片目录{}出错,ex:{}'.format(export_dir, str(ex))) + return + + json_file = os.path.abspath(os.path.join( + export_dir, + u'{}_Policy_{}.json'.format(self.strategy.name, self.strategy.curDateTime.strftime('%Y%m%d_%H%M')))) + + try: + json_data = self.toJson() + + if self.strategy and self.strategy.backtesting: + json_data['save_time'] = self.strategy.curDateTime.strftime('%Y-%m-%d %H:%M:%S') + else: + json_data['save_time'] = datetime.now().strftime('%Y-%m-%d %H:%M:%S') + + with open(json_file, 'w') as f: + data = json.dumps(json_data, indent=4) + f.write(data) + # self.writeCtaLog(u'写入Policy文件:{}成功'.format(json_file)) + except IOError as ex: + self.writeCtaError(u'写入Policy文件{}出错,ex:{}'.format(json_file, str(ex))) + + def get_data_folder(self): + """获取数据目录""" + # 工作目录 + currentFolder = os.path.abspath(os.path.join(os.getcwd(), u'data')) + if os.path.isdir(currentFolder): + # 如果工作目录下,存在data子目录,就使用data子目录 + return currentFolder + else: + # 否则,使用缺省保存目录 vnpy/trader/app/ctaStrategy/data + return os.path.abspath(os.path.join(os.path.dirname(__file__), u'data')) + +class RenkoPolicy(CtaPolicy): + """Renko CTA的策略规则类 包括: 1、风险评估 2、最低止损位置 @@ -19,10 +180,12 @@ class CtaPolicy(object): R2,Small Range/Renko """ - def __init__(self, r1Period=EMPTY_STRING, r2Period=EMPTY_STRING): + def __init__(self, strategy): - self.openR1Period = r1Period # 开仓周期Mode - self.openR2Period = r2Period # 开仓周期Mode + super(RenkoPolicy, self).__init__(strategy) + + self.openR1Period = None # 开仓周期Mode + self.openR2Period = None # 开仓周期Mode self.entryPrice = EMPTY_FLOAT # 开仓价格 self.lastPeriodBasePrice = EMPTY_FLOAT # 上一个周期内的最近高价/低价(看R2 Rsi的高点和低点) @@ -62,5 +225,514 @@ class CtaPolicy(object): self.reducePosOnEmaRtn = False + def set_r1Period(self,r1Period): + self.openR1Period = r1Period + + def set_r2Period(self,r2Period): + self.openR2Period = r2Period + +class TurtlePolicy(CtaPolicy): + """海龟策略事务""" + + def __init__(self, strategy): + super(TurtlePolicy, self).__init__(strategy) + + 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 # 低于首次开仓价的补仓价格 + self.add_pos_count_under_first_price = 0 # 低于首次开仓价的补仓次数 + self.tns_direction = None # 事务方向 DIRECTION_LONG 向上/ DIRECTION_SHORT向下 + self.tns_count = 0 # 当前事务开启后多少个bar,>0,做多方向,<0,做空方向 + self.max_pos = 0 # 事务中最大仓位 + self.tns_has_opened = False + self.tns_open_date = '' # 事务开启的交易日 + self.last_risk_level = 0 # 上一次风控评估的级别 + + def toJson(self): + """ + 将数据转换成dict + :return: + """ + j = OrderedDict() + j['create_time'] = self.create_time.strftime( + '%Y-%m-%d %H:%M:%S') if self.create_time is not None else EMPTY_STRING + j['save_time'] = self.save_time.strftime('%Y-%m-%d %H:%M:%S') if self.save_time is not None else EMPTY_STRING + 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['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['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 + j['tns_direction'] = self.tns_direction if self.tns_direction is not None else EMPTY_STRING + j['tns_count'] = self.tns_count if self.tns_count is not None else 0 + + j['tns_has_opened'] = self.tns_has_opened + j['last_risk_level'] = self.last_risk_level + + return j + + def fromJson(self, json_data): + """ + 将dict转化为属性 + :param json_data: + :return: + """ + if 'create_time' in json_data: + try: + self.create_time = datetime.strptime(json_data['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() + + if 'save_time' in json_data: + try: + self.save_time = datetime.strptime(json_data['save_time'], '%Y-%m-%d %H:%M:%S') + except Exception as ex: + self.writeCtaError(u'解释save_time异常:{}'.format(str(ex))) + self.save_time = datetime.now() + + if 'tns_open_price' in json_data: + try: + self.tns_open_price = json_data['tns_open_price'] + except Exception as ex: + self.writeCtaError(u'解释tns_open_price异常:{}'.format(str(ex))) + self.tns_open_price = 0 + + if 'tns_open_date' in json_data: + try: + self.tns_open_date = json_data['tns_open_date'] + except Exception as ex: + self.writeCtaError(u'解释tns_open_date异常:{}'.format(str(ex))) + self.tns_open_date = '' + + if 'last_under_open_price' in json_data: + try: + self.last_under_open_price = json_data['last_under_open_price'] + except Exception as ex: + self.writeCtaError(u'解释last_under_open_price异常:{}'.format(str(ex))) + self.last_under_open_price = 0 + + if 'last_open_price' in json_data: + try: + self.last_open_price = json_data['last_open_price'] + except Exception as ex: + self.writeCtaError(u'解释last_open_price异常:{}'.format(str(ex))) + self.last_open_price = 0 + + if 'stop_price' in json_data: + try: + self.stop_price = json_data['stop_price'] + except Exception as ex: + self.writeCtaError(u'解释stop_price异常:{}'.format(str(ex))) + self.stop_price = 0 + + if 'high_price_in_long' in json_data: + try: + self.high_price_in_long = json_data['high_price_in_long'] + except Exception as ex: + self.writeCtaError(u'解释high_price_in_long异常:{}'.format(str(ex))) + self.high_price_in_long = 0 + + if 'low_price_in_short' in json_data: + try: + self.low_price_in_short = json_data['low_price_in_short'] + except Exception as ex: + self.writeCtaError(u'解释low_price_in_short异常:{}'.format(str(ex))) + self.low_price_in_short = 0 + + if 'max_pos' in json_data: + try: + self.max_pos = json_data['max_pos'] + except Exception as ex: + self.writeCtaError(u'解释max_pos异常:{}'.format(str(ex))) + self.max_pos = 0 + + if 'add_pos_count_under_first_price' in json_data: + try: + self.add_pos_count_under_first_price = json_data['add_pos_count_under_first_price'] + except Exception as ex: + self.writeCtaError(u'解释add_pos_count_under_first_price异常:{}'.format(str(ex))) + self.add_pos_count_under_first_price = 0 + + if 'tns_direction' in json_data: + try: + self.tns_direction = json_data['tns_direction'] + except Exception as ex: + self.writeCtaError(u'解释tns_direction异常:{}'.format(str(ex))) + self.tns_direction = EMPTY_STRING + + if 'tns_count' in json_data: + try: + self.tns_count = json_data['tns_count'] + except Exception as ex: + self.writeCtaError(u'解释tns_count异常:{}'.format(str(ex))) + self.tns_count = EMPTY_STRING + + if 'tns_has_opened' in json_data: + try: + self.tns_has_opened = json_data['tns_has_opened'] + except Exception as ex: + self.writeCtaError(u'解释tns_has_opened异常:{}'.format(str(ex))) + self.tns_has_opened = False + + if 'last_risk_level' in json_data: + try: + self.last_risk_level = json_data['last_risk_level'] + except Exception as ex: + self.writeCtaError(u'解释last_risk_level异常:{}'.format(str(ex))) + self.last_risk_level = 0 + + + def clean(self): + """ + 清空数据 + :return: + """ + self.writeCtaLog(u'清空policy数据') + self.tns_open_price = 0 + self.tns_open_date = '' + self.last_open_price = 0 + self.last_under_open_price = 0 + self.stop_price = 0 + self.high_price_in_long = 0 + self.low_price_in_short = 0 + self.max_pos = 0 + self.add_pos_count_under_first_price = 0 + self.tns_direction = None + self.tns_has_opened = False + self.last_risk_level = 0 + self.tns_count = 0 + +class TrendPolicy(CtaPolicy): + """ + 趋势策略规则 + 每次趋势交易,看作一个事务 + """ + + def __init__(self, strategy): + super(TrendPolicy, self).__init__(strategy) + + 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 # 低于首次开仓价的补仓价格 + self.add_pos_count_under_first_price = 0 # 低于首次开仓价的补仓次数 + self.tns_direction = None # 日线方向 DIRECTION_LONG 向上/ DIRECTION_SHORT向下 + self.tns_count = 0 # 日线事务,>0,做多方向,<0,做空方向 + self.max_pos = 0 # 事务中最大仓位 + self.pos_to_add = [] # 开仓/加仓得仓位数量分布列表 + self.pos_reduced = {} # 减仓记录 + self.last_reduce_price = 0 # 最近一次减仓记录 + + self.tns_has_opened = False + self.tns_open_date = '' # 事务开启的交易日 + self.last_balance_level = 0 # 上一次平衡的级别 + self.last_risk = EMPTY_STRING # 上一次risk评估类别 + self.cur_risk = EMPTY_STRING # 当前risk评估类别 + + # 加仓记录 + self.dtosc_add_pos = {} # DTOSC 加仓 + + def toJson(self): + """ + 将数据转换成dict + :return: + """ + j = OrderedDict() + j['create_time'] = self.create_time.strftime('%Y-%m-%d %H:%M:%S') if self.create_time is not None else EMPTY_STRING + j['save_time'] = self.save_time.strftime('%Y-%m-%d %H:%M:%S') if self.save_time is not None else EMPTY_STRING + 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['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['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 + j['tns_direction'] = self.tns_direction if self.tns_direction is not None else EMPTY_STRING + j['tns_count'] = self.tns_count if self.tns_count is not None else 0 + j['pos_to_add'] = self.pos_to_add + j['pos_reduced'] = self.pos_reduced + j['tns_has_opened'] = self.tns_has_opened + j['last_balance_level'] = self.last_balance_level + j['last_reduce_price'] = self.last_reduce_price + j['dtosc_add_pos'] = self.dtosc_add_pos + return j + + def fromJson(self, json_data): + """ + 将dict转化为属性 + :param json_data: + :return: + """ + if 'create_time' in json_data: + try: + self.create_time = datetime.strptime(json_data['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() + + if 'save_time' in json_data: + try: + self.save_time = datetime.strptime(json_data['save_time'], '%Y-%m-%d %H:%M:%S') + except Exception as ex: + self.writeCtaError(u'解释save_time异常:{}'.format(str(ex))) + self.save_time = datetime.now() + + if 'tns_open_price' in json_data: + try: + self.tns_open_price = json_data['tns_open_price'] + except Exception as ex: + self.writeCtaError(u'解释tns_open_price异常:{}'.format(str(ex))) + self.tns_open_price = 0 + + if 'tns_open_date' in json_data: + try: + self.tns_open_date = json_data['tns_open_date'] + except Exception as ex: + self.writeCtaError(u'解释tns_open_date异常:{}'.format(str(ex))) + self.tns_open_date = '' + + if 'last_under_open_price' in json_data: + try: + self.last_under_open_price = json_data['last_under_open_price'] + except Exception as ex: + self.writeCtaError(u'解释last_under_open_price异常:{}'.format(str(ex))) + self.last_under_open_price = 0 + + if 'last_open_price' in json_data: + try: + self.last_open_price = json_data['last_open_price'] + except Exception as ex: + self.writeCtaError(u'解释last_open_price异常:{}'.format(str(ex))) + self.last_open_price = 0 + + if 'last_reduce_price' in json_data: + try: + self.last_reduce_price = json_data['last_reduce_price'] + except Exception as ex: + self.writeCtaError(u'解释last_reduce_price异常:{}'.format(str(ex))) + self.last_reduce_price = 0 + + if 'stop_price' in json_data: + try: + self.stop_price = json_data['stop_price'] + except Exception as ex: + self.writeCtaError(u'解释stop_price异常:{}'.format(str(ex))) + self.stop_price = 0 + + if 'high_price_in_long' in json_data: + try: + self.high_price_in_long = json_data['high_price_in_long'] + except Exception as ex: + self.writeCtaError(u'解释high_price_in_long异常:{}'.format(str(ex))) + self.high_price_in_long = 0 + + if 'low_price_in_short' in json_data: + try: + self.low_price_in_short = json_data['low_price_in_short'] + except Exception as ex: + self.writeCtaError(u'解释low_price_in_short异常:{}'.format(str(ex))) + self.low_price_in_short = 0 + + if 'max_pos' in json_data: + try: + self.max_pos = json_data['max_pos'] + except Exception as ex: + self.writeCtaError(u'解释max_pos异常:{}'.format(str(ex))) + self.max_pos = 0 + + if 'add_pos_count_under_first_price' in json_data: + try: + self.add_pos_count_under_first_price = json_data['add_pos_count_under_first_price'] + except Exception as ex: + self.writeCtaError(u'解释add_pos_count_under_first_price异常:{}'.format(str(ex))) + self.add_pos_count_under_first_price = 0 + + if 'tns_direction' in json_data: + try: + self.tns_direction = json_data['tns_direction'] + except Exception as ex: + self.writeCtaError(u'解释tns_direction异常:{}'.format(str(ex))) + self.tns_direction = EMPTY_STRING + + if 'tns_count' in json_data: + try: + self.tns_count = json_data['tns_count'] + except Exception as ex: + self.writeCtaError(u'解释tns_count异常:{}'.format(str(ex))) + self.tns_count = EMPTY_STRING + + if 'pos_to_add' in json_data: + try: + self.pos_to_add = json_data['pos_to_add'] + except Exception as ex: + self.writeCtaError(u'解释pos_to_add异常:{}'.format(str(ex))) + self.pos_to_add = [] + + if 'pos_reduced' in json_data: + try: + self.pos_reduced = json_data['pos_reduced'] + except Exception as ex: + self.writeCtaError(u'解释pos_reduced异常:{}'.format(str(ex))) + self.pos_reduced = {} + + if 'tns_has_opened' in json_data: + try: + self.tns_has_opened = json_data['tns_has_opened'] + except Exception as ex: + self.writeCtaError(u'解释tns_has_opened异常:{}'.format(str(ex))) + self.tns_has_opened = False + + if 'last_balance_level' in json_data: + try: + self.last_balance_level = json_data['last_balance_level'] + except Exception as ex: + self.writeCtaError(u'解释last_balance_level异常:{}'.format(str(ex))) + self.last_balance_level = 0 + + if 'dtosc_add_pos' in json_data: + try: + self.dtosc_add_pos = json_data['dtosc_add_pos'] + except Exception as ex: + self.writeCtaError(u'解释dtosc_add_pos异常:{}'.format(str(ex))) + self.dtosc_add_pos = {} + + def clean(self): + """ + 清空数据 + :return: + """ + self.writeCtaLog(u'清空policy数据') + self.tns_open_price = 0 + self.tns_open_date = '' + self.last_open_price = 0 + self.last_under_open_price = 0 + self.stop_price = 0 + self.high_price_in_long = 0 + self.low_price_in_short = 0 + self.max_pos = 0 + self.add_pos_count_under_first_price = 0 + self.tns_direction = None + self.pos_to_add = [] + self.pos_reduced = {} + self.last_reduce_price = 0 + self.tns_has_opened = False + self.last_balance_level = 0 + self.dtosc_add_pos = {} + + def get_last_reduced_pos(self, reduce_type): + """ + 获取最后得减仓数量 + :param reduce_type: + :return: + """ + reduce_list = self.pos_reduced.get(reduce_type,[]) + if len(reduce_list): + last_pos = reduce_list.pop(-1) + return last_pos + return 0 + + def get_all_reduced_pos(self, reduce_type): + """ + 获取该类型得所有减仓数量 + :param reduce_type: + :return: + """ + reduce_list = self.pos_reduced.get(reduce_type, []) + return sum(reduce_list) + + def add_reduced_pos(self, reduce_type, reduce_volume): + """ + 添加减仓数量 + :param reduce_type: + :param reduce_volume: + :return: + """ + reduce_list = self.pos_reduced.get(reduce_type, []) + reduce_list.append(reduce_volume) + self.pos_reduced[reduce_type] = reduce_list + + def calculatePosToAdd(self, total_pos, add_count): + """ + 计算可供加仓得仓位 + 仓位 M% + 加仓次数限定为:N次(N不超过16次) + 则平均每次加仓手数为M%/N + 那么第一次加仓手数为M%/N*(1+(N/2)/10)  + 第二次加仓手数为M%/N*(1+(N/2-1)/10) + …… + 第N/2次加仓手数为M%/N*1.1 + 第N/2+1次加仓手数为M%/N*0.9 + …… + 第N次加仓手数为M%/N*(1-(N/2)/10) + :param direction: + :return: + """ + max_pos = total_pos + + if len(self.pos_to_add) > 0: + self.writeCtaLog(u'加仓参考清单已存在,不重新分配:{}'.format(self.pos_to_add)) + return + + avg_pos = max_pos / add_count + + for i in range(0, add_count): + if max_pos <= 0: + self.writeCtaLog(u'分配完毕') + break + add_pos = avg_pos * (1+(add_count/2-i)/10) + add_pos = int(add_pos) if add_pos >= 1 else 1 + if max_pos <= add_pos: + add_pos = max_pos + max_pos = 0 + else: + max_pos -= add_pos + + self.pos_to_add.append(add_pos) + + if max_pos > 0: + self.pos_to_add.append(max_pos) + + self.writeCtaLog(u'总数量{},计划:{}'.format(total_pos,self.pos_to_add)) + + def getPosToAdd(self, max_pos): + if len(self.pos_to_add) == 0: + if max_pos > self.max_pos: + self.writeCtaLog(u'最大允许仓位{}>现最大加仓{}'.format(max_pos,self.max_pos)) + self.pos_to_add.append(max_pos - self.max_pos) + return max_pos - self.max_pos + else: + self.writeCtaError(u'没有仓位可加') + return 0 + else: + return self.pos_to_add[0] + + def removePosToAdd(self, remove_pos): + + if len(self.pos_to_add) == 0: + self.writeCtaError(u'可加仓清单列表为空,不能删除') + return + + pos = self.pos_to_add.pop(0) + if pos > remove_pos: + self.writeCtaLog(u'不全移除,剩余:{}补充到最后'.format(pos-remove_pos)) + self.pos_to_add.append(pos - remove_pos) + else: + self.writeCtaLog(u'移除:{},剩余:{}'.format(pos,self.pos_to_add)) diff --git a/vnpy/trader/app/ctaStrategy/ctaTemplate.py b/vnpy/trader/app/ctaStrategy/ctaTemplate.py index be0bdfed..ec76c82c 100644 --- a/vnpy/trader/app/ctaStrategy/ctaTemplate.py +++ b/vnpy/trader/app/ctaStrategy/ctaTemplate.py @@ -259,20 +259,29 @@ class CtaTemplate(object): # ---------------------------------------------------------------------- def writeCtaLog(self, content): """记录CTA日志""" - content = self.name + ':' + content - self.ctaEngine.writeCtaLog(content) + try: + self.ctaEngine.writeCtaLog(content, strategy_name=self.name) + except Exception as ex: + content = self.name + ':' + content + self.ctaEngine.writeCtaLog(content) # ---------------------------------------------------------------------- def writeCtaError(self, content): """记录CTA出错日志""" - content = self.name + ':' + content - self.ctaEngine.writeCtaError(content) + try: + self.ctaEngine.writeCtaError(content, strategy_name=self.name) + except Exception as ex: + content = self.name + ':' + content + self.ctaEngine.writeCtaError(content) # ---------------------------------------------------------------------- def writeCtaWarning(self, content): """记录CTA告警日志""" - content = self.name + ':' + content - self.ctaEngine.writeCtaWarning(content) + try: + self.ctaEngine.writeCtaWarning(content, strategy_name=self.name) + except Exception as ex: + content = self.name + ':' + content + self.ctaEngine.writeCtaWarning(content) # ---------------------------------------------------------------------- def writeCtaNotification(self, content): @@ -287,10 +296,15 @@ class CtaTemplate(object): # ---------------------------------------------------------------------- def writeCtaCritical(self, content): """记录CTA系统异常日志""" - content = self.name + ':' + content + if not self.backtesting: - self.ctaEngine.writeCtaCritical(content) + try: + self.ctaEngine.writeCtaCritical(content,strategy_name=self.name) + except Exception as ex: + content = self.name + ':' + content + self.ctaEngine.writeCtaCritical(content) else: + content = self.name + ':' + content self.ctaEngine.writeCtaError(content) def sendSignal(self,direction,price, level): diff --git a/vnpy/trader/uiBasicWidget.py b/vnpy/trader/uiBasicWidget.py index 4eedf219..1d9e8548 100644 --- a/vnpy/trader/uiBasicWidget.py +++ b/vnpy/trader/uiBasicWidget.py @@ -12,6 +12,7 @@ from vnpy.trader.vtFunction import * from vnpy.trader.vtGateway import * from vnpy.trader.vtText import text as vtText from vnpy.trader.uiQt import QtWidgets, QtGui, QtCore, BASIC_FONT +from vnpy.trader.vtConstant import EXCHANGE_BINANCE,EXCHANGE_OKEX if str(platform.system()) == 'Windows': import winsound @@ -36,8 +37,9 @@ class BasicCell(QtWidgets.QTableWidgetItem): """设置内容""" if text == '0' or text == '0.0' or type(text) == type(None): self.setText('') - #elif type(text) == type(float): - # self.setText(str(text)) + elif isinstance(text, float): + f_str = str("%.8f" % text) + self.setText(floatToStr(f_str)) else: self.setText(str(text)) @@ -295,73 +297,76 @@ class BasicMonitor(QtWidgets.QTableWidget): # ---------------------------------------------------------------------- def updateData(self, data): - """将数据更新到表格中""" - # 如果允许了排序功能,则插入数据前必须关闭,否则插入新的数据会变乱 - if self.sorting: - self.setSortingEnabled(False) - - # 如果设置了dataKey,则采用存量更新模式 - if self.dataKey: - if isinstance(self.dataKey, list): - # 多个key,逐一组合 - key = '_'.join([getattr(data, item, '') for item in self.dataKey]) + try: + """将数据更新到表格中""" + # 如果允许了排序功能,则插入数据前必须关闭,否则插入新的数据会变乱 + if self.sorting: + self.setSortingEnabled(False) + + # 如果设置了dataKey,则采用存量更新模式 + if self.dataKey: + if isinstance(self.dataKey, list): + # 多个key,逐一组合 + key = '_'.join([getattr(data, item, '') for item in self.dataKey]) + else: + # 单个key + key = getattr(data, self.dataKey, None) + if key is None: + print('uiBaseWidget.updateData() error: data had not attribute {} '.format(self.dataKey)) + return + # 如果键在数据字典中不存在,则先插入新的一行,并创建对应单元格 + if key not in self.dataDict: + self.insertRow(0) + d = {} + for n, header in enumerate(self.headerList): + content = safeUnicode(data.__getattribute__(header)) + cellType = self.headerDict[header]['cellType'] + cell = cellType(content, self.mainEngine) + + if self.font: + cell.setFont(self.font) # 如果设置了特殊字体,则进行单元格设置 + + if self.saveData: # 如果设置了保存数据对象,则进行对象保存 + cell.data = data + + self.setItem(0, n, cell) + d[header] = cell + self.dataDict[key] = d + # 否则如果已经存在,则直接更新相关单元格 + else: + d = self.dataDict[key] + for header in self.headerList: + content = safeUnicode(data.__getattribute__(header)) + cell = d[header] + cell.setContent(content) + + if self.saveData: # 如果设置了保存数据对象,则进行对象保存 + cell.data = data + # 否则采用增量更新模式 else: - # 单个key - key = getattr(data, self.dataKey, None) - if key is None: - print('uiBaseWidget.updateData() error: data had not attribute {} '.format(self.dataKey)) - return - # 如果键在数据字典中不存在,则先插入新的一行,并创建对应单元格 - if key not in self.dataDict: - self.insertRow(0) - d = {} + self.insertRow(0) for n, header in enumerate(self.headerList): content = safeUnicode(data.__getattribute__(header)) cellType = self.headerDict[header]['cellType'] cell = cellType(content, self.mainEngine) - + if self.font: - cell.setFont(self.font) # 如果设置了特殊字体,则进行单元格设置 - - if self.saveData: # 如果设置了保存数据对象,则进行对象保存 + cell.setFont(self.font) + + if self.saveData: cell.data = data - + self.setItem(0, n, cell) - d[header] = cell - self.dataDict[key] = d - # 否则如果已经存在,则直接更新相关单元格 - else: - d = self.dataDict[key] - for header in self.headerList: - content = safeUnicode(data.__getattribute__(header)) - cell = d[header] - cell.setContent(content) - - if self.saveData: # 如果设置了保存数据对象,则进行对象保存 - cell.data = data - # 否则采用增量更新模式 - else: - self.insertRow(0) - for n, header in enumerate(self.headerList): - content = safeUnicode(data.__getattribute__(header)) - cellType = self.headerDict[header]['cellType'] - cell = cellType(content, self.mainEngine) - - if self.font: - cell.setFont(self.font) - if self.saveData: - cell.data = data + # 调整列宽 + self.resizeColumns() + + # 重新打开排序 + if self.sorting: + self.setSortingEnabled(True) + except Exception as ex: + print('update data exception:{},{}'.format(str(ex),traceback.format_exc()),file=sys.stderr) - self.setItem(0, n, cell) - - # 调整列宽 - self.resizeColumns() - - # 重新打开排序 - if self.sorting: - self.setSortingEnabled(True) - #---------------------------------------------------------------------- def resizeColumns(self): """调整各列的大小""" @@ -745,7 +750,8 @@ class TradingWidget(QtWidgets.QFrame): EXCHANGE_NYMEX, EXCHANGE_GLOBEX, EXCHANGE_IDEALPRO, - EXCHANGE_OKEX] + EXCHANGE_OKEX, + EXCHANGE_BINANCE] currencyList = [CURRENCY_NONE, CURRENCY_CNY, @@ -815,7 +821,7 @@ class TradingWidget(QtWidgets.QFrame): self.spinVolume = QtWidgets.QDoubleSpinBox() self.spinVolume.setMinimum(0) - self.spinVolume.setDecimals(4) + #self.spinVolume.setDecimals(8) self.spinVolume.setMaximum(sys.maxsize) self.comboPriceType = QtWidgets.QComboBox() @@ -991,7 +997,7 @@ class TradingWidget(QtWidgets.QFrame): # 清空价格数量 self.spinPrice.setValue(0) - self.spinVolume.setValue(0) + #self.spinVolume.setValue(0) # 清空行情显示 self.labelBidPrice1.setText('') @@ -1045,7 +1051,13 @@ class TradingWidget(QtWidgets.QFrame): if tick.vtSymbol == self.symbol: if not self.checkFixed.isChecked(): + if isinstance(tick.lastPrice, float): + p = decimal.Decimal(str(tick.lastPrice)) + decimal_len = abs(p.as_tuple().exponent) + if decimal_len != self.spinPrice.decimals(): + self.spinPrice.setDecimals(decimal_len) self.spinPrice.setValue(tick.lastPrice) + self.labelBidPrice1.setText('{}'.format(tick.bidPrice1)) self.labelAskPrice1.setText('{}'.format(tick.askPrice1)) self.labelBidVolume1.setText('{}'.format(tick.bidVolume1)) @@ -1089,6 +1101,7 @@ class TradingWidget(QtWidgets.QFrame): def sendOrder(self): """发单""" symbol = str(self.lineSymbol.text()) + vtSymbol = symbol exchange = self.comboExchange.currentText() currency = self.comboCurrency.currentText() productClass = self.comboProductClass.currentText() @@ -1109,6 +1122,7 @@ class TradingWidget(QtWidgets.QFrame): req = VtOrderReq() req.symbol = symbol + req.vtSymbol = vtSymbol req.exchange = exchange req.price = self.spinPrice.value() req.volume = self.spinVolume.value() @@ -1157,6 +1171,17 @@ class TradingWidget(QtWidgets.QFrame): pos = cell.data symbol = pos.symbol + symbol_split_list = symbol.split('.') + if len(symbol_split_list)==2: + exchange_name = symbol_split_list[-1] + if exchange_name in [EXCHANGE_OKEX,EXCHANGE_BINANCE]: + symbol = symbol_split_list[0] + + symbol_pair_list = symbol.split('_') + if len(symbol_pair_list) ==1: + if symbol.lower() == 'usdt': + return + symbol = symbol_pair_list[0] + '_' + 'usdt'+'.'+symbol_split_list[-1] # 更新交易组件的显示合约 self.lineSymbol.setText(symbol) self.updateSymbol() @@ -1164,6 +1189,14 @@ class TradingWidget(QtWidgets.QFrame): # 自动填写信息 self.comboPriceType.setCurrentIndex(self.priceTypeList.index(PRICETYPE_LIMITPRICE)) self.comboOffset.setCurrentIndex(self.offsetList.index(OFFSET_CLOSE)) + if isinstance(pos.position, float): + p = decimal.Decimal(str(pos.position)) + decimal_len = abs(p.as_tuple().exponent) + if decimal_len > self.spinVolume.decimals(): + self.spinVolume.setDecimals(decimal_len) + elif isinstance(pos.position, int): + self.spinVolume.setDecimals(0) + self.spinVolume.setValue(pos.position) if pos.direction == DIRECTION_LONG or pos.direction == DIRECTION_NET: diff --git a/vnpy/trader/uiKLine/.gitignore b/vnpy/trader/uiKLine/.gitignore new file mode 100644 index 00000000..7bbc71c0 --- /dev/null +++ b/vnpy/trader/uiKLine/.gitignore @@ -0,0 +1,101 @@ +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +env/ +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +*.egg-info/ +.installed.cfg +*.egg + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +.hypothesis/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# pyenv +.python-version + +# celery beat schedule file +celerybeat-schedule + +# SageMath parsed files +*.sage.py + +# dotenv +.env + +# virtualenv +.venv +venv/ +ENV/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ diff --git a/vnpy/trader/uiKLine/css.qss b/vnpy/trader/uiKLine/css.qss new file mode 100644 index 00000000..acb366d8 --- /dev/null +++ b/vnpy/trader/uiKLine/css.qss @@ -0,0 +1,1342 @@ +/* + * The MIT License (MIT) + * + * Copyright (c) <2013-2014> + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +QToolTip +{ + border: 1px solid #76797C; + background-color: rgb(90, 102, 117);; + color: white; + padding: 5px; + opacity: 200; +} + +QWidget +{ + color: #eff0f1; + background-color: #31363b; + selection-background-color:#3daee9; + selection-color: #eff0f1; + background-clip: border; + border-image: none; + border: 0px transparent black; + outline: 0; +} + +QWidget:item:hover +{ + background-color: #3daee9; + color: #eff0f1; +} + +QWidget:item:selected +{ + background-color: #3daee9; +} + +QCheckBox +{ + spacing: 5px; + outline: none; + color: #eff0f1; + margin-bottom: 2px; +} + +QCheckBox:disabled +{ + color: #76797C; +} + +QCheckBox::indicator, +QGroupBox::indicator +{ + width: 18px; + height: 18px; +} +QGroupBox::indicator +{ + margin-left: 2px; +} + +QCheckBox::indicator:unchecked +{ + image: url(:/qss_icons/rc/checkbox_unchecked.png); +} + +QCheckBox::indicator:unchecked:hover, +QCheckBox::indicator:unchecked:focus, +QCheckBox::indicator:unchecked:pressed, +QGroupBox::indicator:unchecked:hover, +QGroupBox::indicator:unchecked:focus, +QGroupBox::indicator:unchecked:pressed +{ + border: none; + image: url(:/qss_icons/rc/checkbox_unchecked_focus.png); +} + +QCheckBox::indicator:checked +{ + image: url(:/qss_icons/rc/checkbox_checked.png); +} + +QCheckBox::indicator:checked:hover, +QCheckBox::indicator:checked:focus, +QCheckBox::indicator:checked:pressed, +QGroupBox::indicator:checked:hover, +QGroupBox::indicator:checked:focus, +QGroupBox::indicator:checked:pressed +{ + border: none; + image: url(:/qss_icons/rc/checkbox_checked_focus.png); +} + + +QCheckBox::indicator:indeterminate +{ + image: url(:/qss_icons/rc/checkbox_indeterminate.png); +} + +QCheckBox::indicator:indeterminate:focus, +QCheckBox::indicator:indeterminate:hover, +QCheckBox::indicator:indeterminate:pressed +{ + image: url(:/qss_icons/rc/checkbox_indeterminate_focus.png); +} + +QCheckBox::indicator:checked:disabled, +QGroupBox::indicator:checked:disabled +{ + image: url(:/qss_icons/rc/checkbox_checked_disabled.png); +} + +QCheckBox::indicator:unchecked:disabled, +QGroupBox::indicator:unchecked:disabled +{ + image: url(:/qss_icons/rc/checkbox_unchecked_disabled.png); +} + +QRadioButton +{ + spacing: 5px; + outline: none; + color: #eff0f1; + margin-bottom: 2px; +} + +QRadioButton:disabled +{ + color: #76797C; +} +QRadioButton::indicator +{ + width: 21px; + height: 21px; +} + +QRadioButton::indicator:unchecked +{ + image: url(:/qss_icons/rc/radio_unchecked.png); +} + + +QRadioButton::indicator:unchecked:hover, +QRadioButton::indicator:unchecked:focus, +QRadioButton::indicator:unchecked:pressed +{ + border: none; + outline: none; + image: url(:/qss_icons/rc/radio_unchecked_focus.png); +} + +QRadioButton::indicator:checked +{ + border: none; + outline: none; + image: url(:/qss_icons/rc/radio_checked.png); +} + +QRadioButton::indicator:checked:hover, +QRadioButton::indicator:checked:focus, +QRadioButton::indicator:checked:pressed +{ + border: none; + outline: none; + image: url(:/qss_icons/rc/radio_checked_focus.png); +} + +QRadioButton::indicator:checked:disabled +{ + outline: none; + image: url(:/qss_icons/rc/radio_checked_disabled.png); +} + +QRadioButton::indicator:unchecked:disabled +{ + image: url(:/qss_icons/rc/radio_unchecked_disabled.png); +} + + +QMenuBar +{ + background-color: #31363b; + color: #eff0f1; +} + +QMenuBar::item +{ + background: transparent; +} + +QMenuBar::item:selected +{ + background: transparent; + border: 1px solid #76797C; +} + +QMenuBar::item:pressed +{ + border: 1px solid #76797C; + background-color: #3daee9; + color: #eff0f1; + margin-bottom:-1px; + padding-bottom:1px; +} + +QMenu +{ + border: 1px solid #76797C; + color: #eff0f1; + margin: 2px; +} + +QMenu::icon +{ + margin: 5px; +} + +QMenu::item +{ + padding: 5px 30px 5px 30px; + margin-left: 5px; + border: 1px solid transparent; /* reserve space for selection border */ +} + +QMenu::item:selected +{ + color: #eff0f1; +} + +QMenu::separator { + height: 2px; + background: lightblue; + margin-left: 10px; + margin-right: 5px; +} + +QMenu::indicator { + width: 18px; + height: 18px; +} + +/* non-exclusive indicator = check box style indicator + (see QActionGroup::setExclusive) */ +QMenu::indicator:non-exclusive:unchecked { + image: url(:/qss_icons/rc/checkbox_unchecked.png); +} + +QMenu::indicator:non-exclusive:unchecked:selected { + image: url(:/qss_icons/rc/checkbox_unchecked_disabled.png); +} + +QMenu::indicator:non-exclusive:checked { + image: url(:/qss_icons/rc/checkbox_checked.png); +} + +QMenu::indicator:non-exclusive:checked:selected { + image: url(:/qss_icons/rc/checkbox_checked_disabled.png); +} + +/* exclusive indicator = radio button style indicator (see QActionGroup::setExclusive) */ +QMenu::indicator:exclusive:unchecked { + image: url(:/qss_icons/rc/radio_unchecked.png); +} + +QMenu::indicator:exclusive:unchecked:selected { + image: url(:/qss_icons/rc/radio_unchecked_disabled.png); +} + +QMenu::indicator:exclusive:checked { + image: url(:/qss_icons/rc/radio_checked.png); +} + +QMenu::indicator:exclusive:checked:selected { + image: url(:/qss_icons/rc/radio_checked_disabled.png); +} + +QMenu::right-arrow { + margin: 5px; + image: url(:/qss_icons/rc/right_arrow.png) +} + + +QWidget:disabled +{ + color: #454545; + background-color: #31363b; +} + +QAbstractItemView +{ + alternate-background-color: #31363b; + color: #eff0f1; + border: 1px solid 3A3939; + border-radius: 2px; +} + +QWidget:focus, QMenuBar:focus +{ + border: 1px solid #3daee9; +} + +QTabWidget:focus, QCheckBox:focus, QRadioButton:focus, QSlider:focus +{ + border: none; +} + +QLineEdit +{ + background-color: #232629; + padding: 5px; + border-style: solid; + border: 1px solid #76797C; + border-radius: 2px; + color: #eff0f1; +} + +QGroupBox { + border:1px solid #76797C; + border-radius: 2px; + margin-top: 20px; + font-size:18px; + font-weight:bold; +} + +QGroupBox::title { + subcontrol-origin: margin; + subcontrol-position: top center; + padding-left: 10px; + padding-right: 10px; + padding-top: 10px; + font-size:18px; + font-weight:bold; +} + +QAbstractScrollArea +{ + border-radius: 2px; + border: 1px solid #76797C; + background-color: transparent; +} + +QScrollBar:horizontal +{ + height: 15px; + margin: 3px 15px 3px 15px; + border: 1px transparent #2A2929; + border-radius: 4px; + background-color: #2A2929; +} + +QScrollBar::handle:horizontal +{ + background-color: #605F5F; + min-width: 5px; + border-radius: 4px; +} + +QScrollBar::add-line:horizontal +{ + margin: 0px 3px 0px 3px; + border-image: url(:/qss_icons/rc/right_arrow_disabled.png); + width: 10px; + height: 10px; + subcontrol-position: right; + subcontrol-origin: margin; +} + +QScrollBar::sub-line:horizontal +{ + margin: 0px 3px 0px 3px; + border-image: url(:/qss_icons/rc/left_arrow_disabled.png); + height: 10px; + width: 10px; + subcontrol-position: left; + subcontrol-origin: margin; +} + +QScrollBar::add-line:horizontal:hover,QScrollBar::add-line:horizontal:on +{ + border-image: url(:/qss_icons/rc/right_arrow.png); + height: 10px; + width: 10px; + subcontrol-position: right; + subcontrol-origin: margin; +} + + +QScrollBar::sub-line:horizontal:hover, QScrollBar::sub-line:horizontal:on +{ + border-image: url(:/qss_icons/rc/left_arrow.png); + height: 10px; + width: 10px; + subcontrol-position: left; + subcontrol-origin: margin; +} + +QScrollBar::up-arrow:horizontal, QScrollBar::down-arrow:horizontal +{ + background: none; +} + + +QScrollBar::add-page:horizontal, QScrollBar::sub-page:horizontal +{ + background: none; +} + +QScrollBar:vertical +{ + background-color: #2A2929; + width: 15px; + margin: 15px 3px 15px 3px; + border: 1px transparent #2A2929; + border-radius: 4px; +} + +QScrollBar::handle:vertical +{ + background-color: #605F5F; + min-height: 5px; + border-radius: 4px; +} + +QScrollBar::sub-line:vertical +{ + margin: 3px 0px 3px 0px; + border-image: url(:/qss_icons/rc/up_arrow_disabled.png); + height: 10px; + width: 10px; + subcontrol-position: top; + subcontrol-origin: margin; +} + +QScrollBar::add-line:vertical +{ + margin: 3px 0px 3px 0px; + border-image: url(:/qss_icons/rc/down_arrow_disabled.png); + height: 10px; + width: 10px; + subcontrol-position: bottom; + subcontrol-origin: margin; +} + +QScrollBar::sub-line:vertical:hover,QScrollBar::sub-line:vertical:on +{ + + border-image: url(:/qss_icons/rc/up_arrow.png); + height: 10px; + width: 10px; + subcontrol-position: top; + subcontrol-origin: margin; +} + + +QScrollBar::add-line:vertical:hover, QScrollBar::add-line:vertical:on +{ + border-image: url(:/qss_icons/rc/down_arrow.png); + height: 10px; + width: 10px; + subcontrol-position: bottom; + subcontrol-origin: margin; +} + +QScrollBar::up-arrow:vertical, QScrollBar::down-arrow:vertical +{ + background: none; +} + + +QScrollBar::add-page:vertical, QScrollBar::sub-page:vertical +{ + background: none; +} + +QTextEdit +{ + background-color: #232629; + color: #eff0f1; + border: 1px solid #76797C; +} + +QPlainTextEdit +{ + background-color: #232629;; + color: #eff0f1; + border-radius: 2px; + border: 1px solid #76797C; +} + +QHeaderView::section +{ + background-color: #76797C; + color: #eff0f1; + padding: 5px; + border: 1px solid #76797C; +} + +QSizeGrip { + image: url(:/qss_icons/rc/sizegrip.png); + width: 12px; + height: 12px; +} + + +QMainWindow::separator +{ + background-color: #31363b; + color: white; + padding-left: 4px; + spacing: 2px; + border: 1px dashed #76797C; +} + +QMainWindow::separator:hover +{ + + background-color: #787876; + color: white; + padding-left: 4px; + border: 1px solid #76797C; + spacing: 2px; +} + + +QMenu::separator +{ + height: 1px; + background-color: #76797C; + color: white; + padding-left: 4px; + margin-left: 10px; + margin-right: 5px; +} + + +QFrame +{ + border-radius: 2px; + border: 1px solid #76797C; +} + +QFrame[frameShape="0"] +{ + border-radius: 2px; + border: 1px transparent #76797C; +} + +QStackedWidget +{ + border: 1px transparent black; +} + +QToolBar { + border: 1px transparent #393838; + background: 1px solid #31363b; + font-weight: bold; +} + +QToolBar::handle:horizontal { + image: url(:/qss_icons/rc/Hmovetoolbar.png); +} +QToolBar::handle:vertical { + image: url(:/qss_icons/rc/Vmovetoolbar.png); +} +QToolBar::separator:horizontal { + image: url(:/qss_icons/rc/Hsepartoolbar.png); +} +QToolBar::separator:vertical { + image: url(:/qss_icons/rc/Vsepartoolbar.png); +} +QToolButton#qt_toolbar_ext_button { + background: #58595a +} + +QPushButton +{ + color: #eff0f1; + background-color: #31363b; + border-width: 1px; + border-color: #76797C; + border-style: solid; + padding: 5px; + border-radius: 2px; + outline: none; +} + +QPushButton:disabled +{ + background-color: #31363b; + border-width: 1px; + border-color: #454545; + border-style: solid; + padding-top: 5px; + padding-bottom: 5px; + padding-left: 10px; + padding-right: 10px; + border-radius: 2px; + color: #454545; +} + +QPushButton:focus { + background-color: #3daee9; + color: white; +} + +QPushButton:pressed +{ + background-color: #3daee9; + padding-top: -15px; + padding-bottom: -17px; +} + +QPushButton#blueButton{ + border-radius: 4px; + border: 2px solid rgb(41, 41, 41); + background-color: rgb(0, 112, 193); +} +QPushButton#blueButton:hover{ + border-color: rgb(45, 45, 45); +} +QPushButton#blueButton:pressed, QPushButton#buleButton:checked{ + border-color: rgb(0, 160, 230); +} + + +QPushButton#redButton{ + border-radius: 4px; + border: 2px solid rgb(41, 41, 41); + background-color: rgb(114, 26, 20); +} +QPushButton#redButton:hover{ + border-color: rgb(45, 45, 45); +} +QPushButton#redButton:pressed, QPushButton#buleButton:checked{ + border-color: rgb(0, 160, 230); +} + +QPushButton#greenButton{ + border-radius: 4px; + border: 2px solid rgb(41, 41, 41); + background-color: rgb(118, 197, 126); +} +QPushButton#greenButton:hover{ + border-color: rgb(45, 45, 45); +} +QPushButton#greenButton:pressed, QPushButton#buleButton:checked{ + border-color: rgb(0, 160, 230); +} + + +QComboBox +{ + selection-background-color: #3daee9; + border-style: solid; + border: 1px solid #76797C; + border-radius: 2px; + padding: 5px; + min-width: 75px; +} + +QPushButton:checked{ + background-color: #76797C; + border-color: #6A6969; +} + +QComboBox:hover,QPushButton:hover,QAbstractSpinBox:hover,QLineEdit:hover,QTextEdit:hover,QPlainTextEdit:hover,QAbstractView:h +over,QTreeView:hover +{ + border: 1px solid #3daee9; + color: #eff0f1; +} + +QComboBox:on +{ + padding-top: 3px; + padding-left: 4px; + selection-background-color: #4a4a4a; +} + +QComboBox QAbstractItemView +{ + background-color: #232629; + border-radius: 2px; + border: 1px solid #76797C; + selection-background-color: #3daee9; +} + +QComboBox::drop-down +{ + subcontrol-origin: padding; + subcontrol-position: top right; + width: 15px; + + border-left-width: 0px; + border-left-color: darkgray; + border-left-style: solid; + border-top-right-radius: 3px; + border-bottom-right-radius: 3px; +} + +QComboBox::down-arrow +{ + image: url(:/qss_icons/rc/down_arrow_disabled.png); +} + +QComboBox::down-arrow:on, QComboBox::down-arrow:hover, +QComboBox::down-arrow:focus +{ + image: url(:/qss_icons/rc/down_arrow.png); +} + +QAbstractSpinBox { + padding: 5px; + border: 1px solid #76797C; + background-color: #232629; + color: #eff0f1; + border-radius: 2px; + min-width: 75px; +} + +QAbstractSpinBox:up-button +{ + background-color: transparent; + subcontrol-origin: border; + subcontrol-position: center right; +} + +QAbstractSpinBox:down-button +{ + background-color: transparent; + subcontrol-origin: border; + subcontrol-position: center left; +} + +QAbstractSpinBox::up-arrow,QAbstractSpinBox::up-arrow:disabled,QAbstractSpinBox::up-arrow:off { + image: url(:/qss_icons/rc/up_arrow_disabled.png); + width: 10px; + height: 10px; +} +QAbstractSpinBox::up-arrow:hover +{ + image: url(:/qss_icons/rc/up_arrow.png); +} + + +QAbstractSpinBox::down-arrow,QAbstractSpinBox::down-arrow:disabled,QAbstractSpinBox::down-arrow:off +{ + image: url(:/qss_icons/rc/down_arrow_disabled.png); + width: 10px; + height: 10px; +} +QAbstractSpinBox::down-arrow:hover +{ + image: url(:/qss_icons/rc/down_arrow.png); +} + + +QLabel +{ + border: 0px solid black; +} + +QLabel#whiteLabel +{ + border: 0px solid black; + background:white; + color:rgb(100,100,100,250); + font-size:15px; + font-weight:bold; + font-family:Roman times; +} + +QLabel#whiteLabel:hover +{ + color:rgb(100,100,100,120); +} + +QLabel#blueLabel +{ + border: 0px solid black; + background:rgb(0, 112, 193); + font-size:15px; + font-weight:bold; + font-family:Roman times; +} + +QLabel#blueLabel:hover +{ + color:rgb(100,100,100,120); +} + +QLabel#redLabel +{ + border: 0px solid black; + background: rgb(114, 26, 20); + font-size:15px; + font-weight:bold; + font-family:Roman times; +} + +QLabel#redLabel:hover +{ + color:rgb(100,100,100,120); +} + +QLabel#greenLabel +{ + border: 0px solid black; + background: rgb(118, 197, 126); + font-size:15px; + font-weight:bold; + font-family:Roman times; +} + +QLabel#greenLabel:hover +{ + color:rgb(100,100,100,120); +} + +QLabel#orangeLabel +{ + border: 0px solid black; + background: rgb(255, 121, 0); + font-size:15px; + font-weight:bold; + font-family:Roman times; +} + +QLabel#orangeLabel:hover +{ + color:rgb(100,100,100,120); +} + +QLabel#purseLabel +{ + border: 0px solid black; + background: rgb(98, 43, 98); + font-size:15px; + font-weight:bold; + font-family:Roman times; +} + +QLabel#purseLabel:hover +{ + color:rgb(100,100,100,120); +} + +QTabWidget{ + border: 0px transparent black; +} + +QTabWidget::pane { + border: 1px solid #76797C; + padding: 5px; + margin: 0px; +} + +QTabBar +{ + qproperty-drawBase: 0; + left: 5px; /* move to the right by 5px */ + border-radius: 3px; +} + +QTabBar:focus +{ + border: 0px transparent black; +} + +QTabBar::close-button { + image: url(:/qss_icons/rc/close.png); + background: transparent; +} + +QTabBar::close-button:hover +{ + image: url(:/qss_icons/rc/close-hover.png); + background: transparent; +} + +QTabBar::close-button:pressed { + image: url(:/qss_icons/rc/close-pressed.png); + background: transparent; +} + +/* TOP TABS */ +QTabBar::tab:top { + color: #eff0f1; + border: 1px solid #76797C; + border-bottom: 1px transparent black; + background-color: #31363b; + padding: 5px; + min-width: 50px; + border-top-left-radius: 2px; + border-top-right-radius: 2px; +} + +QTabBar::tab:top:!selected +{ + color: #eff0f1; + background-color: #54575B; + border: 1px solid #76797C; + border-bottom: 1px transparent black; + border-top-left-radius: 2px; + border-top-right-radius: 2px; +} + +QTabBar::tab:top:!selected:hover { + background-color: #3daee9; +} + +/* BOTTOM TABS */ +QTabBar::tab:bottom { + color: #eff0f1; + border: 1px solid #76797C; + border-top: 1px transparent black; + background-color: #31363b; + padding: 5px; + border-bottom-left-radius: 2px; + border-bottom-right-radius: 2px; + min-width: 50px; +} + +QTabBar::tab:bottom:!selected +{ + color: #eff0f1; + background-color: #54575B; + border: 1px solid #76797C; + border-top: 1px transparent black; + border-bottom-left-radius: 2px; + border-bottom-right-radius: 2px; +} + +QTabBar::tab:bottom:!selected:hover { + background-color: #3daee9; +} + +/* LEFT TABS */ +QTabBar::tab:left { + color: #eff0f1; + border: 1px solid #76797C; + border-left: 1px transparent black; + background-color: #31363b; + padding: 5px; + border-top-right-radius: 2px; + border-bottom-right-radius: 2px; + min-height: 50px; +} + +QTabBar::tab:left:!selected +{ + color: #eff0f1; + background-color: #54575B; + border: 1px solid #76797C; + border-left: 1px transparent black; + border-top-right-radius: 2px; + border-bottom-right-radius: 2px; +} + +QTabBar::tab:left:!selected:hover { + background-color: #3daee9; +} + + +/* RIGHT TABS */ +QTabBar::tab:right { + color: #eff0f1; + border: 1px solid #76797C; + border-right: 1px transparent black; + background-color: #31363b; + padding: 5px; + border-top-left-radius: 2px; + border-bottom-left-radius: 2px; + min-height: 50px; +} + +QTabBar::tab:right:!selected +{ + color: #eff0f1; + background-color: #54575B; + border: 1px solid #76797C; + border-right: 1px transparent black; + border-top-left-radius: 2px; + border-bottom-left-radius: 2px; +} + +QTabBar::tab:right:!selected:hover { + background-color: #3daee9; +} + +QTabBar QToolButton::right-arrow:enabled { + image: url(:/qss_icons/rc/right_arrow.png); + } + + QTabBar QToolButton::left-arrow:enabled { + image: url(:/qss_icons/rc/left_arrow.png); + } + +QTabBar QToolButton::right-arrow:disabled { + image: url(:/qss_icons/rc/right_arrow_disabled.png); + } + + QTabBar QToolButton::left-arrow:disabled { + image: url(:/qss_icons/rc/left_arrow_disabled.png); + } + + +QDockWidget { + background: #31363b; + border: 1px solid #403F3F; + titlebar-close-icon: url(:/qss_icons/rc/close.png); + titlebar-normal-icon: url(:/qss_icons/rc/undock.png); +} + +QDockWidget::close-button, QDockWidget::float-button { + border: 1px solid transparent; + border-radius: 2px; + background: transparent; +} + +QDockWidget::close-button:hover, QDockWidget::float-button:hover { + background: rgba(255, 255, 255, 10); +} + +QDockWidget::close-button:pressed, QDockWidget::float-button:pressed { + padding: 1px -1px -1px 1px; + background: rgba(255, 255, 255, 10); +} + +QTreeView, QListView +{ + border: 1px solid #76797C; + background-color: #232629; +} + +QTreeView:branch:selected, QTreeView:branch:hover +{ + background: url(:/qss_icons/rc/transparent.png); +} + +QTreeView::branch:has-siblings:!adjoins-item { + border-image: url(:/qss_icons/rc/transparent.png); +} + +QTreeView::branch:has-siblings:adjoins-item { + border-image: url(:/qss_icons/rc/transparent.png); +} + +QTreeView::branch:!has-children:!has-siblings:adjoins-item { + border-image: url(:/qss_icons/rc/transparent.png); +} + +QTreeView::branch:has-children:!has-siblings:closed, +QTreeView::branch:closed:has-children:has-siblings { + image: url(:/qss_icons/rc/branch_closed.png); +} + +QTreeView::branch:open:has-children:!has-siblings, +QTreeView::branch:open:has-children:has-siblings { + image: url(:/qss_icons/rc/branch_open.png); +} + +QTreeView::branch:has-children:!has-siblings:closed:hover, +QTreeView::branch:closed:has-children:has-siblings:hover { + image: url(:/qss_icons/rc/branch_closed-on.png); + } + +QTreeView::branch:open:has-children:!has-siblings:hover, +QTreeView::branch:open:has-children:has-siblings:hover { + image: url(:/qss_icons/rc/branch_open-on.png); + } + +QListView::item:!selected:hover, QTreeView::item:!selected:hover { + background: rgba(167,218,245, 0.3); + outline: 0; + color: #eff0f1 +} + +QListView::item:selected:hover, QTreeView::item:selected:hover { + background: #3daee9; + color: #eff0f1; +} + +QSlider::groove:horizontal { + border: 1px solid #565a5e; + height: 4px; + background: #565a5e; + margin: 0px; + border-radius: 2px; +} + +QSlider::handle:horizontal { + background: #232629; + border: 1px solid #565a5e; + width: 16px; + height: 16px; + margin: -8px 0; + border-radius: 9px; +} + +QSlider::groove:vertical { + border: 1px solid #565a5e; + width: 4px; + background: #565a5e; + margin: 0px; + border-radius: 3px; +} + +QSlider::handle:vertical { + background: #232629; + border: 1px solid #565a5e; + width: 16px; + height: 16px; + margin: 0 -8px; + border-radius: 9px; +} + +QToolButton { + background-color: transparent; + border: 1px transparent #76797C; + border-radius: 2px; + margin: 3px; + padding: 5px; +} + +QToolButton[popupMode="1"] { /* only for MenuButtonPopup */ + padding-right: 20px; /* make way for the popup button */ + border: 1px #76797C; + border-radius: 5px; +} + +QToolButton[popupMode="2"] { /* only for InstantPopup */ + padding-right: 10px; /* make way for the popup button */ + border: 1px #76797C; +} + + +QToolButton:hover, QToolButton::menu-button:hover { + background-color: transparent; + border: 1px solid #3daee9; + padding: 5px; +} + +QToolButton:checked, QToolButton:pressed, + QToolButton::menu-button:pressed { + background-color: #3daee9; + border: 1px solid #3daee9; + padding: 5px; +} + +/* the subcontrol below is used only in the InstantPopup or DelayedPopup mode */ +QToolButton::menu-indicator { + image: url(:/qss_icons/rc/down_arrow.png); + top: -7px; left: -2px; /* shift it a bit */ +} + +/* the subcontrols below are used only in the MenuButtonPopup mode */ +QToolButton::menu-button { + border: 1px transparent #76797C; + border-top-right-radius: 6px; + border-bottom-right-radius: 6px; + /* 16px width + 4px for border = 20px allocated above */ + width: 16px; + outline: none; +} + +QToolButton::menu-arrow { + image: url(:/qss_icons/rc/down_arrow.png); +} + +QToolButton::menu-arrow:open { + border: 1px solid #76797C; +} + +QPushButton::menu-indicator { + subcontrol-origin: padding; + subcontrol-position: bottom right; + left: 8px; +} + +QTableView +{ + border: 1px solid #76797C; + gridline-color: #31363b; + background-color: #232629; +} + + +QTableView, QHeaderView +{ + border-radius: 0px; +} + +QTableView::item:pressed, QListView::item:pressed, QTreeView::item:pressed { + background: #3daee9; + color: #eff0f1; +} + +QTableView::item:selected:active, QTreeView::item:selected:active, QListView::item:selected:active { + background: #3daee9; + color: #eff0f1; +} + + +QHeaderView +{ + background-color: #31363b; + border: 1px transparent; + border-radius: 0px; + margin: 0px; + padding: 0px; + +} + +QHeaderView::section { + background-color: #31363b; + color: #eff0f1; + padding: 5px; + border: 1px solid #76797C; + border-radius: 0px; + text-align: center; +} + +QHeaderView::section::vertical::first, QHeaderView::section::vertical::only-one +{ + border-top: 1px solid #76797C; +} + +QHeaderView::section::vertical +{ + border-top: transparent; +} + +QHeaderView::section::horizontal::first, QHeaderView::section::horizontal::only-one +{ + border-left: 1px solid #76797C; +} + +QHeaderView::section::horizontal +{ + border-left: transparent; +} + + +QHeaderView::section:checked + { + color: white; + background-color: #334e5e; + } + + /* style the sort indicator */ +QHeaderView::down-arrow { + image: url(:/qss_icons/rc/down_arrow.png); +} + +QHeaderView::up-arrow { + image: url(:/qss_icons/rc/up_arrow.png); +} + + +QTableCornerButton::section { + background-color: #31363b; + border: 1px transparent #76797C; + border-radius: 0px; +} + +QToolBox { + padding: 5px; + border: 1px transparent black; +} + +QToolBox::tab { + color: #eff0f1; + background-color: #31363b; + border: 1px solid #76797C; + border-bottom: 1px transparent #31363b; + border-top-left-radius: 5px; + border-top-right-radius: 5px; +} + +QToolBox::tab:selected { /* italicize selected tabs */ + font: italic; + background-color: #31363b; + border-color: #3daee9; + } + +QStatusBar::item { + border: 0px transparent dark; + } + + +QFrame[height="3"], QFrame[width="3"] { + background-color: #76797C; +} + + +QSplitter::handle { + border: 1px dashed #76797C; +} + +QSplitter::handle:hover { + background-color: #787876; + border: 1px solid #76797C; +} + +QSplitter::handle:horizontal { + width: 1px; +} + +QSplitter::handle:vertical { + height: 1px; +} + +QProgressBar { + border: 1px solid #76797C; + border-radius: 5px; + text-align: center; +} + +QProgressBar::chunk { + background-color: #05B8CC; +} + + diff --git a/vnpy/trader/uiKLine/cta.ico b/vnpy/trader/uiKLine/cta.ico new file mode 100644 index 00000000..1cc84063 Binary files /dev/null and b/vnpy/trader/uiKLine/cta.ico differ diff --git a/vnpy/trader/uiKLine/uiCrosshair.py b/vnpy/trader/uiKLine/uiCrosshair.py new file mode 100644 index 00000000..f827b00b --- /dev/null +++ b/vnpy/trader/uiKLine/uiCrosshair.py @@ -0,0 +1,298 @@ +# encoding: UTF-8 +import sys,os + +import pyqtgraph as pg +import datetime as dt +import numpy as np +import traceback + +from qtpy import QtWidgets, QtGui, QtCore +from pyqtgraph.Point import Point +import pandas as pd + +######################################################################## +# 十字光标支持 +######################################################################## +class Crosshair(QtCore.QObject): + """ + 此类给pg.PlotWidget()添加crossHair功能,PlotWidget实例需要初始化时传入 + """ + signal = QtCore.Signal(type(tuple([]))) + signalInfo = QtCore.Signal(float,float) + #---------------------------------------------------------------------- + def __init__(self, parent, master): + """Constructor""" + self.__view = parent # PlatWidget + self.master = master # KLineWidget + super(Crosshair, self).__init__() + + self.xAxis = 0 + self.yAxis = 0 + + self.datas = None + + self.yAxises = [0 for i in range(3)] + self.leftX = [0 for i in range(3)] + self.showHLine = [False for i in range(3)] + self.textPrices = [pg.TextItem('', anchor=(1, 1)) for i in range(3)] + # 取得 widget上的:主图/成交量/副图指标 + self.views = [parent.centralWidget.getItem(i+1, 0) for i in range(3)] + self.rects = [self.views[i].sceneBoundingRect() for i in range(3)] + self.vLines = [pg.InfiniteLine(angle=90, movable=False) for i in range(3)] + self.hLines = [pg.InfiniteLine(angle=0, movable=False) for i in range(3)] + + #mid 在y轴动态跟随最新价显示最新价和最新时间 + self.__textDate = pg.TextItem('date', anchor=(1, 1)) # 文本:日期/时间 + self.__textInfo = pg.TextItem('lastBarInfo') # 文本:bar信息 + self.__text_main_indicators = pg.TextItem('lastIndicatorsInfo', anchor=(1, 0)) # 文本:主图指标 + self.__text_sub_indicators = pg.TextItem('lastSubIndicatorsInfo', anchor=(1, 0)) # 文本: 副图指标 + self.__textVolume = pg.TextItem('lastBarVolume', anchor=(1,0)) # 文本: 成交量 + + self.__textDate.setZValue(2) + self.__textInfo.setZValue(2) + self.__text_main_indicators.setZValue(2) + self.__text_sub_indicators.setZValue(2) + self.__textVolume.setZValue(2) + self.__textInfo.border = pg.mkPen(color=(230, 255, 0, 255), width=1.2) + + for i in range(3): + self.textPrices[i].setZValue(2) + self.vLines[i].setPos(0) + self.hLines[i].setPos(0) + self.vLines[i].setZValue(0) + self.hLines[i].setZValue(0) + self.views[i].addItem(self.vLines[i]) + self.views[i].addItem(self.hLines[i]) + self.views[i].addItem(self.textPrices[i]) + + self.views[0].addItem(self.__textInfo, ignoreBounds=True) + self.views[0].addItem(self.__text_main_indicators, ignoreBounds=True) + + # 添加成交量的提示信息到 volume view + if self.master.display_vol: + self.views[1].addItem(self.__textVolume, ignoreBounds=True) + + # 添加 副图指标/日期信息到 副图 + if self.master.display_sub: + self.views[2].addItem(self.__textDate, ignoreBounds=True) + self.views[2].addItem(self.__text_sub_indicators, ignoreBounds=True) + else: + # 没有启用副图,日期信息放在主图 + self.views[0].addItem(self.__textDate, ignoreBounds=True) + + self.proxy = pg.SignalProxy(self.__view.scene().sigMouseMoved, rateLimit=360, slot=self.__mouseMoved) + # 跨线程刷新界面支持 + self.signal.connect(self.update) + self.signalInfo.connect(self.plotInfo) + + #---------------------------------------------------------------------- + def update(self, pos): + """刷新界面显示""" + try: + xAxis, yAxis = pos + xAxis, yAxis = (self.xAxis, self.yAxis) if xAxis is None else (xAxis, yAxis) + self.moveTo(xAxis, yAxis) + except Exception as ex: + print(u'Crosshair.update() exception:{},trace:{}'.format(str(ex), traceback.format_exc())) + + #---------------------------------------------------------------------- + def __mouseMoved(self,evt): + """鼠标移动回调""" + try: + pos = evt[0] + self.rects = [self.views[i].sceneBoundingRect() for i in range(3)] + for i in range(3): + self.showHLine[i] = False + if i == 1 and not self.master.display_vol: + continue + + if i == 2 and not self.master.display_sub: + continue + + if self.rects[i].contains(pos): + mousePoint = self.views[i].vb.mapSceneToView(pos) + xAxis = mousePoint.x() + yAxis = mousePoint.y() + self.yAxises[i] = yAxis + self.showHLine[i] = True + self.moveTo(xAxis, yAxis) + except Exception as ex: + print(u'_mouseMove() exception:{},trace:{}'.format(str(ex), traceback.format_exc())) + + #---------------------------------------------------------------------- + def moveTo(self, xAxis, yAxis): + """ + 移动 + :param xAxis: + :param yAxis: + :return: + """ + try: + xAxis,yAxis = (self.xAxis,self.yAxis) if xAxis is None else (int(xAxis),yAxis) + self.rects = [self.views[i].sceneBoundingRect() for i in range(3)] + if not xAxis or not yAxis: + return + self.xAxis = xAxis + self.yAxis = yAxis + self.vhLinesSetXY(xAxis,yAxis) + self.plotInfo(xAxis,yAxis) + self.master.pi_volume.update() + except Exception as ex: + print(u'_mouseMove() exception:{},trace:{}'.format(str(ex), traceback.format_exc())) + #---------------------------------------------------------------------- + def vhLinesSetXY(self,xAxis, yAxis): + """水平和竖线位置设置""" + for i in range(3): + self.vLines[i].setPos(xAxis) + if self.showHLine[i]: + self.hLines[i].setPos(yAxis if i==0 else self.yAxises[i]) + self.hLines[i].show() + else: + self.hLines[i].hide() + + #---------------------------------------------------------------------- + def plotInfo(self, xAxis, yAxis): + """ + 被嵌入的plotWidget在需要的时候通过调用此方法显示K线信息 + """ + if self.datas is None or xAxis >= len(self.datas): + return + + tickDatetime = None + openPrice = 0 + closePrice = 0 + highPrice = 0 + lowPrice = 0 + preClosePrice = 0 + volume = 0 + openInterest = 0 + + try: + # 获取K线数据 + data = self.datas[xAxis] + lastdata = self.datas[xAxis-1] + tickDatetime = pd.to_datetime(data['datetime']) + openPrice = data['open'] + closePrice = data['close'] + lowPrice = data['low'] + highPrice = data['high'] + volume = int(data['volume']) + openInterest = int(data['openInterest']) + preClosePrice = lastdata['close'] + + except Exception as ex: + print(u'exception:{},trace:{}'.format(str(ex), traceback.format_exc())) + return + + + if( isinstance(tickDatetime, dt.datetime)): + datetimeText = dt.datetime.strftime(tickDatetime, '%Y-%m-%d %H:%M:%S') + dateText = dt.datetime.strftime(tickDatetime, '%Y-%m-%d') + timeText = dt.datetime.strftime(tickDatetime, '%H:%M:%S') + else: + datetimeText = "" + dateText = "" + timeText = "" + + # 显示所有的主图技术指标 + html = u'
' + for indicator in self.master.main_indicator_data: + val = self.master.main_indicator_data[indicator][xAxis] + col = self.master.main_indicator_colors[indicator] + html += u'  %s:%.2f' %(col,indicator,val) + html += u'
' + self.__text_main_indicators.setHtml(html) + + # 显示所有的主图技术指标 + html = u'
' + for indicator in self.master.sub_indicator_data: + val = self.master.sub_indicator_data[indicator][xAxis] + col = self.master.sub_indicator_colors[indicator] + html += u'  %s:%.2f' %(col,indicator,val) + html += u'
' + self.__text_sub_indicators.setHtml(html) + + # 和上一个收盘价比较,决定K线信息的字符颜色 + cOpen = 'red' if openPrice > preClosePrice else 'green' + cClose = 'red' if closePrice > preClosePrice else 'green' + cHigh = 'red' if highPrice > preClosePrice else 'green' + cLow = 'red' if lowPrice > preClosePrice else 'green' + + self.__textInfo.setHtml( + u'
\ + 日期
\ + %s
\ + 时间
\ + %s
\ + 价格
\ + (开) %.3f
\ + (高) %.3f
\ + (低) %.3f
\ + (收) %.3f
\ + 成交量
\ + (量) %d
\ +
'\ + % (dateText,timeText,cOpen,openPrice,cHigh,highPrice,\ + cLow,lowPrice,cClose,closePrice,volume)) + self.__textDate.setHtml( + '
\ + %s\ +
'\ + % (datetimeText)) + + self.__textVolume.setHtml( + '
\ + VOL : %.3f\ +
'\ + % (volume)) + # 坐标轴宽度 + rightAxisWidth = self.views[0].getAxis('right').width() + bottomAxisHeight = self.views[2].getAxis('bottom').height() + offset = QtCore.QPointF(rightAxisWidth, bottomAxisHeight) + + # 各个顶点 + t1 = [None, None, None] + br = [None, None, None] + t1[0] = self.views[0].vb.mapSceneToView(self.rects[0].topLeft()) + br[0] = self.views[0].vb.mapSceneToView(self.rects[0].bottomRight() - offset) + + if self.master.display_vol: + t1[1] = self.views[1].vb.mapSceneToView(self.rects[1].topLeft()) + br[1] = self.views[1].vb.mapSceneToView(self.rects[1].bottomRight() - offset) + if self.master.display_sub: + t1[2] = self.views[2].vb.mapSceneToView(self.rects[2].topLeft()) + br[2] = self.views[2].vb.mapSceneToView(self.rects[2].bottomRight() - offset) + + + # 显示价格 + for i in range(3): + if self.showHLine[i] and t1[i] is not None and br[i] is not None: + self.textPrices[i].setHtml( + '
\ + \ + %0.3f\ + \ +
'\ + % (yAxis if i==0 else self.yAxises[i])) + self.textPrices[i].setPos(br[i].x(),yAxis if i==0 else self.yAxises[i]) + self.textPrices[i].show() + else: + self.textPrices[i].hide() + + + # 设置坐标 + self.__textInfo.setPos(t1[0]) + self.__text_main_indicators.setPos(br[0].x(), t1[0].y()) + if self.master.display_sub: + self.__text_sub_indicators.setPos(br[2].x(), t1[2].y()) + if self.master.display_vol: + self.__textVolume.setPos(br[1].x(),t1[1].y()) + + # 修改对称方式防止遮挡 + self.__textDate.anchor = Point((1,1)) if xAxis > self.master.index else Point((0,1)) + if br[2] is not None: + self.__textDate.setPos(xAxis, br[2].y()) + elif br[1] is not None: + self.__textDate.setPos(xAxis, br[1].y()) + else: + self.__textDate.setPos(xAxis, br[0].y()) diff --git a/vnpy/trader/uiKLine/uiKLine.py b/vnpy/trader/uiKLine/uiKLine.py new file mode 100644 index 00000000..b9c8325d --- /dev/null +++ b/vnpy/trader/uiKLine/uiKLine.py @@ -0,0 +1,1402 @@ +# -*- coding: utf-8 -*- +""" +Python K线模块,包含十字光标和鼠标键盘交互 +Support By 量投科技(http://www.quantdo.com.cn/) + +修改by:华富资产,李来佳 +20180515 change log: +1.修改命名规则,将图形划分为 主图(main),副图1(volume),副图2:(sub),能够设置开关显示/关闭 两个副图。区分indicator/signal +2.修改鼠标滚动得操作,去除focus,增加双击鼠标事件 +3.增加重定位接口,供上层界面调用,实现多周期窗口的时间轴同步。 + +""" + + +# Qt相关和十字光标 +import sys +from vnpy.trader.uiKLine.uiCrosshair import Crosshair +from vnpy.trader.vtConstant import DIRECTION_LONG,DIRECTION_SHORT, OFFSET_OPEN, OFFSET_CLOSE +import pyqtgraph as pg +from qtpy import QtGui, QtCore, QtWidgets +# 其他 +import numpy as np +import pandas as pd +from functools import partial +from datetime import datetime,timedelta +from collections import deque,OrderedDict +import traceback +import copy + +######################################################################## +# 键盘鼠标功能 +######################################################################## +class KeyWraper(QtWidgets.QWidget): + """键盘鼠标功能支持的窗体元类""" + #初始化 + #---------------------------------------------------------------------- + def __init__(self, parent=None): + QtWidgets.QWidget.__init__(self, parent) + + # 定时器(for 鼠标双击) + self.timer = QtCore.QTimer() + self.timer.setInterval(300) + self.timer.setSingleShot(True) + self.timer.timeout.connect(self.timeout) + self.click_count = 0 # 鼠标点击次数 + self.pos = None # 鼠标点击的位置 + + # 激活鼠标跟踪功能 + self.setMouseTracking(True) + + def timeout(self): + """鼠标双击定时检查""" + if self.click_count == 1 and self.pos is not None: + self.onLClick(self.pos) + + self.click_count = 0 + self.pos = None + + def keyPressEvent(self, event): + """ + 重载方法keyPressEvent(self,event),即按键按下事件方法 + :param event: + :return: + """ + if event.key() == QtCore.Qt.Key_Up: + self.onUp() + elif event.key() == QtCore.Qt.Key_Down: + self.onDown() + elif event.key() == QtCore.Qt.Key_Left: + self.onLeft() + elif event.key() == QtCore.Qt.Key_Right: + self.onRight() + elif event.key() == QtCore.Qt.Key_PageUp: + self.onPre() + elif event.key() == QtCore.Qt.Key_PageDown: + self.onNxt() + event.accept() + + def mousePressEvent(self, event): + """ + 重载方法mousePressEvent(self,event),即鼠标点击事件方法 + :param event: + :return: + """ + if event.button() == QtCore.Qt.RightButton: + self.onRClick(event.pos()) + elif event.button() == QtCore.Qt.LeftButton: + self.click_count += 1 + if not self.timer.isActive(): + self.timer.start() + + if self.click_count > 1: + self.onDoubleClick(event.pos()) + else: + self.pos = event.pos() + + event.accept() + + def mouseRelease(self, event): + """ + 重载方法mouseReleaseEvent(self,event),即鼠标点击事件方法 + :param event: + :return: + """ + if event.button() == QtCore.Qt.RightButton: + self.onRRelease(event.pos()) + elif event.button() == QtCore.Qt.LeftButton: + self.onLRelease(event.pos()) + self.releaseMouse() + + def wheelEvent(self, event): + """ + 重载方法wheelEvent(self,event),即滚轮事件方法 + :param event: + :return: + """ + try: + pos = event.angleDelta() + if pos.y() > 0: + self.onUp() + else: + self.onDown() + event.accept() + except Exception as ex: + print(u'wheelEvent exception:{},{}'.format(str(ex), traceback.format_exc())) + + def paintEvent(self, event): + """ + 重载方法paintEvent(self,event),即拖动事件方法 + :param event: + :return: + """ + self.onPaint() + event.accept() + + # PgDown键 + #---------------------------------------------------------------------- + def onNxt(self): + pass + + # PgUp键 + #---------------------------------------------------------------------- + def onPre(self): + pass + + # 向上键和滚轮向上 + #---------------------------------------------------------------------- + def onUp(self): + pass + + # 向下键和滚轮向下 + #---------------------------------------------------------------------- + def onDown(self): + pass + + # 向左键 + #---------------------------------------------------------------------- + def onLeft(self): + pass + + # 向右键 + #---------------------------------------------------------------------- + def onRight(self): + pass + + # 鼠标左单击 + #---------------------------------------------------------------------- + def onLClick(self,pos): + print('single left click') + + # 鼠标右单击 + #---------------------------------------------------------------------- + def onRClick(self, pos): + pass + + def onDoubleClick(self, pos): + print('double click') + + # 鼠标左释放 + #---------------------------------------------------------------------- + def onLRelease(self,pos): + pass + + # 鼠标右释放 + #---------------------------------------------------------------------- + def onRRelease(self,pos): + pass + + # 画图 + #---------------------------------------------------------------------- + def onPaint(self): + pass + + +######################################################################## +# 选择缩放功能支持 +######################################################################## +class CustomViewBox(pg.ViewBox): + #---------------------------------------------------------------------- + def __init__(self, *args, **kwds): + pg.ViewBox.__init__(self, *args, **kwds) + # 拖动放大模式 + #self.setMouseMode(self.RectMode) + + ## 右键自适应 + #---------------------------------------------------------------------- + def mouseClickEvent(self, ev): + if ev.button() == QtCore.Qt.RightButton: + self.autoRange() + +class MyStringAxis(pg.AxisItem): + """ + 时间序列横坐标支持 + changelog: by 李来佳 + 增加时间与x轴的双向映射 + """ + + # 初始化 + #---------------------------------------------------------------------- + def __init__(self, xdict, *args, **kwargs): + pg.AxisItem.__init__(self, *args, **kwargs) + self.minVal = 0 + self.maxVal = 0 + # 序列 <= > 时间 + self.xdict = OrderedDict() + self.xdict.update(xdict) + # 时间 <=> 序列 + self.tdict = OrderedDict([(v,k) for k,v in xdict.items()]) + self.x_values = np.asarray(xdict.keys()) + self.x_strings = list(xdict.values()) + self.setPen(color=(255, 255, 255, 255), width=0.8) + self.setStyle(tickFont = QtGui.QFont("Roman times",10,QtGui.QFont.Bold),autoExpandTextSpace=True) + + def update_xdict(self, xdict): + """ + 更新坐标映射表 + :param xdict: + :return: + """ + # 更新 x轴-时间映射 + self.xdict.update(xdict) + # 更新 时间-x轴映射 + tdict = dict([(v, k) for k, v in xdict.items()]) + self.tdict.update(tdict) + + # 重新生成x轴队列和时间字符串显示队列 + self.x_values = np.asarray(self.xdict.keys()) + self.x_strings = list(self.xdict.values()) + + def get_x_by_time(self, t_value): + """ + 通过 时间,找到匹配或最接近x轴 + :param t_value: datetime 类型时间 + :return: + """ + last_time = None + for t in self.x_strings: + if last_time is None: + last_time = t + continue + if t > t_value: + break + last_time = t + + x = self.tdict.get(last_time, 0) + return x + + def tickStrings(self, values, scale, spacing): + """ + 将原始横坐标转换为时间字符串,第一个坐标包含日期 + :param values: + :param scale: + :param spacing: + :return: + """ + strings = [] + for v in values: + vs = v * scale + if vs in self.x_values: + vstr = self.x_strings[np.abs(self.x_values-vs).argmin()] + vstr = vstr.strftime('%Y-%m-%d %H:%M:%S') + else: + vstr = "" + strings.append(vstr) + return strings + +class CandlestickItem(pg.GraphicsObject): + """K线图形对象""" + + # 初始化 + #---------------------------------------------------------------------- + def __init__(self, data): + """初始化""" + pg.GraphicsObject.__init__(self) + # 数据格式: [ (time, open, close, low, high),...] + self.data = data + # 只重画部分图形,大大提高界面更新速度 + self.rect = None + self.picture = None + self.setFlag(self.ItemUsesExtendedStyleOption) + # 画笔和画刷 + w = 0.4 + self.offset = 0 + self.low = 0 + self.high = 1 + self.picture = QtGui.QPicture() + self.pictures = [] + self.bPen = pg.mkPen(color=(0, 240, 240, 255), width=w*2) # 阴线画笔 + self.bBrush = pg.mkBrush((0, 240, 240, 255)) # 阴线主体 + self.rPen = pg.mkPen(color=(255, 60, 60, 255), width=w*2) # 阳线画笔 + self.rBrush = pg.mkBrush((255, 60, 60, 255)) # 阳线主体 + self.rBrush.setStyle(QtCore.Qt.NoBrush) + # 刷新K线 + self.generatePicture(self.data) + + # 画K线 + #---------------------------------------------------------------------- + def generatePicture(self,data=None,redraw=False): + """重新生成图形对象""" + # 重画或者只更新最后一个K线 + if redraw: + self.pictures = [] + elif self.pictures: + self.pictures.pop() + w = 0.4 + bPen = self.bPen + bBrush = self.bBrush + rPen = self.rPen + rBrush = self.rBrush + low,high = (data[0]['low'],data[0]['high']) if len(data)>0 else (0,1) + for (t, open0, close0, low0, high0) in data: + # t 并不是时间,是序列 + if t >= len(self.pictures): + # 每一个K线创建一个picture + picture = QtGui.QPicture() + p = QtGui.QPainter(picture) + low, high = (min(low, low0), max(high, high0)) + + # 下跌蓝色(实心), 上涨红色(空心) + pen, brush, pmin, pmax = (bPen, bBrush, close0, open0)\ + if open0 > close0 else (rPen, rBrush, open0, close0) + p.setPen(pen) + p.setBrush(brush) + + # 画K线方块和上下影线 + if open0 == close0: + p.drawLine(QtCore.QPointF(t-w,open0), QtCore.QPointF(t+w, close0)) + else: + p.drawRect(QtCore.QRectF(t-w, open0, w*2, close0-open0)) + if pmin > low0: + p.drawLine(QtCore.QPointF(t,low0), QtCore.QPointF(t, pmin)) + if high0 > pmax: + p.drawLine(QtCore.QPointF(t,pmax), QtCore.QPointF(t, high0)) + + p.end() + # 添加到队列中 + self.pictures.append(picture) + + # 更新所有K线的最高/最低 + self.low,self.high = low,high + + # 手动重画 + #---------------------------------------------------------------------- + def update(self): + if not self.scene() is None: + self.scene().update() + + # 自动重画 + #---------------------------------------------------------------------- + def paint(self, painter, opt, w): + # 获取显示区域 + rect = opt.exposedRect + # 获取显示区域/数据的滑动最小值/最大值,即需要显示的数据最小值/最大值。 + xmin,xmax = (max(0,int(rect.left())),min(int(len(self.pictures)),int(rect.right()))) + + # 区域发生变化,或者没有最新图片(缓存),重画 + if not self.rect == (rect.left(),rect.right()) or self.picture is None: + # 更新显示区域 + self.rect = (rect.left(),rect.right()) + # 重画,并缓存为最新图片 + self.picture = self.createPic(xmin,xmax) + self.picture.play(painter) + + # 存在缓存,直接显示出来 + elif not self.picture is None: + self.picture.play(painter) + + # 缓存图片 + #---------------------------------------------------------------------- + def createPic(self,xmin,xmax): + picture = QtGui.QPicture() + p = QtGui.QPainter(picture) + # 全部数据,[xmin~xmax]的k线,重画一次 + [pic.play(p) for pic in self.pictures[xmin:xmax]] + p.end() + return picture + + # 定义显示边界,x轴:0~K线数据长度;Y轴,最低值~最高值-最低值 + #---------------------------------------------------------------------- + def boundingRect(self): + return QtCore.QRectF(0,self.low,len(self.pictures),(self.high-self.low)) + + +######################################################################## +class KLineWidget(KeyWraper): + """用于显示价格走势图""" + + # 是否完成了历史数据的读取 + initCompleted = False + clsId = 0 + #---------------------------------------------------------------------- + def __init__(self, parent=None,display_vol=False, display_sub=False, **kargs): + """Constructor""" + self.parent = parent + super(KLineWidget, self).__init__(parent) + + # 当前序号 + self.index = None # 下标 + self.countK = 60 # 显示的K线数量范围 + + KLineWidget.clsId += 1 + self.windowId = str(KLineWidget.clsId) + + self.title = u'KLineWidget' + + # # 保存K线数据的列表和Numpy Array对象 + self.datas = [] # 'datetime','open','close','low','high','volume','openInterest + self.listBar = [] # 蜡烛图使用的Bar list :'time_int','open','close','low','high' + self.listVol = [] # 成交量(副图使用)的 volume list + + # 交易事务有关的线段 + self.list_trans = [] # 交易事务( {'start_time','end_time','tns_type','start_price','end_price','start_x','end_x','completed'} + self.list_trans_lines = [] + + # 交易记录相关的箭头 + self.list_trade_arrow = [] # 交易图标 list + self.x_t_trade_map = OrderedDict() # x 轴 与交易信号的映射 + self.t_trade_dict = OrderedDict() # t 时间的交易记录 + + # 标记相关 + self.list_markup = [] + self.x_t_markup_map = OrderedDict() # x轴与标记的映射 + self.t_markup_dict = OrderedDict() # t 时间的标记 + + # 所有K线上指标 + self.main_color_pool = deque(['red', 'green', 'yellow', 'white']) + self.main_indicator_data = {} # 主图指标数据(字典,key是指标,value是list) + self.main_indicator_colors = {} # 主图指标颜色(字典,key是指标,value是list + self.main_indicator_plots = {} # 主图指标的所有画布(字典,key是指标,value是plot) + + self.display_vol = display_vol + self.display_sub = display_sub + + # 所副图上信号图 + self.sub_color_pool = deque(['red', 'green', 'yellow', 'white']) + self.sub_indicator_data = {} + self.sub_indicator_colors = {} + self.sub_indicator_plots = {} + + # 初始化完成 + self.initCompleted = False + + # 调用函数 + self.initUi() + + # 通知上层时间切换点的回调函数 + self.relocate_notify_func = None + + #---------------------------------------------------------------------- + # 初始化相关 + #---------------------------------------------------------------------- + def initUi(self): + """ + 初始化界面 + leyout 如下: + ------------------———————— + \ 主图(K线/主图指标/交易信号 \ + \ \ + ----------------------------------- + \ 副图1(成交量) \ + ----------------------------------- + \ 副图2(持仓量/副图指标) \ + ----------------------------------- + :return: + """ + self.setWindowTitle(u'K线工具') + # 主图 + self.pw = pg.PlotWidget() + # 界面布局 + self.lay_KL = pg.GraphicsLayout(border=(100,100,100)) + #self.lay_KL.setContentsMargins(10, 10, 10, 10) + self.lay_KL.setContentsMargins(5, 5, 5, 5) + self.lay_KL.setSpacing(0) + self.lay_KL.setBorder(color=(100, 100, 100, 250), width=0.4) + self.lay_KL.setZValue(0) + self.KLtitle = self.lay_KL.addLabel(u'') + self.pw.setCentralItem(self.lay_KL) + # 设置横坐标 + xdict = {} + self.axisTime = MyStringAxis(xdict, orientation='bottom') + # 初始化子图 + self.init_plot_main() + self.init_plot_volume() + self.init_plot_sub() + # 注册十字光标 + self.crosshair = Crosshair(self.pw, self) + # 设置界面 + self.vb = QtWidgets.QVBoxLayout() + self.vb.addWidget(self.pw) + self.setLayout(self.vb) + # 初始化完成 + self.initCompleted = True + + # ---------------------------------------------------------------------- + def create_plot_item(self, name): + """生成PlotItem对象""" + vb = CustomViewBox() + plotItem = pg.PlotItem(viewBox = vb, name=name ,axisItems={'bottom': self.axisTime}) + plotItem.setMenuEnabled(False) + plotItem.setClipToView(True) + plotItem.hideAxis('left') + plotItem.showAxis('right') + plotItem.setDownsampling(mode='peak') + plotItem.setRange(xRange = (0,1),yRange = (0,1)) + plotItem.getAxis('right').setWidth(60) + plotItem.getAxis('right').setStyle(tickFont = QtGui.QFont("Roman times",10,QtGui.QFont.Bold)) + plotItem.getAxis('right').setPen(color=(255, 255, 255, 255), width=0.8) + plotItem.showGrid(True, True) + plotItem.hideButtons() + return plotItem + + # ---------------------------------------------------------------------- + def init_plot_main(self): + """ + 初始化主图 + 1、添加 K线(蜡烛图) + :return: + """ + # 创建K线PlotItem + self.pi_main = self.create_plot_item('_'.join([self.windowId, 'Plot_Main'])) + + # 创建蜡烛图 + self.ci_candle = CandlestickItem(self.listBar) + + # 添加蜡烛图到主图 + self.pi_main.addItem(self.ci_candle) + self.pi_main.setMinimumHeight(200) + self.pi_main.setXLink('_'.join([self.windowId, 'Plot_Sub'])) + self.pi_main.hideAxis('bottom') + + # 添加主图到window layout + self.lay_KL.nextRow() + self.lay_KL.addItem(self.pi_main) + + # ---------------------------------------------------------------------- + def init_plot_volume(self): + """ + 初始化成交量副图 + :return: + """ + + # 创建plot item + self.pi_volume = self.create_plot_item('_'.join([self.windowId, 'Plot_Volume'])) + + if self.display_vol: + # 以蜡烛图(柱状图)的形式创建成交量图形对象 + self.ci_volume = CandlestickItem(self.listVol) + + # 副图1,添加成交量子图 + self.pi_volume.addItem(self.ci_volume) + self.pi_volume.setMaximumHeight(150) + self.pi_volume.setXLink('_'.join([self.windowId, 'Plot_Sub'])) + self.pi_volume.hideAxis('bottom') + else: + self.pi_volume.setMaximumHeight(1) + self.pi_volume.setXLink('_'.join([self.windowId, 'Plot_Sub'])) + self.pi_volume.hideAxis('bottom') + + # 添加副图1到window layout + self.lay_KL.nextRow() + self.lay_KL.addItem(self.pi_volume) + + # ---------------------------------------------------------------------- + def init_plot_sub(self): + """ + 初始化副图(只有一个图层) + :return: + """ + self.pi_sub = self.create_plot_item('_'.join([self.windowId, 'Plot_Sub'])) + + if self.display_sub: + # 副图的plot对象 + self.curve_sub = self.pi_sub.plot() + else: + self.pi_sub.setMaximumHeight(1) + self.pi_sub.setXLink('_'.join([self.windowId, 'Plot_Sub'])) + self.pi_sub.hideAxis('bottom') + + # 添加副图到窗体layer中 + self.lay_KL.nextRow() + self.lay_KL.addItem(self.pi_sub) + + #---------------------------------------------------------------------- + # 画图相关 + #---------------------------------------------------------------------- + def plot_volume(self, redraw=False, xmin=0, xmax=-1): + """重画成交量子图""" + if self.initCompleted: + self.ci_volume.generatePicture(self.listVol[xmin:xmax], redraw) # 画成交量子图 + + #---------------------------------------------------------------------- + def plot_kline(self, redraw=False, xmin=0, xmax=-1): + """重画K线子图""" + if self.initCompleted: + self.ci_candle.generatePicture(self.listBar[xmin:xmax], redraw) # 画K线 + + for indicator in list(self.main_indicator_data.keys()): + if indicator in self.main_indicator_plots: + self.main_indicator_plots[indicator].setData(self.main_indicator_data[indicator], + pen=self.main_indicator_colors[indicator][0], + name=indicator) + #---------------------------------------------------------------------- + def plot_sub(self, xmin=0, xmax=-1): + """重画持仓量子图""" + if self.initCompleted: + for indicator in list(self.sub_indicator_data.keys()): + # 调用该信号/指标画布(plotDataItem.setData()),更新数据,更新画笔颜色,更新名称 + if indicator in self.sub_indicator_plots: + self.sub_indicator_plots[indicator].setData(self.sub_indicator_data[indicator], + pen=self.sub_indicator_colors[indicator][0], + name=indicator) + + # ---------------------------------------------------------------------- + def add_indicator(self, indicator, is_main=True): + """ + 新增指标信号图 + :param indicator: 指标/信号的名称,如ma10, + :param is_main: 是否为主图 + :return: + """ + if is_main: + if indicator in self.main_indicator_plots: + self.pi_main.removeItem(self.main_indicator_plots[indicator]) # 存在该指标/信号,先移除原有画布 + + self.main_indicator_plots[indicator] = self.pi_main.plot() # 为该指标/信号,创建新的主图画布,登记字典 + self.main_indicator_colors[indicator] = self.main_color_pool[0] # 登记该指标/信号使用的颜色 + self.main_color_pool.append(self.main_color_pool.popleft()) # 调整剩余颜色 + if indicator not in self.main_indicator_data: + self.main_indicator_data[indicator] = [] + else: + if indicator in self.sub_indicator_plots: + self.pi_sub.removeItem(self.sub_indicator_plots[indicator]) # 若存在该指标/信号,先移除原有的附图画布 + self.sub_indicator_plots[indicator] = self.pi_sub.plot() # 为该指标/信号,创建新的主图画布,登记字典 + self.sub_indicator_colors[indicator] = self.sub_color_pool[0] # 登记该指标/信号使用的颜色 + self.sub_color_pool.append(self.sub_color_pool.popleft()) # 调整剩余颜色 + if indicator not in self.sub_indicator_data: + self.sub_indicator_data[indicator] = [] + + # ---------------------------------------------------------------------- + def plot_indicator(self, datas, is_main=True, clear=False): + """ + 刷新指标/信号图( 新数据) + :param datas: 所有数据 + :param is_main: 是否为主图 + :param clear: 是否要清除旧数据 + :return: + """ + if clear: + self.clear_indicator(is_main) # 清除主图/副图 + + if is_main: + for indicator in datas: + self.add_indicator(indicator, is_main) # 逐一添加主图信号/指标 + self.main_indicator_data[indicator] = datas[indicator] # 更新组件数据字典 + # 调用该信号/指标画布(plotDataItem.setData()),更新数据,更新画笔颜色,更新名称 + self.main_indicator_plots[indicator].setData(datas[indicator], pen=self.main_indicator_colors[indicator][0], name=indicator) + else: + for indicator in datas: + self.add_indicator(indicator, is_main) # 逐一增加子图指标/信号 + self.sub_indicator_data[indicator] = datas[indicator] # 更新组件数据字典 + # 调用该信号/指标画布(plotDataItem.setData()),更新数据,更新画笔颜色,更新名称 + self.sub_indicator_plots[indicator].setData(datas[indicator], pen=self.sub_indicator_colors[indicator][0], name=indicator) + + #---------------------------------------------------------------------- + def update_all(self): + """ + 手动更新所有K线图形,K线播放模式下需要 + """ + datas = self.datas + + + if self.display_vol: + self.ci_volume.pictrue = None + self.ci_volume.update() + + self.ci_candle.pictrue = None + self.ci_candle.update() + + def update(view, low, high): + """ + 更新视图 + :param view: viewbox + :param low: + :param high: + :return: + """ + vRange = view.viewRange() + xmin = max(0,int(vRange[0][0])) + xmax = max(0,int(vRange[0][1])) + xmax = min(xmax,len(datas)) + if len(datas)>0 and xmax > xmin: + ymin = min(datas[xmin:xmax][low]) + ymax = max(datas[xmin:xmax][high]) + view.setRange(yRange = (ymin,ymax)) + else: + view.setRange(yRange = (0,1)) + + update(self.pi_main.getViewBox(), 'low', 'high') + update(self.pi_volume.getViewBox(), 'volume', 'volume') + + #---------------------------------------------------------------------- + def plot_all(self, redraw=True, xMin=0, xMax=-1): + """ + 重画所有界面 + redraw :False=重画最后一根K线; True=重画所有 + xMin,xMax : 数据范围 + """ + + xMax = len(self.datas) if xMax < 0 else xMax + self.countK = xMax-xMin + self.index = int((xMax+xMin)/2) # 设置当前索引所在位置为数据的中心点 + self.pi_sub.setLimits(xMin=xMin, xMax=xMax) + self.pi_main.setLimits(xMin=xMin, xMax=xMax) + self.plot_kline(redraw, xMin, xMax) # K线图 + + if self.display_vol: + self.pi_volume.setLimits(xMin=xMin, xMax=xMax) + self.plot_volume(redraw, xMin, xMax) # K线副图,成交量 + + self.plot_sub(0, len(self.datas)) # K线副图,持仓量 + self.refresh() + + #---------------------------------------------------------------------- + def refresh(self): + """ + 刷新三个子图的显示范围 + """ + datas = self.datas + # 计算界面上显示数量的最小x/最大x + minutes = int(self.countK/2) + xmin = max(0,self.index-minutes) + xmax = xmin+2*minutes + + # 更新主图/副图/成交量的 x范围 + self.pi_sub.setRange(xRange = (xmin, xmax)) + self.pi_main.setRange(xRange = (xmin, xmax)) + self.pi_volume.setRange(xRange = (xmin, xmax)) + + #---------------------------------------------------------------------- + # 快捷键相关 + #---------------------------------------------------------------------- + def onNxt(self): + """跳转到下一个开平仓点""" + try: + if len(self.x_t_trade_map)>0 and self.index is not None: + datalen = len(self.datas) + self.index+=1 + while self.index < datalen and self.index in self.x_t_trade_map: + self.index+=1 + self.refresh() + x = self.index + y = self.datas[x]['close'] + self.crosshair.signal.emit((x,y)) + except Exception as ex: + print(u'{} onDown() exception:{},trace:{}'.format(self.title, str(ex), traceback.format_exc())) + + #---------------------------------------------------------------------- + def onPre(self): + """跳转到上一个开平仓点""" + try: + if len(self.x_t_trade_map)>0 and not self.index is None: + self.index-=1 + while self.index > 0 and self.index in self.x_t_trade_map: + self.index-=1 + self.refresh() + x = self.index + y = self.datas[x]['close'] + self.crosshair.signal.emit((x,y)) + except Exception as ex: + print(u'{}.onDown() exception:{},trace:{}'.format(self.title, str(ex), traceback.format_exc())) + + # ---------------------------------------------------------------------- + def onDown(self): + """放大显示区间""" + try: + self.countK = min(len(self.datas),int(self.countK*1.2)+1) + self.refresh() + if len(self.datas)>0: + x = self.index-self.countK/2+2 if int(self.crosshair.xAxis)self.index+self.countK/2-2 else x + x = int(x) + y = self.datas[x][2] + self.crosshair.signal.emit((x,y)) + print(u'onDown:countK:{},x:{},y:{},index:{}'.format(self.countK,x,y,self.index)) + except Exception as ex: + print(u'{}.onDown() exception:{},trace:{}'.format(self.title, str(ex), traceback.format_exc())) + + # ---------------------------------------------------------------------- + def onUp(self): + """缩小显示区间""" + try: + # 减少界面显示K线数量 + self.countK = max(3,int(self.countK/1.2)-1) + self.refresh() + if len(self.datas) > 0: + x = self.index-int(self.countK/2)+2 if int(self.crosshair.xAxis) self.index+ (self.countK/2)-2 else x + x = int(x) + y = self.datas[x]['close'] + self.crosshair.signal.emit((x,y)) + print(u'onUp:countK:{},x:{},y:{},index:{}'.format(self.countK, x, y, self.index)) + except Exception as ex: + print(u'{}.onDown() exception:{},trace:{}'.format(self.title, str(ex), traceback.format_exc())) + + #---------------------------------------------------------------------- + def onLeft(self): + """向左移动""" + try: + if len(self.datas)>0 and int(self.crosshair.xAxis)>2: + x = int(self.crosshair.xAxis)-1 + y = self.datas[x]['close'] + if x <= self.index-self.countK/2+2 and self.index>1: + self.index -= 1 + self.refresh() + self.crosshair.signal.emit((x,y)) + + print(u'onLeft:countK:{},x:{},y:{},index:{}'.format(self.countK, x, y, self.index)) + except Exception as ex: + print(u'{}.onLeft() exception:{},trace:{}'.format(self.title, str(ex), traceback.format_exc())) + # ---------------------------------------------------------------------- + def onRight(self): + """向右移动""" + try: + if len(self.datas)>0 and int(self.crosshair.xAxis)= self.index+int(self.countK/2)-2: + self.index += 1 + self.refresh() + self.crosshair.signal.emit((x,y)) + except Exception as ex: + print(u'{}.onLeft() exception:{},trace:{}'.format(self.title, str(ex), traceback.format_exc())) + + def onDoubleClick(self, pos): + """ + 鼠标双击事件 + :param pos: + :return: + """ + try: + if len(self.datas) > 0 and int(self.crosshair.xAxis) >= 0: + x = int(self.crosshair.xAxis) + time_value = self.axisTime.xdict.get(x, None) + self.index = x + + print(u'{} doubleclick: {},x:{},index:{}'.format(self.title, time_value, x, self.index)) + + if self.relocate_notify_func is not None and time_value is not None: + self.relocate_notify_func(self.windowId, time_value, self.countK) + except Exception as ex: + print(u'{}.onDoubleClick() exception:{},trace:{}'.format(self.title, str(ex), traceback.format_exc())) + + def relocate(self, window_id, t_value, count_k): + """ + 重定位到最靠近t_value的x坐标 + :param window_id: + :param t_value: + :param count_k + :return: + """ + if self.windowId == window_id or count_k < 2: + return + + try: + x_value = self.axisTime.get_x_by_time(t_value) + self.countK = count_k + + if 0 < x_value <= len(self.datas): + self.index = x_value + x = self.index + y = self.datas[x]['close'] + self.refresh() + self.crosshair.signal.emit((x, y)) + print(u'{} reloacate to :{},{}'.format(self.title, x,y)) + except Exception as ex: + print(u'{}.relocate() exception:{},trace:{}'.format(self.title, str(ex), traceback.format_exc())) + + # ---------------------------------------------------------------------- + + # 界面回调相关 + #---------------------------------------------------------------------- + def onPaint(self): + """界面刷新回调""" + view = self.pi_main.getViewBox() + vRange = view.viewRange() + xmin = max(0,int(vRange[0][0])) + xmax = max(0,int(vRange[0][1])) + self.index = int((xmin+xmax)/2)+1 + + #---------------------------------------------------------------------- + def resignData(self,datas): + """更新数据,用于Y坐标自适应""" + self.crosshair.datas = datas + def viewXRangeChanged(low,high,self): + vRange = self.viewRange() + xmin = max(0,int(vRange[0][0])) + xmax = max(0,int(vRange[0][1])) + xmax = min(xmax,len(datas)) + if len(datas)>0 and xmax > xmin: + ymin = min(datas[xmin:xmax][low]) + ymax = max(datas[xmin:xmax][high]) + self.setRange(yRange = (ymin,ymax)) + else: + self.setRange(yRange = (0,1)) + + view = self.pi_main.getViewBox() + view.sigXRangeChanged.connect(partial(viewXRangeChanged,'low','high')) + + if self.display_vol: + view = self.pi_volume.getViewBox() + view.sigXRangeChanged.connect(partial(viewXRangeChanged,'volume','volume')) + if self.display_sub: + view = self.pi_sub.getViewBox() + #view.sigXRangeChanged.connect(partial(viewXRangeChanged,'openInterest','openInterest')) + view.setRange(yRange = (0,100)) + + #---------------------------------------------------------------------- + # 数据相关 + #---------------------------------------------------------------------- + def clearData(self): + """清空数据""" + # 清空数据,重新画图 + self.time_index = [] + self.listBar = [] + self.listVol = [] + + self.list_trade_arrow = [] + self.x_t_trade_map = OrderedDict() + self.t_trade_dict = OrderedDict() + + self.list_trans = [] + self.list_trans_lines = [] + + self.list_markup = [] + self.x_t_markup_map = OrderedDict() + self.t_markup_dict = OrderedDict() + + # 清空主图指标 + self.main_indicator_data = {} + # 清空副图指标 + self.sub_indicator_data = {} + + self.datas = None + + #---------------------------------------------------------------------- + def clear_indicator(self, main=True): + """清空指标图形""" + # 清空信号图 + if main: + for indicator in self.main_indicator_plots: + self.pi_main.removeItem(self.main_indicator_plots[indicator]) + self.main_indicator_data = {} + self.main_indicator_plots = {} + else: + for indicator in self.sub_indicator_plots: + self.pi_sub.removeItem(self.sub_indicator_plots[indicator]) + self.sub_indicator_data = {} + self.sub_indicator_plots = {} + + + #---------------------------------------------------------------------- + def onBar(self, bar, main_indicator_datas, sub_indicator_datas, nWindow = 20, inited=False): + """ + 新增K线数据,K线播放模式 + + :param bar: VtBarData + :param main_indicator_datas: + :param sub_indicator_datas: + :param nWindow: + :return: nWindow : 最大数据窗口 + """ + # 是否需要更新K线 + newBar = False if len(self.datas)>0 and bar.datetime==self.datas[-1].datetime else True + nrecords = len(self.datas) if newBar else len(self.datas)-1 + bar.openInterest = np.random.randint(0,3) if bar.openInterest==np.inf or bar.openInterest==-np.inf else bar.openInterest + recordVol = (nrecords, bar.volume,0,0,bar.volume) if bar.close < bar.open else (nrecords,0,bar.volume,0,bar.volume) + + if newBar and any(self.datas): + # 主图数据增加一项 + self.datas.resize(nrecords+1, refcheck=0) + self.listBar.resize(nrecords+1, refcheck=0) + # 成交量指标,增加一项 + self.listVol.resize(nrecords+1, refcheck=0) + + # 主图指标,增加一项 + for indicator in list(self.main_indicator_data.keys()): + indicator_data = self.main_indicator_data.get(indicator,[]) + indicator_data.append(0) + + # 副图指标,增加一行 + for indicator in list(self.sub_indicator_data.keys()): + indicator_data = self.sub_indicator_data.get(indicator, []) + indicator_data.append(0) + + elif any(self.datas): + + # 主图指标,移除第一项 + for indicator in list(self.main_indicator_data.keys()): + indicator_data = self.main_indicator_data.get(indicator, []) + indicator_data.pop() + + # 副图指标,移除第一项 + for indicator in list(self.sub_indicator_data.keys()): + indicator_data = self.sub_indicator_data.get(indicator, []) + indicator_data.pop() + + if any(self.datas): + self.datas[-1] = (bar.datetime, bar.open, bar.close, bar.low, bar.high, bar.volume, bar.openInterest) + self.listBar[-1] = (nrecords, bar.open, bar.close, bar.low, bar.high) + self.listVol[-1] = recordVol + + # 主图指标,更新最后记录 + for indicator in list(self.main_indicator_data.keys()): + indicator_data = self.main_indicator_data.get(indicator, []) + indicator_data[-1] = main_indicator_datas.get(indicator, 0) + + # 副图指标,更新最后记录 + for indicator in list(self.sub_indicator_data.keys()): + indicator_data = self.sub_indicator_data.get(indicator, []) + indicator_data[-1] = sub_indicator_datas.get(indicator, 0) + + else: + self.datas = np.rec.array([(datetime, bar.open, bar.close, bar.low, bar.high, bar.volume, bar.openInterest)],\ + names=('datetime', 'open','close','low','high','volume','openInterest')) + self.listBar = np.rec.array([(nrecords, bar.open, bar.close, bar.low, bar.high)],\ + names=('time_int', 'open','close','low','high')) + self.listVol = np.rec.array([recordVol], names=('time_int','open','close','low','high')) + + # 主图指标,添加数据 + for indicator in list(self.main_indicator_data.keys()): + indicator_data = self.main_indicator_data.get(indicator, []) + indicator_data.append(main_indicator_datas.get(indicator, 0)) + + # 副图指标,添加数据 + for indicator in list(self.sub_indicator_data.keys()): + indicator_data = self.sub_indicator_data.get(indicator, []) + indicator_data.append(sub_indicator_datas.get(indicator, 0)) + + self.resignData(self.datas) + + self.axisTime.update_xdict({nrecords:bar.datetime}) + + if 'openInterest' in self.sub_indicator_data: + self.sub_indicator_data['openInterest'].append(bar.openInterest) + + self.resignData(self.datas) + nWindow0 = min(nrecords,nWindow) + xMax = nrecords+2 + xMin = max(0, nrecords-nWindow0) + if inited: + self.plot_all(False, xMin, xMax) + if not newBar: + self.update_all() + self.index = 0 + self.crosshair.signal.emit((None,None)) + + def add_signal(self, t_value, direction, offset, price, volume): + """ + 增加信号 + :param t_value: + :param direction: + :param offset: + :param price: + :param volume: + :return: + """ + # 找到信号时间最贴近的bar x轴 + x = self.axisTime.get_x_by_time(t_value) + need_plot_arrow = False + + # 修正一下 信号时间,改为bar的时间 + if x not in self.x_t_trade_map: + bar_time = self.axisTime.xdict.get(x, t_value) + else: + # 如果存在映射,就更新 + bar_time = self.x_t_trade_map[x] + + trade_node = self.t_trade_dict.get(bar_time, None) + if trade_node is None: + # 当前时间无交易信号 + self.t_trade_dict[bar_time] = {'x': x, 'signals': [{'direction': direction, 'offset':offset,'price': price,'volume': volume}]} + self.x_t_trade_map[x] = bar_time + need_plot_arrow = True + else: + #match_signals = [t for t in trade_node['signals'] if t['direction'] == direction and t['offset'] == offset] + #if len(match_signals) == 0: + need_plot_arrow = True + trade_node['signals'].append({'direction': direction, 'offset':offset, 'price': price, 'volume': volume}) + self.x_t_trade_map[x] = bar_time + + # 需要显示图标 + if need_plot_arrow: + arrow = None + # 多信号 + if direction == DIRECTION_LONG: + if offset == OFFSET_OPEN: + # buy + arrow = pg.ArrowItem(pos=(x, price), angle=135, brush=None, pen={'color':'r','width':1}, tipAngle=30, baseAngle=20, tailLen=10, tailWidth=2) + else: + # cover + arrow = pg.ArrowItem(pos=(x, price), angle=0, brush=(255, 0, 0),pen=None, headLen=20, headWidth=20, tailLen=10, tailWidth=2) + # 空信号 + elif direction == DIRECTION_SHORT: + if offset == OFFSET_CLOSE: + # sell + arrow = pg.ArrowItem(pos=(x, price), angle=0, brush=(0, 255, 0),pen=None ,headLen=20, headWidth=20, tailLen=10, tailWidth=2) + else: + # short + arrow = pg.ArrowItem(pos=(x, price), angle=-135, brush=None, pen={'color':'g','width':1},tipAngle=30, baseAngle=20, tailLen=10, tailWidth=2) + if arrow: + self.pi_main.addItem(arrow) + self.list_trade_arrow.append(arrow) + + def add_signals(self, df_trade_list): + """ + 批量导入交易记录(vnpy回测中导出的trade_list.csv) + :param df_trade_list: + :return: + """ + if df_trade_list is None or len(df_trade_list) ==0: + print(u'dataframe is None or Empty',file=sys.stderr) + return + + for idx in df_trade_list.index: + # 开仓时间 + open_time = df_trade_list['OpenTime'].loc[idx] + if not isinstance(open_time,datetime) and isinstance(open_time,str): + open_time = datetime.strptime(open_time,'%Y-%m-%d %H:%M:%S') + + open_price = df_trade_list['OpenPrice'].loc[idx] + direction = df_trade_list['Direction'].loc[idx] + if direction == 'Long': + open_direction = DIRECTION_LONG + close_direction = DIRECTION_SHORT + else: + open_direction = DIRECTION_SHORT + close_direction = DIRECTION_LONG + + close_time = df_trade_list['CloseTime'].loc[idx] + if not isinstance(close_time, datetime) and isinstance(close_time, str): + close_time = datetime.strptime(close_time, '%Y-%m-%d %H:%M:%S') + + close_price = df_trade_list['ClosePrice'].loc[idx] + volume = df_trade_list['Volume'].loc[idx] + + # 添加开仓信号 + self.add_signal(t_value=open_time, direction=open_direction, offset=OFFSET_OPEN, price=open_price, volume=volume) + + # 添加平仓信号 + self.add_signal(t_value=close_time, direction=close_direction, offset=OFFSET_CLOSE, price=close_price, + volume=volume) + + def add_trans(self, tns_dict): + """ + 添加事务画线 + {'start_time','end_time','tns_type','start_price','end_price','start_x','end_x','completed'} + :return: + """ + if len(self.datas)==0: + print(u'No datas exist',file=sys.stderr) + return + tns = copy.copy(tns_dict) + + completed = tns.get('completed', False) + end_price = tns.get('end_price',0) + if not completed: + end_x = len(self.datas) -1 + end_price = self.datas[end_x]['close'] + tns['end_x'] = end_x + tns['end_price'] = end_price + tns['completed'] = False + else: + tns['end_x'] = self.axisTime.get_x_by_time(tns['end_time']) + + tns['start_x'] = self.axisTime.get_x_by_time(tns['start_time']) + # 将上一个线段设置为True + if len(self.list_trans) > 0: + self.list_trans[-1]['completed'] = True + pos = np.array([[tns['start_x'],tns['start_price']],[tns['end_x'],tns['end_price']]]) + + tns_line = pg.GraphItem(pos=pos, adj=np.array([[0,1]])) + self.pi_main.addItem(tns_line) + self.list_trans.append(tns) + + def add_trans_df(self,df_trans): + """ + 批量增加 + :param df_trans: + :return: + """ + if df_trans is None or len(df_trans) == 0: + print(u'dataframe is None or Empty', file=sys.stderr) + return + + for idx in df_trans.index: + if idx == 0: + continue + # 事务开始时间 + start_time = df_trans['datetime'].loc[idx-1] + if not isinstance(start_time,datetime) and isinstance(start_time,str): + start_time = datetime.strptime(start_time,'%Y-%m-%d %H:%M:%S') + + end_time = df_trans['datetime'].loc[idx] + if not isinstance(end_time, datetime) and isinstance(end_time, str): + end_time = datetime.strptime(end_time, '%Y-%m-%d %H:%M:%S') + + tns_type = df_trans['direction'].loc[idx-1] + tns_type = DIRECTION_LONG if tns_type== 'long' else DIRECTION_SHORT + + start_price = df_trans['price'].loc[idx-1] + end_price = df_trans['price'].loc[idx] + start_x = self.axisTime.get_x_by_time(start_time) + end_x = self.axisTime.get_x_by_time(end_time) + + self.add_trans({'start_time' : start_time, 'end_time' : end_time, + 'start_price': start_price, 'end_price': end_price, + 'start_x' : start_x, 'end_x' : end_x, + 'tns_type' : tns_type, 'completed': True}) + + def add_markup(self, t_value,price, txt): + """ + 添加标记 + :param t_value: 时间-》坐标x + :param price: 坐标y + :param txt: 文字 + :return: + """ + # 找到信号时间最贴近的bar x轴 + x = self.axisTime.get_x_by_time(t_value) + + # 修正一下 标记时间,改为bar的时间 + if x not in self.x_t_markup_map: + bar_time = self.axisTime.xdict.get(x, t_value) + else: + # 如果存在映射,就更新 + bar_time = self.x_t_markup_map[x] + + markup_node = self.t_markup_dict.get(bar_time, None) + if markup_node is None: + # 当前时间无标记 + markup_node = {'x': x, 'markup': [txt]} + self.t_markup_dict[bar_time] = markup_node + self.x_t_markup_map[x] = bar_time + else: + if '.' in txt: + txt_list = txt.split('.') + else: + txt_list = [txt] + + for t in txt_list: + if t in markup_node['markup']: + continue + markup_node['markup'].append(t) + + if 'textitem' in markup_node: + markup_node['textitem'].setText(';'.join(markup_node.get('markup',[]))) + else: + textitem = pg.TextItem(markup_node['markup'][0]) + textitem.setPos(x,price) + markup_node['textitem'] = textitem + self.list_markup.append(textitem) + self.pi_main.addItem(textitem) + + def add_markups(self, df_markup, include_list=[], exclude_list=[]): + """ + 批量增加标记 + :param df_markup: Dataframe(datetime, price, markup) + :param include_list: 如果len(include_list)>0,只显示里面的内容 + :param exclude_list: 如果exclude_list里面存在,不显示 + :return: + """ + if df_markup is None or len(df_markup) == 0: + print(u'df_markup is None or Empty', file=sys.stderr) + return + + for idx in df_markup.index: + # 标记时间 + t_value = df_markup['datetime'].loc[idx] + if not isinstance(t_value, datetime) and isinstance(t_value, str): + t_value = datetime.strptime(t_value, '%Y-%m-%d %H:%M:%S') + + price = df_markup['price'].loc[idx] + markup_text = df_markup['markup'].loc[idx] + if '.' in markup_text: + markup_texts = markup_text.split('.') + else: + markup_texts = [markup_text] + + for txt in markup_texts: + if len(include_list) > 0 and markup_text not in include_list: + continue + + if len(exclude_list) > 0 and markup_text in exclude_list: + continue + + self.add_markup(t_value=t_value, price=price, txt= markup_text) + + #---------------------------------------------------------------------- + def loadData(self, df_datas, main_indicators=[], sub_indicators=[]): + """ + 载入pandas.DataFrame数据 + :param df_datas:DataFrame数据格式,cols : datetime, open, close, low, high, ,,indicator,indicator2,indicator,,, + :param main_indicators: 主图的indicator list + :param sub_indicators: 副图的indicator list + :return: + """ + # 设置中心点时间 + self.index = 0 + # 绑定数据,更新横坐标映射,更新Y轴自适应函数,更新十字光标映射 + df_datas['time_int'] = np.array(range(len(df_datas.index))) + self.datas = df_datas[['open', 'close', 'low', 'high', 'volume', 'openInterest']].to_records() + self.axisTime.xdict={} + xdict = dict(enumerate(df_datas.index.tolist())) + self.axisTime.update_xdict(xdict) + self.resignData(self.datas) + # 更新画图用到的数据 + self.listBar = df_datas[['time_int', 'open', 'close', 'low', 'high']].to_records(False) + + # 成交量颜色和涨跌同步,K线方向由涨跌决定 + datas0 = pd.DataFrame() + datas0['open'] = df_datas.apply(lambda x:0 if x['close'] >= x['open'] else x['volume'], axis=1) + datas0['close'] = df_datas.apply(lambda x:0 if x['close'] < x['open'] else x['volume'], axis=1) + datas0['low'] = 0 + datas0['high'] = df_datas['volume'] + datas0['time_int'] = np.array(range(len(df_datas.index))) + self.listVol = datas0[['time_int', 'open', 'close', 'low', 'high']].to_records(False) + + for indicator in main_indicators: + list_indicator = list(df_datas[indicator]) + self.main_indicator_data[indicator] = list_indicator + for indicator in sub_indicators: + list_indicator = list(df_datas[indicator]) + self.sub_indicator_data[indicator] = list_indicator + + # 调用画图函数 + self.plot_all(redraw=True, xMin=0, xMax=len(self.datas)) + self.crosshair.signal.emit((None,None)) + +######################################################################## +# 功能测试 +######################################################################## +import sys +from vnpy.trader.uiQt import createQApp +if __name__ == '__main__': + app = QtWidgets.QApplication([]) #QtGui.QApplication(sys.argv) + + # 界面设置 + cfgfile = QtCore.QFile('css.qss') + cfgfile.open(QtCore.QFile.ReadOnly) + styleSheet = cfgfile.readAll() + styleSheet = str(styleSheet) + app.setStyleSheet(styleSheet) + + # K线界面 + try: + ui = KLineWidget(display_sub=True) + ui.show() + ui.KLtitle.setText('I99',size='20pt') + ui.add_indicator(indicator='ma5', is_main=True) + ui.add_indicator(indicator='ma10', is_main=True) + ui.add_indicator(indicator='ma18', is_main=True) + ui.add_indicator(indicator='sk',is_main=False) + ui.add_indicator(indicator='sd', is_main=False) + df = pd.read_csv('I99_d.csv') + df = df.set_index(pd.DatetimeIndex(df['datetime'])) + ui.loadData(df, main_indicators=['ma5','ma10','ma18'], sub_indicators=['sk','sd']) + + #df_trade = pd.read_csv('TradeList.csv') + #ui.add_signals(df_trade) + + #df_tns = pd.read_csv('tns.csv') + #ui.add_trans_df(df_tns) + + #df_markup = pd.read_csv('dist.csv') + #df_markup = df_markup[['datetime','price','operation']] + #df_markup.rename(columns={'operation':'markup'},inplace=True) + #ui.add_markups(df_markup=df_markup, exclude_list=['buy','short','sell','cover']) +# + app.exec_() + + except Exception as ex: + print(u'exception:{},trace:{}'.format(str(ex),traceback.format_exc())) diff --git a/vnpy/trader/uiKLine/uiMulti4KLine.py b/vnpy/trader/uiKLine/uiMulti4KLine.py new file mode 100644 index 00000000..7eddb21c --- /dev/null +++ b/vnpy/trader/uiKLine/uiMulti4KLine.py @@ -0,0 +1,157 @@ +# -*- coding: utf-8 -*- +""" +多周期显示K线, +时间点同步 +华富资产/李来佳 +""" + +import sys +import os +import ctypes +import platform +system = platform.system() + +# 将repostory的目录,作为根目录,添加到系统环境中。 +ROOT_PATH = os.path.abspath(os.path.join(os.path.dirname(__file__), '..' , '..')) +sys.path.append(ROOT_PATH) + + +from vnpy.trader.uiKLine.uiCrosshair import Crosshair +from vnpy.trader.uiKLine.uiKLine import * + + +class GridKline(QtWidgets.QWidget): + + def __init__(self, parent=None): + self.parent = parent + super(GridKline, self).__init__(parent) + + self.periods = ['m30', 'h1', 'h2', 'd'] + self.kline_dict = {} + + self.initUI() + + def initUI(self): + gridLayout = QtWidgets.QGridLayout() + + for period_name in self.periods: + canvas = KLineWidget(display_vol=False, display_sub=True) + canvas.show() + canvas.KLtitle.setText('{}'.format(period_name), size='18pt') + canvas.title = '{}'.format(period_name) + canvas.add_indicator(indicator='ma5', is_main=True) + canvas.add_indicator(indicator='ma10', is_main=True) + canvas.add_indicator(indicator='ma18', is_main=True) + canvas.add_indicator(indicator='sk', is_main=False) + canvas.add_indicator(indicator='sd', is_main=False) + self.kline_dict[period_name] = canvas + # 注册重定向事件 + canvas.relocate_notify_func = self.onRelocate + + gridLayout.addWidget(self.kline_dict['m30'], 0, 1) + gridLayout.addWidget(self.kline_dict['h1'], 0, 2) + gridLayout.addWidget(self.kline_dict['h2'], 1, 1) + gridLayout.addWidget(self.kline_dict['d'], 1, 2) + + self.setLayout(gridLayout) + + self.show() + + self.load_multi_kline() + + # ---------------------------------------------------------------------- + def load_multi_kline(self): + """加载多周期窗口""" + + try: + for period_name in self.periods: + canvas = self.kline_dict.get(period_name,None) + if canvas is not None: + df = pd.read_csv('I99_{}.csv'.format(period_name)) + df = df.set_index(pd.DatetimeIndex(df['datetime'])) + canvas.loadData(df, main_indicators=['ma5', 'ma10', 'ma18'], sub_indicators=['sk', 'sd']) + + # 载入 回测引擎生成的成交记录 + trade_list_file = 'TradeList.csv' + if os.path.exists(trade_list_file): + df_trade = pd.read_csv(trade_list_file) + self.kline_dict['h1'].add_signals(df_trade) + + # 载入策略生成的交易事务过程 + tns_file = 'tns.csv' + if os.path.exists(tns_file): + df_tns = pd.read_csv(tns_file) + self.kline_dict['h2'].add_trans_df(df_tns) + self.kline_dict['d'].add_trans_df(df_tns) + + + except Exception as ex: + traceback.print_exc() + QtWidgets.QMessageBox.warning(self, 'Exception', u'Load data Exception', + QtWidgets.QMessageBox.Cancel, + QtWidgets.QMessageBox.NoButton) + + return + + def onRelocate(self,window_id, t_value, count_k): + """ + 重定位所有周期的时间 + :param window_id: + :param t_value: + :return: + """ + for period_name in self.periods: + try: + canvas = self.kline_dict.get(period_name, None) + if canvas is not None: + canvas.relocate(window_id,t_value, count_k) + except Exception as ex: + traceback.print_exc() +######################################################################## +# 功能测试 +######################################################################## + +from vnpy.trader.uiQt import createQApp +from vnpy.trader.vtFunction import loadIconPath + + +def display_multi_grid(): + + qApp = createQApp() + qApp.setWindowIcon(QtGui.QIcon(loadIconPath('dashboard.ico'))) + w = GridKline() + w.showMaximized() + sys.exit(qApp.exec_()) + +if __name__ == '__main__': + +# + ## 界面设置 + #cfgfile = QtCore.QFile('css.qss') + #cfgfile.open(QtCore.QFile.ReadOnly) + #styleSheet = cfgfile.readAll() + #styleSheet = str(styleSheet) + #qApp.setStyleSheet(styleSheet) +# + # K线界面 + try: + #ui = KLineWidget(display_vol=False,display_sub=True) + #ui.show() + #ui.KLtitle.setText('btc(H2)',size='20pt') + #ui.add_indicator(indicator='ma5', is_main=True) + #ui.add_indicator(indicator='ma10', is_main=True) + #ui.add_indicator(indicator='ma18', is_main=True) + #ui.add_indicator(indicator='sk',is_main=False) + #ui.add_indicator(indicator='sd', is_main=False) + #ui.loadData(pd.DataFrame.from_csv('data/btc_h2.csv'), main_indicators=['ma5','ma10','ma18'], sub_indicators=['sk','sd']) + + #ui = MultiKline(parent=app) + #ui.show() + + #app.exec_() + # + + display_multi_grid() + + except Exception as ex: + print(u'exception:{},trace:{}'.format(str(ex), traceback.format_exc())) diff --git a/vnpy/trader/vtFunction.py b/vnpy/trader/vtFunction.py index 7a1b0cbb..ef498ddd 100644 --- a/vnpy/trader/vtFunction.py +++ b/vnpy/trader/vtFunction.py @@ -11,7 +11,23 @@ from datetime import datetime import importlib MAX_NUMBER = 10000000000000 -MAX_DECIMAL = 4 +MAX_DECIMAL = 8 + + +def floatToStr(float_str): + """格式化显示浮点字符串,去除后面的0""" + if '.' in float_str: + llen = len(float_str) + if llen > 0: + for i in range(llen): + vv = llen - i - 1 + if float_str[vv] not in ['.', '0']: + return float_str[:vv + 1] + elif float_str[vv] in ['.']: + return float_str[:vv] + return float_str + else: + return float_str # ---------------------------------------------------------------------- def safeUnicode(value): @@ -25,8 +41,8 @@ def safeUnicode(value): if type(value) is float: d = decimal.Decimal(str(value)) if abs(d.as_tuple().exponent) > MAX_DECIMAL: - value = round(value, ndigits=MAX_DECIMAL) - + value = round(float(value), ndigits=MAX_DECIMAL) + return value #---------------------------------------------------------------------- diff --git a/vnpy/trader/vtObject.py b/vnpy/trader/vtObject.py index 0d981b34..a94266b0 100644 --- a/vnpy/trader/vtObject.py +++ b/vnpy/trader/vtObject.py @@ -312,6 +312,7 @@ class VtOrderReq(object): """Constructor""" self.symbol = EMPTY_STRING # 代码 self.exchange = EMPTY_STRING # 交易所 + self.vtSymbol = EMPTY_STRING # VT合约代码 self.price = EMPTY_FLOAT # 价格 self.volume = EMPTY_FLOAT # 数量 @@ -338,6 +339,7 @@ class VtCancelOrderReq(object): """Constructor""" self.symbol = EMPTY_STRING # 代码 self.exchange = EMPTY_STRING # 交易所 + self.vtSymbol = EMPTY_STRING # VT合约代码 # 以下字段主要和CTP、LTS类接口相关 self.orderID = EMPTY_STRING # 报单号