From 7f76e50501fe0244b3ae7f71002539cd2bd44b79 Mon Sep 17 00:00:00 2001 From: msincenselee Date: Tue, 29 May 2018 11:27:41 +0800 Subject: [PATCH] =?UTF-8?q?=E6=94=AF=E6=8C=81=E6=95=B0=E5=AD=97=E8=B4=A7?= =?UTF-8?q?=E5=B8=81=EF=BC=8C=E5=A2=9E=E5=8A=A0=E5=A4=9A=E5=91=A8=E6=9C=9F?= =?UTF-8?q?=E6=95=B0=E6=8D=AE=E5=B1=95=E7=A4=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- vnpy/data/binance/__init__.py | 0 vnpy/data/binance/binance_data.py | 119 ++ vnpy/data/okex/__init__.py | 0 vnpy/data/okex/okex_data.py | 87 + vnpy/trader/app/ctaStrategy/ctaBacktesting.py | 184 ++- vnpy/trader/app/ctaStrategy/ctaEngine.py | 135 +- vnpy/trader/app/ctaStrategy/ctaLineBar.py | 315 +++- vnpy/trader/app/ctaStrategy/ctaPolicy.py | 684 +++++++- vnpy/trader/app/ctaStrategy/ctaTemplate.py | 30 +- vnpy/trader/uiBasicWidget.py | 157 +- vnpy/trader/uiKLine/.gitignore | 101 ++ vnpy/trader/uiKLine/css.qss | 1342 ++++++++++++++++ vnpy/trader/uiKLine/cta.ico | Bin 0 -> 186094 bytes vnpy/trader/uiKLine/uiCrosshair.py | 298 ++++ vnpy/trader/uiKLine/uiKLine.py | 1402 +++++++++++++++++ vnpy/trader/uiKLine/uiMulti4KLine.py | 157 ++ vnpy/trader/vtFunction.py | 22 +- vnpy/trader/vtObject.py | 2 + 18 files changed, 4798 insertions(+), 237 deletions(-) create mode 100644 vnpy/data/binance/__init__.py create mode 100644 vnpy/data/binance/binance_data.py create mode 100644 vnpy/data/okex/__init__.py create mode 100644 vnpy/data/okex/okex_data.py create mode 100644 vnpy/trader/uiKLine/.gitignore create mode 100644 vnpy/trader/uiKLine/css.qss create mode 100644 vnpy/trader/uiKLine/cta.ico create mode 100644 vnpy/trader/uiKLine/uiCrosshair.py create mode 100644 vnpy/trader/uiKLine/uiKLine.py create mode 100644 vnpy/trader/uiKLine/uiMulti4KLine.py 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 0000000000000000000000000000000000000000..1cc84063b099fa1bbf69cf5f1e31d551293d87fc GIT binary patch literal 186094 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 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 # 报单号