diff --git a/README.md b/README.md index 16e6e637..bb534100 100644 --- a/README.md +++ b/README.md @@ -37,8 +37,11 @@ - 提供单独重启某一策略实例功能,可在线更新策略源码后,重启某一策略实例,不影响其他运行实例。 - 支持单策略多合约行情订阅,支持指数合约行情订阅 - 提供组合回测引擎,能够直接加载cta_strategy_pro_setting.json文件进行组合回测。 + - 拆分组合回测引擎和回测引擎,组合回测引擎支持bar/tick级别的组合回测 - 增加定时器,推动策略on_timer - 增加定时推送策略持仓event + - 增加CtaPro模板,支持精细化策略持久模板, + - 增加CtaPro期货模板,支持FAK委托,自动换月等 8、增强主引擎,包括: diff --git a/requirements.txt b/requirements.txt index 45f44853..df41c201 100644 --- a/requirements.txt +++ b/requirements.txt @@ -5,8 +5,6 @@ qdarkstyle requests websocket-client peewee -pymysql -psycopg2 mongoengine numpy pandas>=0.24.2 @@ -21,3 +19,5 @@ deap pyzmq wmi QScintilla +pytdx +pykalman diff --git a/vnpy/data/renko/test_rebuild_future.py b/tests/renko/test_rebuild_future.py similarity index 73% rename from vnpy/data/renko/test_rebuild_future.py rename to tests/renko/test_rebuild_future.py index 8fbd89ca..2e82856d 100644 --- a/vnpy/data/renko/test_rebuild_future.py +++ b/tests/renko/test_rebuild_future.py @@ -12,7 +12,7 @@ from vnpy.data.renko.rebuild_future import * # Mongo数据库得地址,renko数据库名,tick文件缓存目录 setting = { - "host": "192.168.0.207", + "host": "127.0.0.1", "db_name": FUTURE_RENKO_DB_NAME, "cache_folder": os.path.join(vnpy_root, 'tick_data', 'tdx', 'future') } @@ -20,8 +20,12 @@ builder = FutureRenkoRebuilder(setting) # 生成单个 # builder.start(symbol='RB99',min_diff=1, height=10, start_date='2019-04-01', end_date='2019-09-10') + # 生成多个 -builder.start(symbol='J99', price_tick=0.5, height=[10], start_date='2016-01-01', end_date='2016-02-16') +#builder.start(symbol='J99', price_tick=0.5, height=[10, 'K3'], start_date='2016-01-01', end_date='2016-02-16') + +# 在数据库最新renko基础上开始追加数据 +builder.start(symbol='J99', price_tick=0.5, height=[10, 'K3', 'K5'], start_date='2016-01-01', refill=True) # 导出csv # builder.export(symbol='RB99',height=10, start_date='2019-04-01', end_date='2019-09-10') diff --git a/vnpy/data/renko/test_rebuild_stock.py b/tests/renko/test_rebuild_stock.py similarity index 100% rename from vnpy/data/renko/test_rebuild_stock.py rename to tests/renko/test_rebuild_stock.py diff --git a/vnpy/app/account_recorder/engine.py b/vnpy/app/account_recorder/engine.py index c5809ef2..9c396858 100644 --- a/vnpy/app/account_recorder/engine.py +++ b/vnpy/app/account_recorder/engine.py @@ -14,21 +14,31 @@ ''' -import json -import os import sys import copy import traceback -import csv + from datetime import datetime, timedelta from queue import Queue from threading import Thread from time import time -from concurrent.futures import ThreadPoolExecutor from vnpy.event import Event, EventEngine -from vnpy.trader.event import * -from vnpy.trader.constant import Direction +from vnpy.trader.event import ( + EVENT_TIMER, + EVENT_ACCOUNT, + EVENT_ORDER, + EVENT_TRADE, + EVENT_POSITION, + EVENT_HISTORY_TRADE, + EVENT_HISTORY_ORDER, + EVENT_FUNDS_FLOW, + EVENT_STRATEGY_POS, + EVENT_ERROR, + EVENT_WARNING, + EVENT_CRITICAL, +) +# from vnpy.trader.constant import Direction from vnpy.trader.engine import BaseEngine, MainEngine from vnpy.trader.utility import get_trading_date, load_json, save_json from vnpy.data.mongo.mongo_data import MongoData @@ -246,7 +256,7 @@ class AccountRecorder(BaseEngine): self.save_setting() except Exception as ex: - self.main_engine.writeError(u'更新数据日期异常:{}'.format(str(ex))) + self.main_engine.write_error(u'更新数据日期异常:{}'.format(str(ex))) self.write_log(traceback.format_exc()) def get_begin_day(self, gw_name: str, data_type: str): @@ -393,7 +403,7 @@ class AccountRecorder(BaseEngine): price = self.main_engine.get_price(pos.vt_symbol) if price: data.update({'cur_price': price}) - except: + except: # noqa pass self.update_data(db_name=ACCOUNT_DB_NAME, col_name=TODAY_POSITION_COL, fld=fld, data=data) @@ -561,7 +571,7 @@ class AccountRecorder(BaseEngine): self.write_log(u'运行 {}.{} 更新 耗时:{}ms >200ms,数据:{}' .format(db_name, col_name, execute_ms, d)) - except Exception as ex: + except Exception as ex: # noqa pass # ---------------------------------------------------------------------- diff --git a/vnpy/app/cta_backtester/engine.py b/vnpy/app/cta_backtester/engine.py index d9df495d..48c83e59 100644 --- a/vnpy/app/cta_backtester/engine.py +++ b/vnpy/app/cta_backtester/engine.py @@ -4,7 +4,6 @@ import traceback from datetime import datetime from threading import Thread from pathlib import Path -from inspect import getfile from vnpy.event import Event, EventEngine from vnpy.trader.engine import BaseEngine, MainEngine diff --git a/vnpy/app/cta_strategy_pro/__init__.py b/vnpy/app/cta_strategy_pro/__init__.py index 6cd18448..a6d88f19 100644 --- a/vnpy/app/cta_strategy_pro/__init__.py +++ b/vnpy/app/cta_strategy_pro/__init__.py @@ -1,17 +1,18 @@ from pathlib import Path from vnpy.trader.app import BaseApp -from vnpy.trader.constant import Direction,Offset,Status +from vnpy.trader.constant import Direction,Offset,Status,Color from vnpy.trader.object import TickData, BarData, TradeData, OrderData from vnpy.trader.utility import BarGenerator, ArrayManager from .cta_position import CtaPosition from .cta_line_bar import CtaLineBar, CtaMinuteBar, CtaHourBar, CtaDayBar, CtaWeekBar +from .base import APP_NAME, StopOrder, CtaComponent from .cta_policy import CtaPolicy from .cta_grid_trade import CtaGrid, CtaGridTrade -from .base import APP_NAME, StopOrder + from .engine import CtaEngine -from .template import CtaTemplate, CtaSignal, TargetPosTemplate, CtaProTemplate +from .template import CtaTemplate, CtaSignal, TargetPosTemplate, CtaProTemplate, CtaProFutureTemplate class CtaStrategyProApp(BaseApp): """""" diff --git a/vnpy/app/cta_strategy_pro/back_testing.py b/vnpy/app/cta_strategy_pro/back_testing.py new file mode 100644 index 00000000..e10bb189 --- /dev/null +++ b/vnpy/app/cta_strategy_pro/back_testing.py @@ -0,0 +1,2159 @@ +# encoding: UTF-8 + +''' +本文件中包含的是CTA模块的组合回测引擎,回测引擎的API和CTA引擎一致, +可以使用和实盘相同的代码进行回测。 +华富资产 李来佳 +''' +from __future__ import division + +import sys +import os +import importlib +import csv +import copy +import pandas as pd +import traceback +import numpy as np +import logging + +from collections import OrderedDict, defaultdict +from datetime import datetime, timedelta +from functools import lru_cache +from pathlib import Path + +from .base import ( + EngineType, + STOPORDER_PREFIX, + StopOrder, + StopOrderStatus +) +from .template import CtaTemplate + +from .cta_fund_kline import FundKline + +from vnpy.trader.object import ( + BarData, + TickData, + OrderData, + TradeData, + ContractData +) +from vnpy.trader.constant import ( + Exchange, + Direction, + Offset, + Status, + OrderType, + Product +) +from vnpy.trader.converter import PositionHolding + +from vnpy.trader.utility import ( + get_underlying_symbol, + round_to, + extract_vt_symbol, + format_number, + import_module_by_str +) + +from vnpy.trader.util_logger import setup_logger + + +class BackTestingEngine(object): + """ + CTA回测引擎 + 函数接口和策略引擎保持一样, + 从而实现同一套代码从回测到实盘。 + 针对1分钟bar的回测 + 或者tick级别得回测 + 提供对组合回测/批量回测得服务 + + """ + + def __init__(self, event_engine=None): + """Constructor""" + + # 绑定事件引擎 + self.event_engine = event_engine + + # 引擎类型为回测 + self.engine_type = EngineType.BACKTESTING + + # 回测策略相关 + self.classes = {} # 策略类,class_name: stategy_class + self.class_module_map = {} # 策略类名与模块名映射 class_name: mudule_name + self.strategies = {} # 回测策略实例, key = strategy_name, value= strategy + self.symbol_strategy_map = defaultdict(list) # vt_symbol: strategy list + + self.test_name = 'portfolio_test_{}'.format(datetime.now().strftime('%M%S')) # 回测策略组合的实例名字 + self.daily_report_name = '' # 策略的日净值报告文件名称 + + self.test_start_date = '' # 组合回测启动得日期 + self.init_days = 0 # 初始化天数 + self.test_end_date = '' # 组合回测结束日期 + + self.slippage = {} # 回测时假设的滑点 + self.commission_rate = {} # 回测时假设的佣金比例(适用于百分比佣金) + self.fix_commission = {} # 每手固定手续费 + self.size = {} # 合约大小,默认为1 + self.price_tick = {} # 价格最小变动 + self.margin_rate = {} # 回测合约的保证金比率 + self.price_dict = {} # 登记vt_symbol对应的最新价 + self.contract_dict = {} # 登记vt_symbol得对应合约信息 + self.symbol_exchange_dict = {} # 登记symbol: exchange的对应关系 + + self.data_start_date = None # 回测数据开始日期,datetime对象 (用于截取数据) + self.data_end_date = None # 回测数据结束日期,datetime对象 (用于截取数据) + self.strategy_start_date = None # 策略启动日期(即前面的数据用于初始化),datetime对象 + + self.stop_order_count = 0 # 本地停止单编号 + self.stop_orders = {} # 本地停止单 + self.active_stop_orders = {} # 活动本地停止单 + + self.limit_order_count = 0 # 限价单编号 + self.limit_orders = OrderedDict() # 限价单字典 + self.active_limit_orders = OrderedDict() # 活动限价单字典,用于进行撮合用 + + self.order_strategy_dict = {} # orderid 与 strategy的映射 + + # 持仓缓存字典 + # key为vt_symbol,value为PositionBuffer对象 + self.pos_holding_dict = {} + + self.trade_count = 0 # 成交编号 + self.trade_dict = OrderedDict() # 用于统计成交收益时,还没处理得交易 + self.trades = OrderedDict() # 记录所有得成交记录 + self.trade_pnl_list = [] # 交易记录列表 + + self.long_position_list = [] # 多单持仓 + self.short_position_list = [] # 空单持仓 + + # 当前最新数据,用于模拟成交用 + self.gateway_name = u'BackTest' + + self.last_bar = {} # 最新的bar + self.last_tick = {} # 最新tick + self.last_dt = None # 最新时间 + + # csvFile相关 + self.bar_interval_seconds = 60 # csv文件,属于K线类型,K线的周期(秒数),缺省是1分钟 + + # 费用风控情况 + self.percent = 0.0 + self.percent_limit = 30 # 投资仓位比例上限 + + # 回测计算相关 + self.use_margin = True # 使用保证金模式(期货使用,计算保证金时,按照开仓价计算。股票是按照当前价计算) + + self.init_capital = 1000000 # 期初资金 + self.cur_capital = self.init_capital # 当前资金净值 + self.net_capital = self.init_capital # 实时资金净值(每日根据capital和持仓浮盈计算) + self.max_capital = self.init_capital # 资金最高净值 + self.max_net_capital = self.init_capital + self.avaliable = self.init_capital + + self.max_pnl = 0 # 最高盈利 + self.min_pnl = 0 # 最大亏损 + self.max_occupy_rate = 0 # 最大保证金占比 + self.winning_result = 0 # 盈利次数 + self.losing_result = 0 # 亏损次数 + + self.total_trade_count = 0 # 总成交数量 + self.total_winning = 0 # 总盈利 + self.total_losing = 0 # 总亏损 + self.total_turnover = 0 # 总成交金额(合约面值) + self.total_commission = 0 # 总手续费 + self.total_slippage = 0 # 总滑点 + + self.time_list = [] # 时间序列 + self.pnl_list = [] # 每笔盈亏序列 + self.capital_list = [] # 盈亏汇总的时间序列 + self.drawdown_list = [] # 回撤的时间序列 + self.drawdown_rate_list = [] # 最大回撤比例的时间序列(成交结算) + + self.max_net_capital_time = '' + self.max_drawdown_rate_time = '' + self.daily_max_drawdown_rate = 0 # 按照日结算价计算 + + self.pnl_strategy_dict = {} # 策略实例的平仓盈亏 + + self.is_plot_daily = False + self.daily_list = [] # 按日统计得序列 + self.daily_first_benchmark = None + + self.logger = None + self.strategy_loggers = {} + self.debug = False + + self.is_7x24 = False + self.logs_path = None + self.data_path = None + + self.fund_kline_dict = {} + self.acivte_fund_kline = False + + def create_fund_kline(self, name, use_renko=False): + """ + 创建资金曲线 + :param name: 账号名,或者策略名 + :param use_renko: + :return: + """ + setting = {} + setting.update({'name': name}) + setting['para_ma1_len'] = 5 + setting['para_ma2_len'] = 10 + setting['para_ma3_len'] = 20 + setting['para_active_yb'] = True + setting['price_tick'] = 0.01 + setting['underlying_symbol'] = 'fund' + if use_renko: + # 使用砖图,高度是资金的千分之一 + setting['height'] = self.init_capital * 0.001 + setting['use_renko'] = True + + fund_kline = FundKline(cta_engine=self, setting=setting) + self.fund_kline_dict.update({name: fund_kline}) + return fund_kline + + def get_fund_kline(self, name: str = None): + # 指定资金账号/策略名 + if name: + kline = self.fund_kline_dict.get(name, None) + return kline + + # 没有指定账号,并且存在一个或多个资金K线 + if len(self.fund_kline_dict) > 0: + # 优先找vt_setting中,配置了strategy_groud的资金K线 + kline = self.fund_kline_dict.get(self.test_name, None) + + # 找不到,返回第一个 + if kline is None: + kline = self.fund_kline_dict.values()[0] + return kline + else: + return None + + def get_account(self, vt_accountid: str = ""): + """返回账号的实时权益,可用资金,仓位比例,投资仓位比例上限""" + if self.net_capital == 0.0: + self.percent = 0.0 + + return self.net_capital, self.avaliable, self.percent, self.percent_limit + + def set_test_start_date(self, start_date: str = '20100416', init_days: int = 10): + """设置回测的启动日期""" + self.test_start_date = start_date + self.init_days = init_days + + self.data_start_date = datetime.strptime(start_date, '%Y%m%d') + + # 初始化天数 + init_time_delta = timedelta(init_days) + + self.strategy_start_date = self.data_start_date + init_time_delta + self.write_log(u'设置:回测数据开始日期:{},初始化数据为{}天,策略自动启动日期:{}' + .format(self.data_start_date, self.init_days, self.strategy_start_date)) + + def set_test_end_date(self, end_date: str = ''): + """设置回测的结束日期""" + self.test_end_date = end_date + if end_date: + self.data_end_date = datetime.strptime(end_date, '%Y%m%d') + # 若不修改时间则会导致不包含dataEndDate当天数据 + self.data_end_date.replace(hour=23, minute=59) + else: + self.data_end_date = datetime.now() + self.write_log(u'设置:回测数据结束日期:{}'.format(self.data_end_date)) + + def set_init_capital(self, capital: float): + """设置期初净值""" + self.cur_capital = capital # 资金 + self.net_capital = capital # 实时资金净值(每日根据capital和持仓浮盈计算) + self.max_capital = capital # 资金最高净值 + self.max_net_capital = capital + self.avaliable = capital + self.init_capital = capital + + def set_margin_rate(self, vt_symbol: str, margin_rate: float): + """设置某个合约得保证金比率""" + self.margin_rate.update({vt_symbol: margin_rate}) + + @lru_cache() + def get_margin_rate(self, vt_symbol: str): + return self.margin_rate.get(vt_symbol, 0.1) + + def set_slippage(self, vt_symbol: str, slippage: float): + """设置滑点点数""" + self.slippage.update({vt_symbol: slippage}) + + @lru_cache() + def get_slippage(self, vt_symbol: str): + """获取滑点""" + return self.slippage.get(vt_symbol, 0) + + def set_size(self, vt_symbol: str, size: int): + """设置合约大小""" + self.size.update({vt_symbol: size}) + + @lru_cache() + def get_size(self, vt_symbol: str): + """查询合约的size""" + return self.size.get(vt_symbol, 10) + + def set_price(self, vt_symbol: str, price: float): + self.price_dict.update({vt_symbol: price}) + + def get_price(self, vt_symbol: str): + return self.price_dict.get(vt_symbol, None) + + def set_commission_rate(self, vt_symbol: str, rate: float): + """设置佣金比例""" + self.commission_rate.update({vt_symbol: rate}) + + if rate >= 0.1: + self.fix_commission.update({vt_symbol: rate}) + + def get_commission_rate(self, vt_symbol: str): + """ 获取保证金比例,缺省万分之一""" + return self.commission_rate.get(vt_symbol, float(0.00001)) + + def get_fix_commission(self, vt_symbol: str): + return self.fix_commission.get(vt_symbol, 0) + + def set_price_tick(self, vt_symbol: str, price_tick: float): + """设置价格最小变动""" + self.price_tick.update({vt_symbol: price_tick}) + + def get_price_tick(self, vt_symbol: str): + return self.price_tick.get(vt_symbol, 1) + + def set_contract(self, symbol: str, exchange: Exchange, product: Product, name: str, size: int, price_tick: float): + """设置合约信息""" + vt_symbol = '.'.join([symbol, exchange.value]) + if vt_symbol not in self.contract_dict: + c = ContractData( + gateway_name=self.gateway_name, + symbol=symbol, + exchange=exchange, + name=name, + product=product, + size=size, + pricetick=price_tick + ) + self.contract_dict.update({vt_symbol: c}) + self.set_size(vt_symbol, size) + # self.set_margin_rate(vt_symbol, ) + self.set_price_tick(vt_symbol, price_tick) + self.symbol_exchange_dict.update({symbol: exchange}) + + @lru_cache() + def get_contract(self, vt_symbol): + """获取合约配置信息""" + return self.contract_dict.get(vt_symbol) + + @lru_cache() + def get_exchange(self, symbol: str): + return self.symbol_exchange_dict.get(symbol, Exchange.LOCAL) + + def set_name(self, test_name): + """ + 设置组合的运行实例名称 + :param test_name: + :return: + """ + self.test_name = test_name + + def set_daily_report_name(self, report_file): + """ + 设置策略的日净值记录csv保存文件名(含路径) + :param report_file: 保存文件名(含路径) + :return: + """ + self.daily_report_name = report_file + + def prepare_env(self, test_settings): + """ + 根据配置参数,准备环境 + 包括: + 回测名称 ,是否debug,数据目录/日志目录, + 资金/保证金类型/仓位控制 + 回测开始/结束日期 + :param test_settings: + :return: + """ + self.output('back_testing prepare_env') + if 'name' in test_settings: + self.set_name(test_settings.get('name')) + + self.debug = test_settings.get('debug', False) + + # 更新数据目录 + if 'data_path' in test_settings: + self.data_path = test_settings.get('data_path') + else: + self.data_path = os.path.abspath(os.path.join(os.getcwd(), 'data')) + + print(f'数据输出目录:{self.data_path}') + + # 更新日志目录 + if 'logs_path' in test_settings: + self.logs_path = os.path.abspath(os.path.join(test_settings.get('logs_path'), self.test_name)) + else: + self.logs_path = os.path.abspath(os.path.join(os.getcwd(), 'log', self.test_name)) + print(f'日志输出目录:{self.logs_path}') + + # 创建日志 + self.create_logger(debug=self.debug) + + # 设置资金 + if 'init_capital' in test_settings: + self.write_log(u'设置期初资金:{}'.format(test_settings.get('init_capital'))) + self.set_init_capital(test_settings.get('init_capital')) + + # 缺省使用保证金方式。(期货使用保证金/股票不使用保证金) + self.use_margin = test_settings.get('use_margin', True) + + # 设置最大资金使用比例 + if 'percent_limit' in test_settings: + self.write_log(u'设置最大资金使用比例:{}%'.format(test_settings.get('percent_limit'))) + self.percent_limit = test_settings.get('percent_limit') + + if 'start_date' in test_settings: + if 'strategy_start_date' not in test_settings: + init_days = test_settings.get('init_days', 10) + self.write_log(u'设置回测开始日期:{},数据加载日数:{}'.format(test_settings.get('start_date'), init_days)) + self.set_test_start_date(test_settings.get('start_date'), init_days) + else: + start_date = test_settings.get('start_date') + strategy_start_date = test_settings.get('strategy_start_date') + self.write_log(u'使用指定的数据开始日期:{}和策略启动日期:{}'.format(start_date, strategy_start_date)) + self.test_start_date = start_date + self.data_start_date = datetime.strptime(start_date.replace('-', ''), '%Y%m%d') + self.strategy_start_date = datetime.strptime(strategy_start_date.replace('-', ''), '%Y%m%d') + + if 'end_date' in test_settings: + self.write_log(u'设置回测结束日期:{}'.format(test_settings.get('end_date'))) + self.set_test_end_date(test_settings.get('end_date')) + + # 准备数据 + if 'symbol_datas' in test_settings: + self.write_log(u'准备数据') + self.prepare_data(test_settings.get('symbol_datas')) + + # 设置bar文件的时间间隔秒数 + if 'bar_interval_seconds' in test_settings: + self.write_log(u'设置bar文件的时间间隔秒数:{}'.format(test_settings.get('bar_interval_seconds'))) + self.bar_interval_seconds = test_settings.get('bar_interval_seconds') + + # 资金曲线 + self.acivte_fund_kline = test_settings.get('acivte_fund_kline', False) + if self.acivte_fund_kline: + # 创建资金K线 + self.create_fund_kline(self.test_name, use_renko=test_settings.get('use_renko', False)) + + self.is_plot_daily = test_settings.get('is_plot_daily', False) + + # 加载所有本地策略class + self.load_strategy_class() + + def prepare_data(self, data_dict): + """ + 准备组合数据 + :param data_dict: + :return: + """ + self.output('prepare_data') + + if len(data_dict) == 0: + self.write_log(u'请指定回测数据和文件') + return + + for symbol, symbol_data in data_dict.items(): + self.write_log(u'配置{}数据:{}'.format(symbol, symbol_data)) + self.set_price_tick(symbol, symbol_data.get('price_tick', 1)) + + self.set_slippage(symbol, symbol_data.get('slippage', 0)) + + self.set_size(symbol, symbol_data.get('symbol_size', 10)) + + self.set_margin_rate(symbol, symbol_data.get('margin_rate', 0.1)) + + self.set_commission_rate(symbol, symbol_data.get('commission_rate', float(0.0001))) + + self.set_contract( + symbol=symbol, + name=symbol, + exchange=Exchange(symbol_data.get('exchange', 'LOCAL')), + product=Product(symbol_data.get('product', "期货")), + size=symbol_data.get('symbol_size', 10), + price_tick=symbol_data.get('price_tick', 1) + ) + + def new_tick(self, tick): + """新得tick""" + self.last_tick.update({tick.vt_symbol: tick}) + if self.last_dt is None or (tick.datetime and tick.datetime > self.last_dt): + self.last_dt = tick.datetime + + self.set_price(tick.vt_symbol, tick.last_price) + + self.cross_stop_order(tick=tick) # 撮合停止单 + self.cross_limit_order(tick=tick) # 先撮合限价单 + + # 更新账号级别资金曲线(只有持仓时,才更新) + fund_kline = self.get_fund_kline(self.test_name) + if fund_kline is not None and (len(self.long_position_list) > 0 or len(self.short_position_list) > 0): + fund_kline.update_account(self.last_dt, self.net_capital) + + for strategy in self.symbol_strategy_map.get(tick.vt_symbol, []): + # 更新策略的资金K线 + fund_kline = self.fund_kline_dict.get(strategy.strategy_name, None) + if fund_kline: + hold_pnl = fund_kline.get_hold_pnl() + if hold_pnl != 0: + fund_kline.update_strategy(dt=self.last_dt, hold_pnl=hold_pnl) + + # 推送tick到策略中 + strategy.on_tick(tick) # 推送K线到策略中 + + # 到达策略启动日期,启动策略 + if not strategy.trading and self.strategy_start_date < tick.datetime: + strategy.trading = True + strategy.on_start() + self.output(u'{}策略启动交易'.format(strategy.strategy_name)) + + def new_bar(self, bar): + """新的K线""" + self.last_bar.update({bar.vt_symbol: bar}) + if self.last_dt is None or (bar.datetime and bar.datetime > self.last_dt): + self.last_dt = bar.datetime + self.set_price(bar.vt_symbol, bar.close_price) + self.cross_stop_order(bar=bar) # 撮合停止单 + self.cross_limit_order(bar=bar) # 先撮合限价单 + + # 更新账号的资金曲线(只有持仓时,才更新) + fund_kline = self.get_fund_kline(self.test_name) + if fund_kline is not None and (len(self.long_position_list) > 0 or len(self.short_position_list) > 0): + fund_kline.update_account(self.last_dt, self.net_capital) + + for strategy in self.symbol_strategy_map.get(bar.vt_symbol, []): + # 更新策略的资金K线 + fund_kline = self.fund_kline_dict.get(strategy.strategy_name, None) + if fund_kline: + hold_pnl = fund_kline.get_hold_pnl() + if hold_pnl != 0: + fund_kline.update_strategy(dt=self.last_dt, hold_pnl=hold_pnl) + + # 推送K线到策略中 + strategy.on_bar(bar) # 推送K线到策略中 + + # 到达策略启动日期,启动策略 + if not strategy.trading and self.strategy_start_date < bar.datetime: + strategy.trading = True + strategy.on_start() + self.output(u'{}策略启动交易'.format(strategy.strategy_name)) + + def load_strategy_class(self): + """ + Load strategy class from source code. + """ + self.write_log('加载所有策略class') + # 加载 vnpy/app/cta_strategy_pro/strategies的所有策略 + path1 = Path(__file__).parent.joinpath("strategies") + self.load_strategy_class_from_folder( + path1, "vnpy.app.cta_strategy_pro.strategies") + + def load_strategy_class_from_folder(self, path: Path, module_name: str = ""): + """ + Load strategy class from certain folder. + """ + for dirpath, dirnames, filenames in os.walk(str(path)): + for filename in filenames: + if filename.endswith(".py"): + strategy_module_name = ".".join( + [module_name, filename.replace(".py", "")]) + elif filename.endswith(".pyd"): + strategy_module_name = ".".join( + [module_name, filename.split(".")[0]]) + else: + continue + self.load_strategy_class_from_module(strategy_module_name) + + def load_strategy_class_from_module(self, module_name: str): + """ + Load/Reload strategy class from module file. + """ + try: + module = importlib.import_module(module_name) + + for name in dir(module): + value = getattr(module, name) + if (isinstance(value, type) and issubclass(value, CtaTemplate) and value is not CtaTemplate): + class_name = value.__name__ + if class_name not in self.classes: + self.write_log(f"加载策略类{module_name}.{class_name}") + else: + self.write_log(f"更新策略类{module_name}.{class_name}") + self.classes[class_name] = value + self.class_module_map[class_name] = module_name + return True + except: # noqa + msg = f"策略文件{module_name}加载失败,触发异常:\n{traceback.format_exc()}" + self.write_error(msg) + self.output(msg) + return False + + def load_strategy(self, strategy_name: str, strategy_setting: dict = None): + """ + 装载回测的策略 + setting是参数设置,包括 + class_name: str, 策略类名字 + vt_symbol: str, 缺省合约 + setting: {}, 策略的参数 + auto_init: True/False, 策略是否自动初始化 + auto_start: True/False, 策略是否自动启动 + """ + + # 获取策略的类名 + class_name = strategy_setting.get('class_name', None) + if class_name is None or strategy_name is None: + self.write_error(u'setting中没有class_name') + return + + # strategy_class => module.strategy_class + if '.' not in class_name: + module_name = self.class_module_map.get(class_name, None) + if module_name: + class_name = module_name + '.' + class_name + self.write_log(u'转换策略为全路径:{}'.format(class_name)) + + # 获取策略类的定义 + strategy_class = import_module_by_str(class_name) + if strategy_class is None: + self.write_error(u'加载策略模块失败:{}'.format(class_name)) + return + + # 处理 vt_symbol + vt_symbol = strategy_setting.get('vt_symbol') + if '.' in vt_symbol: + symbol, exchange = extract_vt_symbol(vt_symbol) + else: + symbol = vt_symbol + underly_symbol = get_underlying_symbol(symbol).upper() + exchange = self.get_exchange(f'{underly_symbol}99') + vt_symbol = '.'.join([symbol, exchange.value]) + + # 在期货组合回测,中需要把一般配置的主力合约,更换为指数合约 + if '99' not in symbol and exchange != Exchange.SPD: + underly_symbol = get_underlying_symbol(symbol).upper() + self.write_log(u'更新vt_symbol为指数合约:{}=>{}'.format(vt_symbol, underly_symbol + '99.' + exchange.value)) + vt_symbol = underly_symbol.upper() + '99.' + exchange.value + strategy_setting.update({'vt_symbol': vt_symbol}) + + # 属于自定义套利合约 + if exchange == Exchange.SPD: + symbol_pairs = symbol.split('-') + active_symbol = get_underlying_symbol(symbol_pairs[0]) + passive_symbol = get_underlying_symbol(symbol_pairs[2]) + new_vt_symbol = '-'.join([active_symbol.upper() + '99', + symbol_pairs[1], + passive_symbol.upper() + '99', + symbol_pairs[3], + symbol_pairs[4]]) + '.SPD' + self.write_log(u'更新vt_symbol为指数合约:{}=>{}'.format(vt_symbol, new_vt_symbol)) + vt_symbol = new_vt_symbol + strategy_setting.update({'vt_symbol': vt_symbol}) + + # 取消自动启动 + if 'auto_start' in strategy_setting: + strategy_setting.update({'auto_start': False}) + + # 策略参数设置 + setting = strategy_setting.get('setting', {}) + + # 强制更新回测为True + setting.update({'backtesting': True}) + + # 创建实例 + strategy = strategy_class(self, strategy_name, vt_symbol, setting) + + # 保存到策略实例映射表中 + self.strategies.update({strategy_name: strategy}) + + # 更新vt_symbol合约与策略的订阅关系 + self.subscribe_symbol(strategy_name=strategy_name, vt_symbol=vt_symbol) + + if strategy_setting.get('auto_init', False): + self.write_log(u'自动初始化策略') + strategy.on_init() + + if strategy_setting.get('auto_start', False): + self.write_log(u'自动启动策略') + strategy.on_start() + + if self.acivte_fund_kline: + # 创建策略实例的资金K线 + self.create_fund_kline(name=strategy_name, use_renko=False) + + def subscribe_symbol(self, strategy_name: str, vt_symbol: str, gateway_name: str = '', is_bar: bool = False): + """订阅合约""" + strategy = self.strategies.get(strategy_name, None) + if not strategy: + return False + + # 添加 合约订阅 vt_symbol <=> 策略实例 strategy 映射. + strategies = self.symbol_strategy_map[vt_symbol] + strategies.append(strategy) + return True + + # --------------------------------------------------------------------- + def save_strategy_data(self): + """保存策略数据""" + for strategy in self.strategies.values(): + self.write_log(u'save strategy data') + strategy.save_data() + + def send_order(self, + strategy: CtaTemplate, + vt_symbol: str, + direction: Direction, + offset: Offset, + price: float, + volume: float, + stop: bool, + lock: bool, + order_type: OrderType = OrderType.LIMIT, + gateway_name: str = None): + """发单""" + price_tick = self.get_price_tick(vt_symbol) + price = round_to(price, price_tick) + + if stop: + return self.send_local_stop_order( + strategy=strategy, + vt_symbol=vt_symbol, + direction=direction, + offset=offset, + price=price, + volume=volume, + lock=lock, + gateway_name=gateway_name + ) + else: + return self.send_limit_order( + strategy=strategy, + vt_symbol=vt_symbol, + direction=direction, + offset=offset, + price=price, + volume=volume, + lock=lock, + gateway_name=gateway_name + ) + + def send_limit_order(self, + strategy: CtaTemplate, + vt_symbol: str, + direction: Direction, + offset: Offset, + price: float, + volume: float, + lock: bool, + order_type: OrderType = OrderType.LIMIT, + gateway_name: str = None + ): + self.limit_order_count += 1 + order_id = str(self.limit_order_count) + symbol, exchange = extract_vt_symbol(vt_symbol) + if gateway_name is None: + gateway_name = self.gateway_name + order = OrderData( + gateway_name=gateway_name, + symbol=symbol, + exchange=exchange, + orderid=order_id, + direction=direction, + offset=offset, + type=order_type, + price=round_to(value=price, target=self.get_price_tick(symbol)), + volume=volume, + status=Status.NOTTRADED, + time=str(self.last_dt) + ) + + # 保存到限价单字典中 + self.active_limit_orders[order.vt_orderid] = order + self.limit_orders[order.vt_orderid] = order + self.order_strategy_dict.update({order.vt_orderid: strategy}) + + self.write_log(f'创建限价单:{order.__dict__}') + + return [order.vt_orderid] + + def send_local_stop_order( + self, + strategy: CtaTemplate, + vt_symbol: str, + direction: Direction, + offset: Offset, + price: float, + volume: float, + lock: bool, + gateway_name: str = None): + + """""" + self.stop_order_count += 1 + + stop_order = StopOrder( + vt_symbol=vt_symbol, + direction=direction, + offset=offset, + price=price, + volume=volume, + stop_orderid=f"{STOPORDER_PREFIX}.{self.stop_order_count}", + strategy_name=strategy.strategy_name, + ) + self.write_log(f'创建本地停止单:{stop_order.__dict__}') + self.order_strategy_dict.update({stop_order.stop_orderid: strategy}) + + self.active_stop_orders[stop_order.stop_orderid] = stop_order + self.stop_orders[stop_order.stop_orderid] = stop_order + + return [stop_order.stop_orderid] + + def cancel_order(self, strategy: CtaTemplate, vt_orderid: str): + """撤单""" + if vt_orderid.startswith(STOPORDER_PREFIX): + return self.cancel_stop_order(strategy, vt_orderid) + else: + return self.cancel_limit_order(strategy, vt_orderid) + + def cancel_limit_order(self, strategy: CtaTemplate, vt_orderid: str): + """限价单撤单""" + if vt_orderid in self.active_limit_orders: + order = self.active_limit_orders[vt_orderid] + register_strategy = self.order_strategy_dict.get(vt_orderid, None) + if register_strategy.strategy_name != strategy.strategy_name: + return False + order.status = Status.CANCELLED + order.cancelTime = str(self.last_dt) + self.active_limit_orders.pop(vt_orderid, None) + strategy.on_order(order) + return True + return False + + def cancel_stop_order(self, strategy: CtaTemplate, vt_orderid: str): + """本地停止单撤单""" + if vt_orderid not in self.active_stop_orders: + return False + stop_order = self.active_stop_orders.pop(vt_orderid) + + stop_order.status = StopOrderStatus.CANCELLED + strategy.on_stop_order(stop_order) + return True + + def cancel_all(self, strategy): + """撤销某个策略的所有委托单""" + self.cancel_orders(strategy=strategy) + + def cancel_orders(self, vt_symbol: str = None, offset: Offset = None, strategy: CtaTemplate = None): + """撤销所有单""" + # Symbol参数:指定合约的撤单; + # OFFSET参数:指定Offset的撤单,缺省不填写时,为所有 + # strategy参数: 指定某个策略的单子 + + if len(self.active_limit_orders) > 0: + self.write_log(u'从所有订单中,撤销:开平:{},合约:{},策略:{}' + .format(offset, + vt_symbol if vt_symbol is not None else u'所有', + strategy.strategy_name if strategy else None)) + + for vt_orderid in list(self.active_limit_orders.keys()): + order = self.active_limit_orders.get(vt_orderid, None) + order_strategy = self.order_strategy_dict.get(vt_orderid, None) + if order is None or order_strategy is None: + continue + + if offset is None: + offset_cond = True + else: + offset_cond = order.offset == offset + + if vt_symbol is None: + symbol_cond = True + else: + symbol_cond = order.vt_symbol == vt_symbol + + if strategy is None: + strategy_cond = True + else: + strategy_cond = strategy.strategy_name == order_strategy.strategy_name + + if offset_cond and symbol_cond and strategy_cond: + self.write_log(u'撤销订单:{},{} {}@{}' + .format(vt_orderid, order.direction, order.price, order.volume)) + order.status = Status.CANCELLED + order.cancel_time = str(self.last_dt) + del self.active_limit_orders[vt_orderid] + if strategy: + strategy.on_order(order) + + for stop_orderid in list(self.active_stop_orders.keys()): + order = self.active_stop_orders.get(stop_orderid, None) + order_strategy = self.order_strategy_dict.get(stop_orderid, None) + if order is None or order_strategy is None: + continue + + if offset is None: + offset_cond = True + else: + offset_cond = order.offset == offset + + if vt_symbol is None: + symbol_cond = True + else: + symbol_cond = order.vt_symbol == vt_symbol + + if strategy is None: + strategy_cond = True + else: + strategy_cond = strategy.strategy_name == order_strategy.strategy_name + + if offset_cond and symbol_cond and strategy_cond: + self.write_log(u'撤销本地停止单:{},{} {}@{}' + .format(stop_orderid, order.direction, order.price, order.volume)) + order.status = Status.CANCELLED + order.cancel_time = str(self.last_dt) + self.active_stop_orders.pop(stop_orderid, None) + if strategy: + strategy.on_stop_order(order) + + def cross_stop_order(self, bar: BarData = None, tick: TickData = None): + """ + Cross stop order with last bar/tick data. + """ + vt_symbol = bar.vt_symbol if bar else tick.vt_symbol + + for stop_orderid in list(self.active_stop_orders.keys()): + stop_order = self.active_stop_orders[stop_orderid] + strategy = self.order_strategy_dict.get(stop_orderid, None) + if stop_order.vt_symbol != vt_symbol or stop_order is None or strategy is None: + continue + + # 若买入方向停止单价格高于等于该价格,则会触发 + if bar: + long_cross_price = round_to(value=bar.low_price, target=self.get_price_tick(vt_symbol)) + long_cross_price -= self.get_price_tick(vt_symbol) + # 若卖出方向停止单价格低于等于该价格,则会触发 + short_cross_price = round_to(value=bar.high_price, target=self.get_price_tick(vt_symbol)) + short_cross_price += self.get_price_tick(vt_symbol) + # 在当前时间点前发出的买入委托可能的最优成交价 + long_best_price = round_to(value=bar.open_price, + target=self.get_price_tick(vt_symbol)) + self.get_price_tick(vt_symbol) + + # 在当前时间点前发出的卖出委托可能的最优成交价 + short_best_price = round_to(value=bar.open_price, + target=self.get_price_tick(vt_symbol)) - self.get_price_tick(vt_symbol) + else: + long_cross_price = tick.last_price + short_cross_price = tick.last_price + long_best_price = tick.last_price + short_best_price = tick.last_price + + # Check whether stop order can be triggered. + long_cross = stop_order.direction == Direction.LONG and stop_order.price <= long_cross_price + + short_cross = stop_order.direction == Direction.SHORT and stop_order.price >= short_cross_price + + if not long_cross and not short_cross: + continue + + # Create order data. + self.limit_order_count += 1 + symbol, exchange = extract_vt_symbol(vt_symbol) + order = OrderData( + symbol=symbol, + exchange=exchange, + orderid=str(self.limit_order_count), + direction=stop_order.direction, + offset=stop_order.offset, + price=stop_order.price, + volume=stop_order.volume, + status=Status.ALLTRADED, + gateway_name=self.gateway_name, + ) + order.datetime = self.last_dt + self.write_log(f'停止单被触发:\n{stop_order.__dict__}\n=>委托单{order.__dict__}') + self.limit_orders[order.vt_orderid] = order + + # Create trade data. + if long_cross: + trade_price = max(stop_order.price, long_best_price) + else: + trade_price = min(stop_order.price, short_best_price) + + self.trade_count += 1 + + trade = TradeData( + symbol=order.symbol, + exchange=order.exchange, + orderid=order.orderid, + tradeid=str(self.trade_count), + direction=order.direction, + offset=order.offset, + price=trade_price, + volume=order.volume, + time=self.last_dt.strftime("%Y-%m-%d %H:%M:%S"), + datetime=self.last_dt, + gateway_name=self.gateway_name, + ) + trade.strategy_name = strategy.strategy_name + trade.datetime = self.last_dt + self.write_log(f'停止单触发成交:{trade.__dict__}') + self.trade_dict[trade.vt_tradeid] = trade + self.trades[trade.vt_tradeid] = copy.copy(trade) + + # Update stop order. + stop_order.vt_orderids.append(order.vt_orderid) + stop_order.status = StopOrderStatus.TRIGGERED + + self.active_stop_orders.pop(stop_order.stop_orderid) + + # Push update to strategy. + strategy.on_stop_order(stop_order) + strategy.on_order(order) + self.append_trade(trade) + strategy.on_trade(trade) + + def cross_limit_order(self, bar: BarData = None, tick: TickData = None): + """基于最新数据撮合限价单""" + + vt_symbol = bar.vt_symbol if bar else tick.vt_symbol + + # 遍历限价单字典中的所有限价单 + for vt_orderid in list(self.active_limit_orders.keys()): + order = self.active_limit_orders.get(vt_orderid, None) + if order.vt_symbol != vt_symbol: + continue + + strategy = self.order_strategy_dict.get(order.vt_orderid, None) + if strategy is None: + self.write_error(u'找不到vt_orderid:{}对应的策略'.format(order.vt_orderid)) + continue + if bar: + buy_cross_price = round_to(value=bar.low_price, + target=self.get_price_tick(vt_symbol)) + self.get_price_tick( + vt_symbol) # 若买入方向限价单价格高于该价格,则会成交 + sell_cross_price = round_to(value=bar.high_price, + target=self.get_price_tick(vt_symbol)) - self.get_price_tick( + vt_symbol) # 若卖出方向限价单价格低于该价格,则会成交 + buy_best_cross_price = round_to(value=bar.open_price, + target=self.get_price_tick(vt_symbol)) + self.get_price_tick( + vt_symbol) # 在当前时间点前发出的买入委托可能的最优成交价 + sell_best_cross_price = round_to(value=bar.open_price, + target=self.get_price_tick(vt_symbol)) - self.get_price_tick( + vt_symbol) # 在当前时间点前发出的卖出委托可能的最优成交价 + else: + buy_cross_price = tick.last_price + sell_cross_price = tick.last_price + buy_best_cross_price = tick.last_price + sell_best_cross_price = tick.last_price + + # 判断是否会成交 + buy_cross = order.direction == Direction.LONG and order.price >= buy_cross_price + sell_cross = order.direction == Direction.SHORT and order.price <= sell_cross_price + + # 如果发生了成交 + if buy_cross or sell_cross: + # 推送成交数据 + self.trade_count += 1 # 成交编号自增1 + + trade_id = str(self.trade_count) + symbol, exchange = extract_vt_symbol(vt_symbol) + trade = TradeData( + gateway_name=self.gateway_name, + symbol=symbol, + exchange=exchange, + tradeid=trade_id, + orderid=order.orderid, + direction=order.direction, + offset=order.offset, + volume=order.volume, + time=self.last_dt.strftime("%Y-%m-%d %H:%M:%S"), + datetime=self.last_dt + ) + + # 以买入为例: + # 1. 假设当根K线的OHLC分别为:100, 125, 90, 110 + # 2. 假设在上一根K线结束(也是当前K线开始)的时刻,策略发出的委托为限价105 + # 3. 则在实际中的成交价会是100而不是105,因为委托发出时市场的最优价格是100 + if buy_cross: + trade_price = min(order.price, buy_best_cross_price) + + else: + trade_price = max(order.price, sell_best_cross_price) + trade.price = trade_price + + # 记录该合约来自哪个策略实例 + trade.strategy_name = strategy.strategy_name + + strategy.on_trade(trade) + + for cov_trade in self.convert_spd_trade(trade): + self.trade_dict[cov_trade.vt_tradeid] = cov_trade + self.trades[cov_trade.vt_tradeid] = copy.copy(cov_trade) + self.write_log(u'vt_trade_id:{0}'.format(cov_trade.vt_tradeid)) + + # 更新持仓缓存数据 + pos_buffer = self.pos_holding_dict.get(cov_trade.vt_symbol, None) + if not pos_buffer: + pos_buffer = PositionHolding(self.get_contract(vt_symbol)) + self.pos_holding_dict[cov_trade.vt_symbol] = pos_buffer + pos_buffer.update_trade(cov_trade) + self.write_log(u'{} : crossLimitOrder: TradeId:{}, posBuffer = {}'.format(cov_trade.strategy_name, + cov_trade.tradeid, + pos_buffer.to_str())) + + # 写入交易记录 + self.append_trade(cov_trade) + + # 更新资金曲线 + if 'SPD' not in cov_trade.vt_symbol: + fund_kline = self.get_fund_kline(cov_trade.strategy_name) + if fund_kline: + fund_kline.update_trade(cov_trade) + + # 推送委托数据 + order.traded = order.volume + order.status = Status.ALLTRADED + + strategy.on_order(order) + + # 从字典中删除该限价单 + self.active_limit_orders.pop(vt_orderid, None) + + # 实时计算模式 + self.realtime_calculate() + + def convert_spd_trade(self, trade): + """转换为品种对的交易记录""" + if trade.exchange != Exchange.SPD: + return [trade] + + try: + active_symbol, active_rate, passive_symbol, passive_rate, spd_type = trade.symbol.split('-') + active_rate = int(active_rate) + passive_rate = int(passive_rate) + active_exchange = self.get_exchange(active_symbol) + active_vt_symbol = active_symbol + '.' + active_exchange.value + passive_exchange = self.get_exchange(passive_symbol) + # passive_vt_symbol = active_symbol + '.' + passive_exchange.value + # 主动腿成交记录 + act_trade = TradeData(gateway_name=self.gateway_name, + symbol=active_symbol, + exchange=active_exchange, + orderid='spd_' + str(trade.orderid), + tradeid='spd_act_' + str(trade.tradeid), + direction=trade.direction, + offset=trade.offset, + strategy_name=trade.strategy_name, + price=self.get_price(active_vt_symbol), + volume=int(trade.volume * active_rate), + time=trade.time, + datetime=trade.datetime + ) + + # 被动腿成交记录 + # 交易方向与spd合约方向相反 + pas_trade = TradeData(gateway_name=self.gateway_name, + symbol=passive_symbol, + exchange=passive_exchange, + orderid='spd_' + str(trade.orderid), + tradeid='spd_pas_' + str(trade.tradeid), + direction=Direction.LONG if trade.direction == Direction.SHORT else Direction.SHORT, + offset=trade.offset, + strategy_name=trade.strategy_name, + time=trade.time, + datetime=trade.datetime + ) + + # 根据套利合约的类型+主合约的价格,反向推导出被动合约的价格 + + if spd_type == 'BJ': + pas_trade.price = (act_trade.price * active_rate * 100 / trade.price) / passive_rate + else: + pas_trade.price = (act_trade.price * active_rate - trade.price) / passive_rate + + pas_trade.price = round_to(value=pas_trade.price, target=self.get_price_tick(pas_trade.vt_symbol)) + pas_trade.volume = int(trade.volume * passive_rate) + pas_trade.time = trade.time + + # 返回原交易记录,主动腿交易记录,被动腿交易记录 + return [trade, act_trade, pas_trade] + + except Exception as ex: + self.write_error(u'转换主动/被动腿异常:{}'.format(str(ex))) + return [trade] + + def update_pos_buffer(self): + """更新持仓信息,把今仓=>昨仓""" + + for k, v in self.pos_holding_dict.items(): + if v.long_td > 0: + self.write_log(u'调整多单持仓:今仓{}=> 0 昨仓{} => 昨仓:{}'.format(v.long_td, v.long_yd, v.long_pos)) + v.long_td = 0 + v.longYd = v.long_pos + + if v.short_td > 0: + self.write_log(u'调整空单持仓:今仓{}=> 0 昨仓{} => 昨仓:{}'.format(v.short_td, v.short_yd, v.short_pos)) + v.short_td = 0 + v.short_yd = v.short_pos + + def get_data_path(self): + """ + 获取数据保存目录 + :return: + """ + if self.data_path is not None: + data_folder = self.data_path + else: + data_folder = os.path.abspath(os.path.join(os.getcwd(), 'data')) + self.data_path = data_folder + if not os.path.exists(data_folder): + os.makedirs(data_folder) + return data_folder + + def get_logs_path(self): + """ + 获取日志保存目录 + :return: + """ + if self.logs_path is not None: + logs_folder = self.logs_path + else: + logs_folder = os.path.abspath(os.path.join(os.getcwd(), 'log')) + self.logs_path = logs_folder + + if not os.path.exists(logs_folder): + os.makedirs(logs_folder) + + return logs_folder + + def create_logger(self, strategy_name=None, debug=False): + """ + 创建日志 + :param strategy_name 策略实例名称 + :param debug:是否详细记录日志 + :return: + """ + if strategy_name is None: + filename = os.path.abspath(os.path.join(self.get_logs_path(), '{}'.format( + self.test_name if len(self.test_name) > 0 else 'portfolio_test'))) + print(u'create logger:{}'.format(filename)) + self.logger = setup_logger(file_name=filename, + name=self.test_name, + log_level=logging.DEBUG if debug else logging.ERROR, + backtesing=True) + else: + filename = os.path.abspath( + os.path.join(self.get_logs_path(), '{}_{}'.format(self.test_name, str(strategy_name)))) + print(u'create logger:{}'.format(filename)) + self.strategy_loggers[strategy_name] = setup_logger(file_name=filename, + name=str(strategy_name), + log_level=logging.DEBUG if debug else logging.ERROR, + backtesing=True) + + def write_log(self, msg: str, strategy_name: str = None, level: int = logging.DEBUG): + """记录日志""" + # log = str(self.datetime) + ' ' + content + # self.logList.append(log) + + if strategy_name is None: + # 写入本地log日志 + if self.logger: + self.logger.log(msg=msg, level=level) + else: + self.create_logger(debug=self.debug) + else: + if strategy_name in self.strategy_loggers: + self.strategy_loggers[strategy_name].log(msg=msg, level=level) + else: + self.create_logger(strategy_name=strategy_name, debug=self.debug) + + def write_error(self, msg, strategy_name=None): + """记录异常""" + + if strategy_name is None: + if self.logger: + self.logger.error(msg) + else: + self.create_logger(debug=self.debug) + else: + if strategy_name in self.strategy_loggers: + self.strategy_loggers[strategy_name].error(msg) + else: + self.create_logger(strategy_name=strategy_name, debug=self.debug) + try: + self.strategy_loggers[strategy_name].error(msg) + except Exception as ex: + print('{}'.format(datetime.now()), file=sys.stderr) + print('could not create cta logger for {},excption:{},trace:{}'.format(strategy_name, str(ex), + traceback.format_exc())) + print(msg, file=sys.stderr) + + def output(self, content): + """输出内容""" + print(self.test_name + "\t" + content) + + def realtime_calculate(self): + """实时计算交易结果 + 支持多空仓位并存""" + + if len(self.trade_dict) < 1: + return + + # 获取所有未处理得成交单 + vt_tradeids = list(self.trade_dict.keys()) + + result_list = [] # 保存交易记录 + longid = '' + shortid = '' + + # 对交易记录逐一处理 + for vt_tradeid in vt_tradeids: + + trade = self.trade_dict.pop(vt_tradeid, None) + if trade is None: + continue + + if trade.volume == 0: + continue + # buy trade + if trade.direction == Direction.LONG and trade.offset == Offset.OPEN: + self.write_log(f'{trade.vt_symbol} buy, price:{trade.price},volume:{trade.volume}') + # 放入多单仓位队列 + self.long_position_list.append(trade) + + # cover trade, + elif trade.direction == Direction.LONG and trade.offset == Offset.CLOSE: + g_id = trade.vt_tradeid # 交易组(多个平仓数为一组) + g_result = None # 组合的交易结果 + + cover_volume = trade.volume + self.write_log(f'{trade.vt_symbol} cover:{cover_volume}') + while cover_volume > 0: + # 如果当前没有空单,属于异常行为 + if len(self.short_position_list) == 0: + self.write_error(u'异常!没有空单持仓,不能cover') + raise Exception(u'异常!没有空单持仓,不能cover') + return + + cur_short_pos_list = [s_pos.volume for s_pos in self.short_position_list] + + self.write_log(u'{}当前空单:{}'.format(trade.vt_symbol, cur_short_pos_list)) + + # 来自同一策略,同一合约才能撮合 + pop_indexs = [i for i, val in enumerate(self.short_position_list) if + val.vt_symbol == trade.vt_symbol and val.strategy_name == trade.strategy_name] + + if len(pop_indexs) < 1: + self.write_error(u'异常,{}没有对应symbol:{}的空单持仓'.format(trade.strategy_name, trade.vt_symbol)) + raise Exception(u'realtimeCalculate2() Exception,没有对应symbol:{0}的空单持仓'.format(trade.vt_symbol)) + return + + pop_index = pop_indexs[0] + # 从未平仓的空头交易 + open_trade = self.short_position_list.pop(pop_index) + + # 开空volume,不大于平仓volume + if cover_volume >= open_trade.volume: + self.write_log(f'cover volume:{cover_volume}, 满足:{open_trade.volume}') + cover_volume = cover_volume - open_trade.volume + if cover_volume > 0: + self.write_log(u'剩余待平数量:{}'.format(cover_volume)) + + self.write_log( + f'{open_trade.vt_symbol} coverd, price: {trade.price},volume:{open_trade.volume}') + + result = TradingResult(open_price=open_trade.price, + open_datetime=open_trade.datetime, + exit_price=trade.price, + close_datetime=trade.datetime, + volume=-open_trade.volume, + rate=self.get_commission_rate(trade.vt_symbol), + slippage=self.get_slippage(trade.vt_symbol), + size=self.get_size(trade.vt_symbol), + group_id=g_id, + fix_commission=self.get_fix_commission(trade.vt_symbol)) + + t = OrderedDict() + t['gid'] = g_id + t['strategy'] = open_trade.strategy_name + t['vt_symbol'] = open_trade.vt_symbol + t['open_time'] = open_trade.time + t['open_price'] = open_trade.price + t['direction'] = u'Short' + t['close_time'] = trade.time + t['close_price'] = trade.price + t['volume'] = open_trade.volume + t['profit'] = result.pnl + t['commission'] = result.commission + self.trade_pnl_list.append(t) + + # 非自定义套利对,才更新到策略盈亏 + if not open_trade.vt_symbol.endswith('SPD'): + # 更新策略实例的累加盈亏 + self.pnl_strategy_dict.update( + {open_trade.strategy_name: self.pnl_strategy_dict.get(open_trade.strategy_name, + 0) + result.pnl}) + + msg = u'gid:{} {}[{}:开空tid={}:{}]-[{}.平空tid={},{},vol:{}],净盈亏pnl={},手续费:{}' \ + .format(g_id, open_trade.vt_symbol, open_trade.time, shortid, open_trade.price, + trade.time, vt_tradeid, trade.price, + open_trade.volume, result.pnl, result.commission) + + self.write_log(msg) + result_list.append(result) + + if g_result is None: + if cover_volume > 0: + # 属于组合 + g_result = copy.deepcopy(result) + + else: + # 更新组合的数据 + g_result.turnover = g_result.turnover + result.turnover + g_result.commission = g_result.commission + result.commission + g_result.slippage = g_result.slippage + result.slippage + g_result.pnl = g_result.pnl + result.pnl + + # 所有仓位平完 + if cover_volume == 0: + self.write_log(u'所有平空仓位撮合完毕') + g_result.volume = abs(trade.volume) + + # 开空volume,大于平仓volume,需要更新减少tradeDict的数量。 + else: + remain_volume = open_trade.volume - cover_volume + self.write_log(f'{open_trade.vt_symbol} short pos: {open_trade.volume} => {remain_volume}') + + result = TradingResult(open_price=open_trade.price, + open_datetime=open_trade.datetime, + exit_price=trade.price, + close_datetime=trade.datetime, + volume=-cover_volume, + rate=self.get_commission_rate(trade.vt_symbol), + slippage=self.get_slippage(trade.vt_symbol), + size=self.get_size(trade.vt_symbol), + group_id=g_id, + fix_commission=self.get_fix_commission(trade.vt_symbol)) + + t = OrderedDict() + t['gid'] = g_id + t['strategy'] = open_trade.strategy_name + t['vt_symbol'] = open_trade.vt_symbol + t['open_time'] = open_trade.time + t['open_price'] = open_trade.price + t['direction'] = u'Short' + t['close_time'] = trade.time + t['close_price'] = trade.price + t['volume'] = cover_volume + t['profit'] = result.pnl + t['commission'] = result.commission + self.trade_pnl_list.append(t) + + # 非自定义套利对,才更新盈亏 + if not (open_trade.vt_symbol.endswith('SPD') or open_trade.vt_symbol.endswith('SPD99')): + # 更新策略实例的累加盈亏 + self.pnl_strategy_dict.update( + {open_trade.strategy_name: self.pnl_strategy_dict.get(open_trade.strategy_name, + 0) + result.pnl}) + + msg = u'gid:{} {}[{}:开空tid={}:{}]-[{}.平空tid={},{},vol:{}],净盈亏pnl={},手续费:{}' \ + .format(g_id, open_trade.vt_symbol, open_trade.time, shortid, open_trade.price, + trade.time, vt_tradeid, trade.price, + cover_volume, result.pnl, result.commission) + + self.write_log(msg) + + # 更新(减少)开仓单的volume,重新推进开仓单列表中 + open_trade.volume = remain_volume + self.write_log(u'更新(减少)开仓单的volume,重新推进开仓单列表中:{}'.format(open_trade.volume)) + self.short_position_list.append(open_trade) + cur_short_pos_list = [s_pos.volume for s_pos in self.short_position_list] + self.write_log(u'当前空单:{}'.format(cur_short_pos_list)) + + cover_volume = 0 + result_list.append(result) + + if g_result is not None: + # 更新组合的数据 + g_result.turnover = g_result.turnover + result.turnover + g_result.commission = g_result.commission + result.commission + g_result.slippage = g_result.slippage + result.slippage + g_result.pnl = g_result.pnl + result.pnl + g_result.volume = abs(trade.volume) + + if g_result is not None: + self.write_log(u'组合净盈亏:{0}'.format(g_result.pnl)) + + # Short Trade + elif trade.direction == Direction.SHORT and trade.offset == Offset.OPEN: + self.write_log(f'{trade.vt_symbol}, short: price:{trade.price},volume{trade.volume}') + self.short_position_list.append(trade) + continue + + # sell trade + elif trade.direction == Direction.SHORT and trade.offset == Offset.CLOSE: + g_id = trade.vt_tradeid # 交易组(多个平仓数为一组) + g_result = None # 组合的交易结果 + + sell_volume = trade.volume + + while sell_volume > 0: + if len(self.long_position_list) == 0: + self.write_error(f'异常,没有{trade.vt_symbol}的多仓') + raise RuntimeError(u'realtimeCalculate2() Exception,没有开多单') + return + + pop_indexs = [i for i, val in enumerate(self.long_position_list) if + val.vt_symbol == trade.vt_symbol and val.strategy_name == trade.strategy_name] + if len(pop_indexs) < 1: + self.write_error(f'没有{trade.strategy_name}对应的symbol{trade.vt_symbol}多单数据,') + raise RuntimeError( + f'realtimeCalculate2() Exception,没有对应的symbol{trade.vt_symbol}多单数据,') + return + + cur_long_pos_list = [s_pos.volume for s_pos in self.long_position_list] + + self.write_log(u'{}当前多单:{}'.format(trade.vt_symbol, cur_long_pos_list)) + + pop_index = pop_indexs[0] + open_trade = self.long_position_list.pop(pop_index) + # 开多volume,不大于平仓volume + if sell_volume >= open_trade.volume: + self.write_log(f'{open_trade.vt_symbol},Sell Volume:{sell_volume} 满足:{open_trade.volume}') + sell_volume = sell_volume - open_trade.volume + + self.write_log(f'{open_trade.vt_symbol},sell, price:{trade.price},volume:{open_trade.volume}') + + result = TradingResult(open_price=open_trade.price, + open_datetime=open_trade.datetime, + exit_price=trade.price, + close_datetime=trade.datetime, + volume=open_trade.volume, + rate=self.get_commission_rate(trade.vt_symbol), + slippage=self.get_slippage(trade.vt_symbol), + size=self.get_size(trade.vt_symbol), + group_id=g_id, + fix_commission=self.get_fix_commission(trade.vt_symbol)) + + t = OrderedDict() + t['gid'] = g_id + t['strategy'] = open_trade.strategy_name + t['vt_symbol'] = open_trade.vt_symbol + t['open_time'] = open_trade.time + t['open_price'] = open_trade.price + t['direction'] = u'Long' + t['close_time'] = trade.time + t['close_price'] = trade.price + t['volume'] = open_trade.volume + t['profit'] = result.pnl + t['commission'] = result.commission + self.trade_pnl_list.append(t) + + # 非自定义套利对,才更新盈亏 + if not (open_trade.vt_symbol.endswith('SPD') or open_trade.vt_symbol.endswith('SPD99')): + # 更新策略实例的累加盈亏 + self.pnl_strategy_dict.update( + {open_trade.strategy_name: self.pnl_strategy_dict.get(open_trade.strategy_name, + 0) + result.pnl}) + + msg = u'gid:{} {}[{}:开多tid={}:{}]-[{}.平多tid={},{},vol:{}],净盈亏pnl={},手续费:{}' \ + .format(g_id, open_trade.vt_symbol, + open_trade.time, longid, open_trade.price, + trade.time, vt_tradeid, trade.price, + open_trade.volume, result.pnl, result.commission) + + self.write_log(msg) + result_list.append(result) + + if g_result is None: + if sell_volume > 0: + # 属于组合 + g_result = copy.deepcopy(result) + else: + # 更新组合的数据 + g_result.turnover = g_result.turnover + result.turnover + g_result.commission = g_result.commission + result.commission + g_result.slippage = g_result.slippage + result.slippage + g_result.pnl = g_result.pnl + result.pnl + + if sell_volume == 0: + g_result.volume = abs(trade.volume) + + # 开多volume,大于平仓volume,需要更新减少tradeDict的数量。 + else: + remain_volume = open_trade.volume - sell_volume + self.write_log(f'{open_trade.vt_symbol} short pos: {open_trade.volume} => {remain_volume}') + + result = TradingResult(open_price=open_trade.price, + open_datetime=open_trade.datetime, + exit_price=trade.price, + close_datetime=trade.datetime, + volume=sell_volume, + rate=self.get_commission_rate(trade.vt_symbol), + slippage=self.get_slippage(trade.vt_symbol), + size=self.get_size(trade.vt_symbol), + group_id=g_id, + fix_commission=self.get_fix_commission(trade.vt_symbol)) + + t = OrderedDict() + t['gid'] = g_id + t['strategy'] = open_trade.strategy_name + t['vt_symbol'] = open_trade.vt_symbol + t['open_time'] = open_trade.time + t['open_price'] = open_trade.price + t['direction'] = u'Long' + t['close_time'] = trade.time + t['close_price'] = trade.price + t['volume'] = sell_volume + t['profit'] = result.pnl + t['commission'] = result.commission + self.trade_pnl_list.append(t) + + # 非自定义套利对,才更新盈亏 + if not (open_trade.vt_symbol.endswith('SPD') or open_trade.vt_symbol.endswith('SPD99')): + # 更新策略实例的累加盈亏 + self.pnl_strategy_dict.update( + {open_trade.strategy_name: self.pnl_strategy_dict.get(open_trade.strategy_name, + 0) + result.pnl}) + + msg = u'Gid:{} {}[{}:开多tid={}:{}]-[{}.平多tid={},{},vol:{}],净盈亏pnl={},手续费:{}' \ + .format(g_id, open_trade.vt_symbol, open_trade.time, longid, open_trade.price, + trade.time, vt_tradeid, trade.price, sell_volume, result.pnl, + result.commission) + + self.write_log(msg) + + # 减少开多volume,重新推进多单持仓列表中 + open_trade.volume = remain_volume + self.long_position_list.append(open_trade) + + sell_volume = 0 + result_list.append(result) + + if g_result is not None: + # 更新组合的数据 + g_result.turnover = g_result.turnover + result.turnover + g_result.commission = g_result.commission + result.commission + g_result.slippage = g_result.slippage + result.slippage + g_result.pnl = g_result.pnl + result.pnl + g_result.volume = abs(trade.volume) + + if g_result is not None: + self.write_log(u'组合净盈亏:{0}'.format(g_result.pnl)) + + # 计算仓位比例 + occupy_money = 0.0 # 保证金 + occupy_long_money_dict = {} # 多单保证金,key为合约短号,value为保证金 + occupy_short_money_dict = {} # 空单保证金,key为合约短号,value为保证金 + occupy_underly_symbol_set = set() # 所有合约短号 + + long_pos_dict = {} + short_pos_dict = {} + if len(self.long_position_list) > 0: + for t in self.long_position_list: + # 不计算套利合约的持仓占用保证金 + if t.vt_symbol.endswith('SPD') or t.vt_symbol.endswith('SPD99'): + continue + # 当前持仓的保证金 + if self.use_margin: + cur_occupy_money = t.price * abs(t.volume) * self.get_size(t.vt_symbol) * self.get_margin_rate( + t.vt_symbol) + else: + cur_occupy_money = self.get_price(t.vt_symbol) * abs(t.volume) * self.get_size( + t.vt_symbol) * self.get_margin_rate(t.vt_symbol) + + # 更新该合约短号的累计保证金 + underly_symbol = get_underlying_symbol(t.symbol) + occupy_underly_symbol_set.add(underly_symbol) + occupy_long_money_dict.update( + {underly_symbol: occupy_long_money_dict.get(underly_symbol, 0) + cur_occupy_money}) + + if t.vt_symbol in long_pos_dict: + long_pos_dict[t.vt_symbol] += abs(t.volume) + else: + long_pos_dict[t.vt_symbol] = abs(t.volume) + + if len(self.short_position_list) > 0: + for t in self.short_position_list: + # 不计算套利合约的持仓占用保证金 + if t.vt_symbol.endswith('SPD') or t.vt_symbol.endswith('SPD99'): + continue + # 当前空单保证金 + if self.use_margin: + cur_occupy_money = max(self.get_price(t.vt_symbol), t.price) * abs(t.volume) * self.get_size( + t.vt_symbol) * self.get_margin_rate(t.vt_symbol) + else: + cur_occupy_money = self.get_price(t.vt_symbol) * abs(t.volume) * self.get_size( + t.vt_symbol) * self.get_margin_rate(t.vt_symbol) + + # 该合约短号的累计空单保证金 + underly_symbol = get_underlying_symbol(t.symbol) + occupy_underly_symbol_set.add(underly_symbol) + occupy_short_money_dict.update( + {underly_symbol: occupy_short_money_dict.get(underly_symbol, 0) + cur_occupy_money}) + + if t.vt_symbol in short_pos_dict: + short_pos_dict[t.vt_symbol] += abs(t.volume) + else: + short_pos_dict[t.vt_symbol] = abs(t.volume) + + # 计算多空的保证金累加(对锁的取最大值) + for underly_symbol in occupy_underly_symbol_set: + occupy_money += max(occupy_long_money_dict.get(underly_symbol, 0), + occupy_short_money_dict.get(underly_symbol, 0)) + + # 可用资金 = 当前净值 - 占用保证金 + self.avaliable = self.net_capital - occupy_money + # 当前保证金占比 + self.percent = round(float(occupy_money * 100 / self.net_capital), 2) + # 更新最大保证金占比 + self.max_occupy_rate = max(self.max_occupy_rate, self.percent) + + # 检查是否有平交易 + if len(result_list) == 0: + msg = u'' + if len(self.long_position_list) > 0: + msg += u'持多仓{0},'.format(str(long_pos_dict)) + + if len(self.short_position_list) > 0: + msg += u'持空仓{0},'.format(str(short_pos_dict)) + + msg += u'资金占用:{0},仓位:{1}%%'.format(occupy_money, self.percent) + + self.write_log(msg) + return + + # 对交易结果汇总统计 + for result in result_list: + if result.pnl > 0: + self.winning_result += 1 + self.total_winning += result.pnl + else: + self.losing_result += 1 + self.total_losing += result.pnl + self.cur_capital += result.pnl + self.max_capital = max(self.cur_capital, self.max_capital) + self.net_capital = max(self.net_capital, self.cur_capital) + self.max_net_capital = max(self.net_capital, self.max_net_capital) + # self.maxVolume = max(self.maxVolume, result.volume) + drawdown = self.net_capital - self.max_net_capital + drawdown_rate = round(float(drawdown * 100 / self.max_net_capital), 4) + + self.pnl_list.append(result.pnl) + self.time_list.append(result.close_datetime) + self.capital_list.append(self.cur_capital) + self.drawdown_list.append(drawdown) + self.drawdown_rate_list.append(drawdown_rate) + + self.total_trade_count += 1 + self.total_turnover += result.turnover + self.total_commission += result.commission + self.total_slippage += result.slippage + + msg = u'[gid:{}] {} 交易盈亏:{},交易手续费:{}回撤:{}/{},账号平仓权益:{},持仓权益:{},累计手续费:{}' \ + .format(result.group_id, result.close_datetime, result.pnl, result.commission, drawdown, + drawdown_rate, self.cur_capital, self.net_capital, self.total_commission) + + self.write_log(msg) + + # 重新计算一次avaliable + self.avaliable = self.net_capital - occupy_money + self.percent = round(float(occupy_money * 100 / self.net_capital), 2) + + def saving_daily_data(self, d, c, m, commission, benchmark=0): + """保存每日数据""" + data = {} + data['date'] = d.strftime('%Y/%m/%d') # 日期 + data['capital'] = c # 当前平仓净值 + data['max_capital'] = m # 之前得最高净值 + today_holding_profit = 0 # 持仓浮盈 + long_pos_occupy_money = 0 + short_pos_occupy_money = 0 + strategy_pnl = {} + for strategy in self.strategies.keys(): + strategy_pnl.update({strategy: self.pnl_strategy_dict.get(strategy, 0)}) + + positionMsg = "" + for longpos in self.long_position_list: + # 不计算套利合约的持仓盈亏 + if longpos.vt_symbol.endswith('SPD') or longpos.vt_symbol.endswith('SPD99'): + continue + symbol = longpos.vt_symbol + # 计算持仓浮盈浮亏/占用保证金 + holding_profit = 0 + last_price = self.get_price(symbol) + if last_price is not None: + holding_profit = (last_price - longpos.price) * longpos.volume * self.get_size(symbol) + long_pos_occupy_money += last_price * abs(longpos.volume) * self.get_size( + symbol) * self.get_margin_rate(symbol) + + # 账号的持仓盈亏 + today_holding_profit += holding_profit + + # 计算每个策略实例的持仓盈亏 + strategy_pnl.update({longpos.strategy_name: strategy_pnl.get(longpos.strategy_name, 0) + holding_profit}) + + positionMsg += "{},long,p={},v={},m={};".format(symbol, longpos.price, longpos.volume, holding_profit) + + for shortpos in self.short_position_list: + # 不计算套利合约的持仓盈亏 + if shortpos.vt_symbol.endswith('SPD') or shortpos.vt_symbol.endswith('SPD99'): + continue + symbol = shortpos.vt_symbol + # 计算持仓浮盈浮亏/占用保证金 + holding_profit = 0 + last_price = self.get_price(symbol) + if last_price is not None: + holding_profit = (shortpos.price - last_price) * shortpos.volume * self.get_size(symbol) + short_pos_occupy_money += last_price * abs(shortpos.volume) * self.get_size( + symbol) * self.get_margin_rate(symbol) + + # 账号的持仓盈亏 + today_holding_profit += holding_profit + # 计算每个策略实例的持仓盈亏 + strategy_pnl.update({shortpos.strategy_name: strategy_pnl.get(shortpos.strategy_name, 0) + holding_profit}) + + positionMsg += "{},short,p={},v={},m={};".format(symbol, shortpos.price, shortpos.volume, holding_profit) + + data['net'] = c + today_holding_profit # 当日净值(含持仓盈亏) + data['rate'] = (c + today_holding_profit) / self.init_capital + data['occupy_money'] = max(long_pos_occupy_money, short_pos_occupy_money) + data['occupy_rate'] = data['occupy_money'] / data['capital'] + data['commission'] = commission + + data.update(self.price_dict) + + data.update(strategy_pnl) + + self.daily_list.append(data) + + # 更新每日浮动净值 + self.net_capital = data['net'] + + # 更新最大初次持仓浮盈净值 + if data['net'] > self.max_net_capital: + self.max_net_capital = data['net'] + self.max_net_capital_time = data['date'] + drawdown_rate = round((float(self.max_net_capital - data['net']) * 100) / self.max_net_capital, 4) + if drawdown_rate > self.daily_max_drawdown_rate: + self.daily_max_drawdown_rate = drawdown_rate + self.max_drawdown_rate_time = data['date'] + + self.write_log(u'{}: net={}, capital={} max={} margin={} commission={}, pos: {}' + .format(data['date'], data['net'], c, m, + today_holding_profit, commission, + positionMsg)) + + # --------------------------------------------------------------------- + def export_trade_result(self): + """ + 导出交易结果(开仓-》平仓, 平仓收益) + 导出每日净值结果表 + :return: + """ + if len(self.trade_pnl_list) == 0: + self.write_log('no traded records') + return + + s = self.test_name.replace('&', '') + s = s.replace(' ', '') + trade_list_csv_file = os.path.abspath(os.path.join(self.get_logs_path(), '{}_trade_list.csv'.format(s))) + + self.write_log(u'save trade records to:{}'.format(trade_list_csv_file)) + import csv + csv_write_file = open(trade_list_csv_file, 'w', encoding='utf8', newline='') + + fieldnames = ['gid', 'strategy', 'vt_symbol', 'open_time', 'open_price', 'direction', 'close_time', + 'close_price', + 'volume', 'profit', 'commission'] + + writer = csv.DictWriter(f=csv_write_file, fieldnames=fieldnames, dialect='excel') + writer.writeheader() + + for row in self.trade_pnl_list: + writer.writerow(row) + + # 导出每日净值记录表 + if not self.daily_list: + return + + if self.daily_report_name == '': + daily_csv_file = os.path.abspath(os.path.join(self.get_logs_path(), '{}_daily_list.csv'.format(s))) + else: + daily_csv_file = self.daily_report_name + self.write_log(u'save daily records to:{}'.format(daily_csv_file)) + + csv_write_file2 = open(daily_csv_file, 'w', encoding='utf8', newline='') + fieldnames = ['date', 'capital', 'net', 'max_capital', 'rate', 'commission', 'long_money', 'short_money', + 'occupy_money', 'occupy_rate', 'today_margin_long', 'today_margin_short'] + # 添加合约的每日close价 + fieldnames.extend(sorted(self.price_dict.keys())) + # 添加策略列表 + fieldnames.extend(sorted(self.strategies.keys())) + writer2 = csv.DictWriter(f=csv_write_file2, fieldnames=fieldnames, dialect='excel') + writer2.writeheader() + + for row in self.daily_list: + writer2.writerow(row) + + if self.is_plot_daily: + # 生成净值曲线图片 + df = pd.DataFrame(self.daily_list) + df = df.set_index('date') + from vnpy.trader.utility import display_dual_axis + plot_file = os.path.abspath(os.path.join(self.get_logs_path(), '{}_plot.png'.format(s))) + + # 双坐标输出,左侧坐标是净值(比率),右侧是各策略的实际资金收益曲线 + display_dual_axis(df=df, columns1=['rate'], columns2=list(self.strategies.keys()), image_name=plot_file) + + return + + def get_result(self): + # 返回回测结果 + d = {} + d['init_capital'] = self.init_capital + d['profit'] = self.cur_capital - self.init_capital + d['max_capital'] = self.max_net_capital # 取消原 maxCapital + + if len(self.pnl_list) == 0: + return {}, [], [] + + d['max_pnl'] = max(self.pnl_list) + d['min_pnl'] = min(self.pnl_list) + + d['max_occupy_rate'] = self.max_occupy_rate + d['total_trade_count'] = self.total_trade_count + d['total_turnover'] = self.total_turnover + d['total_commission'] = self.total_commission + d['total_slippage'] = self.total_slippage + d['time_list'] = self.time_list + d['pnl_list'] = self.pnl_list + d['capital_list'] = self.capital_list + d['drawdown_list'] = self.drawdown_list + d['drawdown_rate_list'] = self.drawdown_rate_list # 净值最大回撤率列表 + d['winning_rate'] = round(100 * self.winning_result / len(self.pnl_list), 4) + + average_winning = 0 # 这里把数据都初始化为0 + average_losing = 0 + profit_loss_ratio = 0 + + if self.winning_result: + average_winning = self.total_winning / self.winning_result # 平均每笔盈利 + if self.losing_result: + average_losing = self.total_losing / self.losing_result # 平均每笔亏损 + if average_losing: + profit_loss_ratio = -average_winning / average_losing # 盈亏比 + + d['average_winning'] = average_winning + d['average_losing'] = average_losing + d['profit_loss_ratio'] = profit_loss_ratio + + # 计算Sharp + if not self.daily_list: + return {}, [], [] + + capital_net_list = [] + capital_list = [] + for row in self.daily_list: + capital_net_list.append(row['net']) + capital_list.append(row['capital']) + + capital = pd.Series(capital_net_list) + log_returns = np.log(capital).diff().fillna(0) + sharpe = (log_returns.mean() * 252) / (log_returns.std() * np.sqrt(252)) + d['sharpe'] = sharpe + + return d, capital_net_list, capital_list + + def show_backtesting_result(self): + """显示回测结果""" + + d, daily_net_capital, daily_capital = self.get_result() + + if len(d) == 0: + self.output(u'无交易结果') + return {}, '' + + # 导出交易清单 + self.export_trade_result() + + result_info = OrderedDict() + + # 输出 + self.output('-' * 30) + result_info.update({u'第一笔交易': str(d['time_list'][0])}) + self.output(u'第一笔交易:\t%s' % d['time_list'][0]) + + result_info.update({u'最后一笔交易': str(d['time_list'][-1])}) + self.output(u'最后一笔交易:\t%s' % d['time_list'][-1]) + + result_info.update({u'总交易次数': d['total_trade_count']}) + self.output(u'总交易次数:\t%s' % format_number(d['total_trade_count'])) + + result_info.update({u'期初资金': d['init_capital']}) + self.output(u'期初资金:\t%s' % format_number(d['init_capital'])) + + result_info.update({u'总盈亏': d['profit']}) + self.output(u'总盈亏:\t%s' % format_number(d['profit'])) + + result_info.update({u'资金最高净值': d['max_capital']}) + self.output(u'资金最高净值:\t%s' % format_number(d['max_capital'])) + + result_info.update({u'资金最高净值时间': str(self.max_net_capital_time)}) + self.output(u'资金最高净值时间:\t%s' % self.max_net_capital_time) + + result_info.update({u'每笔最大盈利': d['max_pnl']}) + self.output(u'每笔最大盈利:\t%s' % format_number(d['max_pnl'])) + + result_info.update({u'每笔最大亏损': d['min_pnl']}) + self.output(u'每笔最大亏损:\t%s' % format_number(d['min_pnl'])) + + result_info.update({u'净值最大回撤': min(d['drawdown_list'])}) + self.output(u'净值最大回撤: \t%s' % format_number(min(d['drawdown_list']))) + + result_info.update({u'净值最大回撤率': self.daily_max_drawdown_rate}) + self.output(u'净值最大回撤率: \t%s' % format_number(self.daily_max_drawdown_rate)) + + result_info.update({u'净值最大回撤时间': str(self.max_drawdown_rate_time)}) + self.output(u'净值最大回撤时间:\t%s' % self.max_drawdown_rate_time) + + result_info.update({u'胜率': d['winning_rate']}) + self.output(u'胜率:\t%s' % format_number(d['winning_rate'])) + + result_info.update({u'盈利交易平均值': d['average_winning']}) + self.output(u'盈利交易平均值\t%s' % format_number(d['average_winning'])) + + result_info.update({u'亏损交易平均值': d['average_losing']}) + self.output(u'亏损交易平均值\t%s' % format_number(d['average_losing'])) + + result_info.update({u'盈亏比': d['profit_loss_ratio']}) + self.output(u'盈亏比:\t%s' % format_number(d['profit_loss_ratio'])) + + result_info.update({u'最大资金占比': d['max_occupy_rate']}) + self.output(u'最大资金占比:\t%s' % format_number(d['max_occupy_rate'])) + + result_info.update({u'平均每笔盈利': d['profit'] / d['total_trade_count']}) + self.output(u'平均每笔盈利:\t%s' % format_number(d['profit'] / d['total_trade_count'])) + + result_info.update({u'平均每笔滑点成本': d['total_slippage'] / d['total_trade_count']}) + self.output(u'平均每笔滑点成本:\t%s' % format_number(d['total_slippage'] / d['total_trade_count'])) + + result_info.update({u'平均每笔佣金': d['total_commission'] / d['total_trade_count']}) + self.output(u'平均每笔佣金:\t%s' % format_number(d['total_commission'] / d['total_trade_count'])) + + result_info.update({u'Sharpe Ratio': d['sharpe']}) + self.output(u'Sharpe Ratio:\t%s' % format_number(d['sharpe'])) + + return result_info + + def put_strategy_event(self, strategy: CtaTemplate): + """发送策略更新事件,回测中忽略""" + pass + + def clear_backtesting_result(self): + """清空之前回测的结果""" + # 清空限价单相关 + self.limit_order_count = 0 + self.limit_orders.clear() + self.active_limit_orders.clear() + + # 清空成交相关 + self.trade_count = 0 + self.trade_dict.clear() + self.trades.clear() + self.trade_pnl_list = [] + + def append_trade(self, trade: TradeData): + """ + 根据策略名称,写入 logs\test_name_straetgy_name_trade.csv文件 + :param trade: + :return: + """ + strategy_name = getattr(trade, 'strategy', self.test_name) + trade_fields = ['symbol', 'exchange', 'vt_symbol', 'tradeid', + 'vt_tradeid', 'orderid', 'vt_orderid', + 'direction', + 'offset', 'price', 'volume', 'time'] + + d = OrderedDict() + try: + for k in trade_fields: + if k in ['exchange', 'direction', 'offset']: + d[k] = getattr(trade, k).value + else: + d[k] = getattr(trade, k, '') + + trade_file = os.path.abspath(os.path.join(self.get_logs_path(), '{}_trade.csv'.format(strategy_name))) + self.append_data(file_name=trade_file, dict_data=d) + except Exception as ex: + self.write_error(u'写入交易记录csv出错:{},{}'.format(str(ex), traceback.format_exc())) + + # 保存记录相关 + def append_data(self, file_name: str, dict_data: OrderedDict, field_names: list = None): + """ + 添加数据到csv文件中 + :param file_name: csv的文件全路径 + :param dict_data: OrderedDict + :return: + """ + if field_names is None or field_names == []: + dict_fieldnames = list(dict_data.keys()) + else: + dict_fieldnames = field_names + + try: + if not os.path.exists(file_name): + self.write_log(u'create csv file:{}'.format(file_name)) + with open(file_name, 'a', encoding='utf8', newline='') as csvWriteFile: + writer = csv.DictWriter(f=csvWriteFile, fieldnames=dict_fieldnames, dialect='excel') + self.write_log(u'write csv header:{}'.format(dict_fieldnames)) + writer.writeheader() + writer.writerow(dict_data) + else: + with open(file_name, 'a', encoding='utf8', newline='') as csvWriteFile: + writer = csv.DictWriter(f=csvWriteFile, fieldnames=dict_fieldnames, dialect='excel', + extrasaction='ignore') + writer.writerow(dict_data) + except Exception as ex: + self.write_error(u'append_data exception:{}'.format(str(ex))) + + +######################################################################## +class TradingResult(object): + """每笔交易的结果""" + + def __init__(self, open_price, open_datetime, exit_price, close_datetime, volume, rate, slippage, size, group_id, + fix_commission=0.0): + """Constructor""" + self.open_price = open_price # 开仓价格 + self.exit_price = exit_price # 平仓价格 + + self.open_datetime = open_datetime # 开仓时间datetime + self.close_datetime = close_datetime # 平仓时间 + + self.volume = volume # 交易数量(+/-代表方向) + self.group_id = group_id # 主交易ID(针对多手平仓) + + self.turnover = (self.open_price + self.exit_price) * size * abs(volume) # 成交金额 + if fix_commission > 0: + self.commission = fix_commission * abs(self.volume) + else: + self.commission = abs(self.turnover * rate) # 手续费成本 + self.slippage = slippage * 2 * size * abs(volume) # 滑点成本 + self.pnl = ((self.exit_price - self.open_price) * volume * size + - self.commission - self.slippage) # 净盈亏 diff --git a/vnpy/app/cta_strategy_pro/base.py b/vnpy/app/cta_strategy_pro/base.py index 24cd2b33..96fbc547 100644 --- a/vnpy/app/cta_strategy_pro/base.py +++ b/vnpy/app/cta_strategy_pro/base.py @@ -1,6 +1,7 @@ """ Defines constants and objects used in CtaStrategyPro App. """ +import sys from abc import ABC from dataclasses import dataclass, field from enum import Enum @@ -93,6 +94,7 @@ INTERVAL_DELTA_MAP = { Interval.DAILY: timedelta(days=1), } + class CtaComponent(ABC): """ CTA策略基础组件""" @@ -103,7 +105,6 @@ class CtaComponent(ABC): """ self.strategy = strategy - # ---------------------------------------------------------------------- def write_log(self, content: str): """记录日志""" if self.strategy: @@ -111,11 +112,9 @@ class CtaComponent(ABC): else: print(content) - # ---------------------------------------------------------------------- def write_error(self, content: str, level: int = ERROR): """记录错误日志""" if self.strategy: self.strategy.write_log(msg=content, level=level) else: print(content, file=sys.stderr) - diff --git a/vnpy/app/cta_strategy_pro/cta_grid_trade.py b/vnpy/app/cta_strategy_pro/cta_grid_trade.py index f8d890e2..d857c523 100644 --- a/vnpy/app/cta_strategy_pro/cta_grid_trade.py +++ b/vnpy/app/cta_strategy_pro/cta_grid_trade.py @@ -9,8 +9,6 @@ import traceback from collections import OrderedDict from datetime import datetime -from dataclasses import dataclass, field -from typing import List from vnpy.trader.utility import get_folder_path from vnpy.app.cta_strategy_pro.base import Direction, CtaComponent diff --git a/vnpy/app/cta_strategy_pro/cta_line_bar.py b/vnpy/app/cta_strategy_pro/cta_line_bar.py index ef831bea..593fd4ba 100644 --- a/vnpy/app/cta_strategy_pro/cta_line_bar.py +++ b/vnpy/app/cta_strategy_pro/cta_line_bar.py @@ -12,6 +12,7 @@ import sys import traceback import talib as ta import numpy as np +import csv from collections import OrderedDict from datetime import datetime, timedelta @@ -27,7 +28,7 @@ from vnpy.app.cta_strategy_pro.base import ( MARKET_ZJ) from vnpy.app.cta_strategy_pro.cta_period import CtaPeriod, Period from vnpy.trader.object import BarData, TickData -from vnpy.trader.constant import Interval, Color, Exchange +from vnpy.trader.constant import Interval, Color from vnpy.trader.utility import round_to, get_trading_date, get_underlying_symbol @@ -1062,7 +1063,6 @@ class CtaLineBar(object): self.line_bar.append(self.cur_bar) # 推入到lineBar队列 - # ---------------------------------------------------------------------- def generate_bar(self, tick: TickData): """生成 line Bar """ @@ -1184,7 +1184,6 @@ class CtaLineBar(object): if not endtick: self.last_tick = tick - # ---------------------------------------------------------------------- def __count_pre_high_low(self): """计算 K线的前周期最高和最低""" @@ -1206,7 +1205,6 @@ class CtaLineBar(object): del self.line_pre_low[0] self.line_pre_low.append(preLow) - # ---------------------------------------------------------------------- def __count_sar(self): """计算K线的SAR""" @@ -1331,7 +1329,6 @@ class CtaLineBar(object): if len(self.line_sar) > self.max_hold_bars: del self.line_sar[0] - # ---------------------------------------------------------------------- def __count_ma(self): """计算K线的MA1 和MA2""" @@ -1486,7 +1483,8 @@ class CtaLineBar(object): if self.para_ma1_len > 0: count_len = min(self.bar_len, self.para_ma1_len) if count_len > 0: - close_ma_array = ta.MA(np.append(self.close_array[-count_len:], [self.line_bar[-1].close_price]), count_len) + close_ma_array = ta.MA(np.append(self.close_array[-count_len:], [self.line_bar[-1].close_price]), + count_len) self._rt_ma1 = round(float(close_ma_array[-1]), self.round_n) # 计算斜率 @@ -1497,7 +1495,8 @@ class CtaLineBar(object): if self.para_ma2_len > 0: count_len = min(self.bar_len, self.para_ma2_len) if count_len > 0: - close_ma_array = ta.MA(np.append(self.close_array[-count_len:], [self.line_bar[-1].close_price]), count_len) + close_ma_array = ta.MA(np.append(self.close_array[-count_len:], [self.line_bar[-1].close_price]), + count_len) self._rt_ma2 = round(float(close_ma_array[-1]), self.round_n) # 计算斜率 @@ -1508,7 +1507,8 @@ class CtaLineBar(object): if self.para_ma3_len > 0: count_len = min(self.bar_len, self.para_ma3_len) if count_len > 0: - close_ma_array = ta.MA(np.append(self.close_array[-count_len:], [self.line_bar[-1].close_price]), count_len) + close_ma_array = ta.MA(np.append(self.close_array[-count_len:], [self.line_bar[-1].close_price]), + count_len) self._rt_ma3 = round(float(close_ma_array[-1]), self.round_n) # 计算斜率 @@ -1558,7 +1558,6 @@ class CtaLineBar(object): return self.line_ma3_atan[-1] return self._rt_ma3_atan - # ---------------------------------------------------------------------- def __count_ema(self): """计算K线的EMA1 和EMA2""" @@ -1613,7 +1612,6 @@ class CtaLineBar(object): del self.line_ema3[0] self.line_ema3.append(barEma3) - # ---------------------------------------------------------------------- def rt_count_ema(self): """计算K线的EMA1 和EMA2""" @@ -1827,7 +1825,7 @@ class CtaLineBar(object): if self.para_atr1_len > 0: count_len = min(self.bar_len, self.para_atr1_len) cur_atr1 = ta.ATR(self.high_array[-count_len * 2:], self.low_array[-count_len * 2:], - self.close_array[-count_len * 2:], count_len) + self.close_array[-count_len * 2:], count_len) self.cur_atr1 = round(cur_atr1[-1], self.round_n) if len(self.line_atr1) > self.max_hold_bars: del self.line_atr1[0] @@ -1836,7 +1834,7 @@ class CtaLineBar(object): if self.para_atr2_len > 0: count_len = min(self.bar_len, self.para_atr2_len) cur_atr2 = ta.ATR(self.high_array[-count_len * 2:], self.low_array[-count_len * 2:], - self.close_array[-count_len * 2:], count_len) + self.close_array[-count_len * 2:], count_len) self.cur_atr2 = round(cur_atr2[-1], self.round_n) if len(self.line_atr2) > self.max_hold_bars: del self.line_atr2[0] @@ -1844,8 +1842,8 @@ class CtaLineBar(object): if self.para_atr3_len > 0: count_len = min(self.bar_len, self.para_atr3_len) - cur_atr3 = ta.ATR(self.high_array[-count_len * 2 :], self.low_array[-count_len * 2:], - self.close_array[-count_len * 2:], count_len) + cur_atr3 = ta.ATR(self.high_array[-count_len * 2:], self.low_array[-count_len * 2:], + self.close_array[-count_len * 2:], count_len) self.cur_atr3 = round(cur_atr3[-1], self.round_n) if len(self.line_atr3) > self.max_hold_bars: @@ -1853,7 +1851,6 @@ class CtaLineBar(object): self.line_atr3.append(self.cur_atr3) - # ---------------------------------------------------------------------- def __count_vol_ma(self): """计算平均成交量""" @@ -1869,7 +1866,6 @@ class CtaLineBar(object): del self.line_vol_ma[0] self.line_vol_ma.append(avgVol) - # ---------------------------------------------------------------------- def __count_rsi(self): """计算K线的RSI""" if self.para_rsi1_len <= 0 and self.para_rsi2_len <= 0: @@ -3943,7 +3939,6 @@ class CtaLineBar(object): return self.line_bias3[-1] return self._rt_bias3 - # ---------------------------------------------------------------------- def write_log(self, content): """记录CTA日志""" self.strategy.write_log(u'[' + self.name + u']' + content) @@ -4586,7 +4581,6 @@ class CtaMinuteBar(CtaLineBar): # 实时计算 self.rt_executed = False - # ---------------------------------------------------------------------- def generate_bar(self, tick): """ 生成 line Bar @@ -4821,8 +4815,6 @@ class CtaHourBar(CtaLineBar): # 实时计算 self.rt_executed = False - # ---------------------------------------------------------------------- - def generate_bar(self, tick): """ 生成 line Bar @@ -5047,7 +5039,6 @@ class CtaDayBar(CtaLineBar): # 实时计算 self.rt_executed = False - # ---------------------------------------------------------------------- def generate_bar(self, tick): """ 生成 line Bar @@ -5280,7 +5271,6 @@ class CtaWeekBar(CtaLineBar): '%Y-%m-%d %H:%M:%S') return friday_night_dt - # ---------------------------------------------------------------------- def generate_bar(self, tick): """ 生成 line Bar @@ -5338,4 +5328,3 @@ class CtaWeekBar(CtaLineBar): self.rt_executed = False self.last_tick = tick - diff --git a/vnpy/app/cta_strategy_pro/cta_policy.py b/vnpy/app/cta_strategy_pro/cta_policy.py index a9e0749d..94992c6c 100644 --- a/vnpy/app/cta_strategy_pro/cta_policy.py +++ b/vnpy/app/cta_strategy_pro/cta_policy.py @@ -7,6 +7,11 @@ from collections import OrderedDict from vnpy.app.cta_strategy_pro.base import CtaComponent from vnpy.trader.utility import get_folder_path +TNS_STATUS_OBSERVATE = 'observate' +TNS_STATUS_ORDERING = 'ordering' +TNS_STATUS_OPENED = 'opened' +TNS_STATUS_CLOSED = 'closed' + class CtaPolicy(CtaComponent): """ @@ -18,7 +23,7 @@ class CtaPolicy(CtaComponent): 构造 :param strategy: """ - super(CtaPolicy,self).__init__(strategy=strategy, kwargs=kwargs) + super().__init__(strategy=strategy, kwargs=kwargs) self.create_time = None self.save_time = None @@ -67,7 +72,8 @@ class CtaPolicy(CtaComponent): 从持久化文件中获取 :return: """ - json_file = os.path.abspath(os.path.join(get_folder_path('data'), u'{}_Policy.json'.format(self.strategy.strategy_name))) + json_file = os.path.abspath( + os.path.join(get_folder_path('data'), u'{}_Policy.json'.format(self.strategy.strategy_name))) json_data = {} if os.path.exists(json_file): diff --git a/vnpy/app/cta_strategy_pro/cta_position.py b/vnpy/app/cta_strategy_pro/cta_position.py index b38cc2ca..00a3d588 100644 --- a/vnpy/app/cta_strategy_pro/cta_position.py +++ b/vnpy/app/cta_strategy_pro/cta_position.py @@ -15,16 +15,16 @@ class CtaPosition(CtaComponent): def __init__(self, strategy, **kwargs): super(CtaPosition, self).__init__(strategy=strategy, kwargs=kwargs) - self.long_pos = 0 # 多仓持仓(正数) - self.short_pos = 0 # 空仓持仓(负数) - self.pos = 0 # 持仓状态 0:空仓/对空平等; >=1 净多仓 ;<=-1 净空仓 - self.maxPos = sys.maxsize # 最大持仓量(多仓+空仓总量) + self.long_pos = 0 # 多仓持仓(正数) + self.short_pos = 0 # 空仓持仓(负数) + self.pos = 0 # 持仓状态 0:空仓/对空平等; >=1 净多仓 ;<=-1 净空仓 + self.maxPos = sys.maxsize # 最大持仓量(多仓+空仓总量) def open_pos(self, direction: Direction, volume: float): """开、加仓""" # volume: 正整数 - if direction == Direction.LONG: # 加多仓 + if direction == Direction.LONG: # 加多仓 if (max(self.pos, self.long_pos) + volume) > self.maxPos: self.write_error(content=f'开仓异常,净:{self.pos},多:{self.long_pos},加多:{volume},超过:{self.maxPos}') @@ -34,7 +34,7 @@ class CtaPosition(CtaComponent): self.long_pos += volume self.pos += volume - if direction == Direction.SHORT: # 加空仓 + if direction == Direction.SHORT: # 加空仓 if (min(self.pos, self.short_pos) - volume) < (0 - self.maxPos): self.write_error(content=f'开仓异常,净:{self.pos},空:{self.short_pos},加空:{volume},超过:{self.maxPos}') @@ -45,11 +45,11 @@ class CtaPosition(CtaComponent): return True - def close_pos(self, direction: Direction, volume:float): + def close_pos(self, direction: Direction, volume: float): """平、减仓""" # vol: 正整数 - if direction == Direction.LONG: # 平空仓 Cover + if direction == Direction.LONG: # 平空仓 Cover if self.short_pos + volume > 0: self.write_error(u'平仓异常,超出仓位。净:{0},空:{1},平仓:{2}'.format(self.pos, self.short_pos, volume)) @@ -61,7 +61,7 @@ class CtaPosition(CtaComponent): # 更新上层策略的pos。该方法不推荐使用 self.strategy.pos = self.pos - if direction == Direction.SHORT: # 平多仓 + if direction == Direction.SHORT: # 平多仓 if self.long_pos - volume < 0: self.write_error(u'平仓异常,超出仓位。净:{0},多:{1},平仓:{2}'.format(self.pos, self.long_pos, volume)) diff --git a/vnpy/app/cta_strategy_pro/engine.py b/vnpy/app/cta_strategy_pro/engine.py index 345f8af4..b0c0fc0b 100644 --- a/vnpy/app/cta_strategy_pro/engine.py +++ b/vnpy/app/cta_strategy_pro/engine.py @@ -1,14 +1,13 @@ """""" import importlib -import csv import os import sys import traceback from collections import defaultdict from pathlib import Path from typing import Any, Callable -from datetime import datetime, timedelta +from datetime import datetime from collections import OrderedDict from concurrent.futures import ThreadPoolExecutor from copy import copy @@ -19,10 +18,8 @@ from vnpy.trader.engine import BaseEngine, MainEngine from vnpy.trader.object import ( OrderRequest, SubscribeRequest, - HistoryRequest, LogData, TickData, - BarData, ContractData ) from vnpy.trader.event import ( @@ -37,8 +34,6 @@ from vnpy.trader.event import ( from vnpy.trader.constant import ( Direction, OrderType, - Interval, - Exchange, Offset, Status ) @@ -359,12 +354,8 @@ class CtaEngine(BaseEngine): if stop_order.vt_symbol != tick.vt_symbol: continue - long_triggered = ( - stop_order.direction == Direction.LONG and tick.last_price >= stop_order.price - ) - short_triggered = ( - stop_order.direction == Direction.SHORT and tick.last_price <= stop_order.price - ) + long_triggered = stop_order.direction == Direction.LONG and tick.last_price >= stop_order.price + short_triggered = stop_order.direction == Direction.SHORT and tick.last_price <= stop_order.price if long_triggered or short_triggered: strategy = self.strategies[stop_order.strategy_name] @@ -1340,7 +1331,7 @@ class CtaEngine(BaseEngine): pos_list.append(pos) except Exception as ex: - self.write_error(u'分解SPD失败') + self.write_error(f'分解SPD失败:{str(ex)}') # update local pos dict self.strategy_pos_dict.update({name: pos_list}) @@ -1439,16 +1430,21 @@ class CtaEngine(BaseEngine): Update setting file. """ strategy = self.strategies[strategy_name] - - strategy_config = self.strategy_setting.get('strategy_name', {}) - - self.strategy_setting[strategy_name] = { + # 原配置 + old_config = self.strategy_setting.get('strategy_name', {}) + new_config = { "class_name": strategy.__class__.__name__, "vt_symbol": strategy.vt_symbol, "auto_init": auto_init, "auto_start": auto_start, "setting": setting } + + if old_config: + self.write_log(f'{strategy_name} 配置变更:\n{old_config} \n=> \n{new_config}') + + self.strategy_setting[strategy_name] = new_config + save_json(self.setting_filename, self.strategy_setting) def remove_strategy_setting(self, strategy_name: str): diff --git a/vnpy/app/cta_strategy_pro/portfolio_testing.py b/vnpy/app/cta_strategy_pro/portfolio_testing.py index e4979474..0b9b460a 100644 --- a/vnpy/app/cta_strategy_pro/portfolio_testing.py +++ b/vnpy/app/cta_strategy_pro/portfolio_testing.py @@ -10,383 +10,62 @@ from __future__ import division import sys import os import gc -import importlib -import csv -import copy import pandas as pd import traceback -import numpy as np import random -import logging +import bz2 +import pickle -from collections import OrderedDict, defaultdict from datetime import datetime, timedelta -from functools import lru_cache -from pathlib import Path from time import sleep -from .base import ( - EngineType, - STOPORDER_PREFIX, - StopOrder, - StopOrderStatus -) -from .template import CtaTemplate - -from .cta_fund_kline import FundKline - from vnpy.trader.object import ( + TickData, BarData, RenkoBarData, - OrderData, - TradeData, - ContractData ) from vnpy.trader.constant import ( Exchange, - Direction, - Offset, - Status, - OrderType, - Product ) -from vnpy.trader.converter import PositionHolding from vnpy.trader.utility import ( get_trading_date, - get_underlying_symbol, - round_to, extract_vt_symbol, - format_number, - import_module_by_str ) -from vnpy.trader.util_logger import setup_logger +from .back_testing import BackTestingEngine -class PortfolioTestingEngine(object): +class PortfolioTestingEngine(BackTestingEngine): """ - CTA组合回测引擎 + CTA组合回测引擎, 使用回测引擎作为父类 函数接口和策略引擎保持一样, 从而实现同一套代码从回测到实盘。 - 针对1分钟bar的回测 + 针对1分钟bar的回测 或者tick回测 导入CTA_Settings - 20190617: - 1.增加保证金选项,股票不按照保证金计算。 - 2.取消输出longPos和ShortPos + """ def __init__(self, event_engine=None): """Constructor""" + super().__init__(event_engine) - # 绑定事件引擎 - self.event_engine = event_engine - - # 引擎类型为回测 - self.engine_type = EngineType.BACKTESTING - - # 回测策略相关 - self.classes = {} # 策略类,class_name: stategy_class - self.class_module_map = {} # 策略类名与模块名映射 class_name: mudule_name - self.strategies = {} # 回测策略实例, key = strategy_name, value= strategy - self.symbol_strategy_map = defaultdict(list) # vt_symbol: strategy list - - self.test_name = 'portfolio_test_{}'.format(datetime.now().strftime('%M%S')) # 回测策略组合的实例名字 - self.daily_report_name = '' # 策略的日净值报告文件名称 - - self.test_start_date = '' # 组合回测启动得日期 - self.init_days = 0 # 初始化天数 - self.test_end_date = '' # 组合回测结束日期 - - self.slippage = {} # 回测时假设的滑点 - self.commission_rate = {} # 回测时假设的佣金比例(适用于百分比佣金) - self.fix_commission = {} # 每手固定手续费 - self.size = {} # 合约大小,默认为1 - self.price_tick = {} # 价格最小变动 - self.margin_rate = {} # 回测合约的保证金比率 - self.price_dict = {} # 登记vt_symbol对应的最新价 - self.contract_dict = {} # 登记vt_symbol得对应合约信息 - self.symbol_exchange_dict = {} # 登记symbol: exchange的对应关系 + self.mode = 'bar' # 'bar': 根据1分钟k线进行回测, 'tick',根据分笔tick进行回测 self.bar_csv_file = {} self.bar_df_dict = {} # 历史数据的df,回测用 self.bar_df = None # 历史数据的df,时间+symbol作为组合索引 + self.bar_interval_seconds = 60 # bar csv文件,属于K线类型,K线的周期(秒数),缺省是1分钟 - self.data_start_date = None # 回测数据开始日期,datetime对象 (用于截取数据) - self.data_end_date = None # 回测数据结束日期,datetime对象 (用于截取数据) - self.strategy_start_date = None # 策略启动日期(即前面的数据用于初始化),datetime对象 + self.tick_path = None # tick级别回测, 路径 - self.stop_order_count = 0 # 本地停止单编号 - self.stop_orders = {} # 本地停止单 - self.active_stop_orders = {} # 活动本地停止单 - - self.limit_order_count = 0 # 限价单编号 - self.limit_orders = OrderedDict() # 限价单字典 - self.active_limit_orders = OrderedDict() # 活动限价单字典,用于进行撮合用 - - self.order_strategy_dict = {} # orderid 与 strategy的映射 - - # 持仓缓存字典 - # key为vt_symbol,value为PositionBuffer对象 - self.pos_holding_dict = {} - - self.trade_count = 0 # 成交编号 - self.trade_dict = OrderedDict() # 用于统计成交收益时,还没处理得交易 - self.trades = OrderedDict() # 记录所有得成交记录 - self.trade_pnl_list = [] # 交易记录列表 - - self.long_position_list = [] # 多单持仓 - self.short_position_list = [] # 空单持仓 - - # 当前最新数据,用于模拟成交用 - self.gateway_name = u'BackTest' - - self.last_bar = {} # 最新的bar - self.last_dt = None # 最新时间 - - # csvFile相关 - self.bar_interval_seconds = 60 # csv文件,属于K线类型,K线的周期(秒数),缺省是1分钟 - - # 费用风控情况 - self.percent = 0.0 - self.percent_limit = 30 # 投资仓位比例上限 - - # 回测计算相关 - self.use_margin = True # 使用保证金模式(期货使用,计算保证金时,按照开仓价计算。股票是按照当前价计算) - - self.init_capital = 1000000 # 期初资金 - self.cur_capital = self.init_capital # 当前资金净值 - self.net_capital = self.init_capital # 实时资金净值(每日根据capital和持仓浮盈计算) - self.max_capital = self.init_capital # 资金最高净值 - self.max_net_capital = self.init_capital - self.avaliable = self.init_capital - - self.max_pnl = 0 # 最高盈利 - self.min_pnl = 0 # 最大亏损 - self.max_occupy_rate = 0 # 最大保证金占比 - self.winning_result = 0 # 盈利次数 - self.losing_result = 0 # 亏损次数 - - self.total_trade_count = 0 # 总成交数量 - self.total_winning = 0 # 总盈利 - self.total_losing = 0 # 总亏损 - self.total_turnover = 0 # 总成交金额(合约面值) - self.total_commission = 0 # 总手续费 - self.total_slippage = 0 # 总滑点 - - self.time_list = [] # 时间序列 - self.pnl_list = [] # 每笔盈亏序列 - self.capital_list = [] # 盈亏汇总的时间序列 - self.drawdown_list = [] # 回撤的时间序列 - self.drawdown_rate_list = [] # 最大回撤比例的时间序列(成交结算) - - self.max_net_capital_time = '' - self.max_drawdown_rate_time = '' - self.daily_max_drawdown_rate = 0 # 按照日结算价计算 - - self.pnl_strategy_dict = {} # 策略实例的平仓盈亏 - - self.daily_list = [] # 按日统计得序列 - self.daily_first_benchmark = None - - self.logger = None - self.strategy_loggers = {} - self.debug = False - - self.is_7x24 = False - self.logs_path = None - self.data_path = None - - self.fund_kline_dict = {} - self.acivte_fund_kline = False - - def create_fund_kline(self, name, use_renko=False): - """ - 创建资金曲线 - :param name: 账号名,或者策略名 - :param use_renko: - :return: - """ - setting = {} - setting.update({'name': name}) - setting['para_ma1_len'] = 5 - setting['para_ma2_len'] = 10 - setting['para_ma3_len'] = 20 - setting['para_active_yb'] = True - setting['price_tick'] = 0.01 - setting['underlying_symbol'] = 'fund' - if use_renko: - # 使用砖图,高度是资金的千分之一 - setting['height'] = self.init_capital * 0.001 - setting['use_renko'] = True - - fund_kline = FundKline(cta_engine=self, setting=setting) - self.fund_kline_dict.update({name: fund_kline}) - return fund_kline - - def get_fund_kline(self, name: str = None): - # 指定资金账号/策略名 - if name: - kline = self.fund_kline_dict.get(name, None) - return kline - - # 没有指定账号,并且存在一个或多个资金K线 - if len(self.fund_kline_dict) > 0: - # 优先找vt_setting中,配置了strategy_groud的资金K线 - kline = self.fund_kline_dict.get(self.test_name, None) - - # 找不到,返回第一个 - if kline is None: - kline = self.fund_kline_dict.values()[0] - return kline - else: - return None - - def get_account(self, vt_accountid: str = ""): - """返回账号的实时权益,可用资金,仓位比例,投资仓位比例上限""" - if self.net_capital == 0.0: - self.percent = 0.0 - - return self.net_capital, self.avaliable, self.percent, self.percent_limit - - def set_test_start_date(self, start_date: str = '20100416', init_days: int = 10): - """设置回测的启动日期""" - self.test_start_date = start_date - self.init_days = init_days - - self.data_start_date = datetime.strptime(start_date, '%Y%m%d') - - # 初始化天数 - init_time_delta = timedelta(init_days) - - self.strategy_start_date = self.data_start_date + init_time_delta - self.write_log(u'设置:回测数据开始日期:{},初始化数据为{}天,策略自动启动日期:{}' - .format(self.data_start_date, self.init_days, self.strategy_start_date)) - - def set_test_end_date(self, end_date: str = ''): - """设置回测的结束日期""" - self.test_end_date = end_date - if end_date: - self.data_end_date = datetime.strptime(end_date, '%Y%m%d') - # 若不修改时间则会导致不包含dataEndDate当天数据 - self.data_end_date.replace(hour=23, minute=59) - else: - self.data_end_date = datetime.now() - self.write_log(u'设置:回测数据结束日期:{}'.format(self.data_end_date)) - - def set_init_capital(self, capital: float): - """设置期初净值""" - self.cur_capital = capital # 资金 - self.net_capital = capital # 实时资金净值(每日根据capital和持仓浮盈计算) - self.max_capital = capital # 资金最高净值 - self.max_net_capital = capital - self.avaliable = capital - self.init_capital = capital - - def set_margin_rate(self, vt_symbol: str, margin_rate: float): - """设置某个合约得保证金比率""" - self.margin_rate.update({vt_symbol: margin_rate}) - - @lru_cache() - def get_margin_rate(self, vt_symbol: str): - return self.margin_rate.get(vt_symbol, 0.1) - - def set_slippage(self, vt_symbol: str, slippage: float): - """设置滑点点数""" - self.slippage.update({vt_symbol: slippage}) - - @lru_cache() - def get_slippage(self, vt_symbol: str): - """获取滑点""" - return self.slippage.get(vt_symbol, 0) - - def set_size(self, vt_symbol: str, size: int): - """设置合约大小""" - self.size.update({vt_symbol: size}) - - @lru_cache() - def get_size(self, vt_symbol: str): - """查询合约的size""" - return self.size.get(vt_symbol, 10) - - def set_price(self, vt_symbol: str, price: float): - self.price_dict.update({vt_symbol: price}) - - def get_price(self, vt_symbol: str): - return self.price_dict.get(vt_symbol, None) - - def set_commission_rate(self, vt_symbol: str, rate: float): - """设置佣金比例""" - self.commission_rate.update({vt_symbol: rate}) - - if rate >= 0.1: - self.fix_commission.update({vt_symbol: rate}) - - def get_commission_rate(self, vt_symbol: str): - """ 获取保证金比例,缺省万分之一""" - return self.commission_rate.get(vt_symbol, float(0.00001)) - - def get_fix_commission(self, vt_symbol: str): - return self.fix_commission.get(vt_symbol, 0) - - def set_price_tick(self, vt_symbol: str, price_tick: float): - """设置价格最小变动""" - self.price_tick.update({vt_symbol: price_tick}) - - def get_price_tick(self, vt_symbol: str): - return self.price_tick.get(vt_symbol, 1) - - def set_contract(self, symbol: str, exchange: Exchange, product: Product, name: str, size: int, price_tick: float): - """设置合约信息""" - vt_symbol = '.'.join([symbol, exchange.value]) - if vt_symbol not in self.contract_dict: - c = ContractData( - gateway_name=self.gateway_name, - symbol=symbol, - exchange=exchange, - name=name, - product=product, - size=size, - pricetick=price_tick - ) - self.contract_dict.update({vt_symbol: c}) - self.set_size(vt_symbol, size) - # self.set_margin_rate(vt_symbol, ) - self.set_price_tick(vt_symbol, price_tick) - self.symbol_exchange_dict.update({symbol: exchange}) - - @lru_cache() - def get_contract(self, vt_symbol): - """获取合约配置信息""" - return self.contract_dict.get(vt_symbol) - - @lru_cache() - def get_exchange(self, symbol: str): - return self.symbol_exchange_dict.get(symbol, Exchange.LOCAL) - - def set_name(self, test_name): - """ - 设置组合的运行实例名称 - :param test_name: - :return: - """ - self.test_name = test_name - - def set_daily_report_name(self, report_file): - """ - 设置策略的日净值记录csv保存文件名(含路径) - :param report_file: 保存文件名(含路径) - :return: - """ - self.daily_report_name = report_file - - def load_csv_to_df(self, vt_symbol, bar_file, data_start_date=None, data_end_date=None): - """回测数据初始化""" + def load_bar_csv_to_df(self, vt_symbol, bar_file, data_start_date=None, data_end_date=None): + """加载回测bar数据到DataFrame""" self.output(u'loading {} from {}'.format(vt_symbol, bar_file)) if vt_symbol in self.bar_df_dict: return True - if not os.path.isfile(bar_file): + if not os.path.exists(bar_file): self.write_error(u'回测时,{}对应的csv bar文件{}不存在'.format(vt_symbol, bar_file)) return False @@ -426,8 +105,9 @@ class PortfolioTestingEngine(object): return True - def comine_df(self): + def comine_bar_df(self): """ + 合并所有回测合约的bar DataFrame =》集中的DataFrame 把bar_df_dict =》bar_df :return: """ @@ -437,9 +117,13 @@ class PortfolioTestingEngine(object): def prepare_env(self, test_settings): self.output('prepare_env') + if 'name' in test_settings: self.set_name(test_settings.get('name')) + self.mode = test_settings.get('mode', 'bar') + self.output(f'采用{self.mode}方式回测') + self.debug = test_settings.get('debug', False) # 更新数据目录 @@ -448,14 +132,14 @@ class PortfolioTestingEngine(object): else: self.data_path = os.path.abspath(os.path.join(os.getcwd(), 'data')) - print(f'数据输出目录:{self.data_path}') + self.output(f'数据输出目录:{self.data_path}') # 更新日志目录 if 'logs_path' in test_settings: self.logs_path = os.path.abspath(os.path.join(test_settings.get('logs_path'), self.test_name)) else: self.logs_path = os.path.abspath(os.path.join(os.getcwd(), 'log', self.test_name)) - print(f'日志输出目录:{self.logs_path}') + self.output(f'日志输出目录:{self.logs_path}') # 创建日志 self.create_logger(debug=self.debug) @@ -500,6 +184,9 @@ class PortfolioTestingEngine(object): self.write_log(u'准备数据') self.prepare_data(test_settings.get('symbol_datas')) + if self.mode == 'tick': + self.tick_path = test_settings.get('tick_path', None) + self.acivte_fund_kline = test_settings.get('acivte_fund_kline', False) if self.acivte_fund_kline: # 创建资金K线 @@ -510,36 +197,22 @@ class PortfolioTestingEngine(object): def prepare_data(self, data_dict): """ 准备组合数据 - :param data_dict: + :param data_dict: 合约得配置参数 :return: """ - self.output('prepare_data') + # 调用回测引擎,跟新合约得数据 + super().prepare_data(data_dict) if len(data_dict) == 0: self.write_log(u'请指定回测数据和文件') return - import os + if self.mode == 'tick': + return + + # 检查/更新bar文件 for symbol, symbol_data in data_dict.items(): self.write_log(u'配置{}数据:{}'.format(symbol, symbol_data)) - self.set_price_tick(symbol, symbol_data.get('price_tick', 1)) - - self.set_slippage(symbol, symbol_data.get('slippage', 0)) - - self.set_size(symbol, symbol_data.get('symbol_size', 10)) - - self.set_margin_rate(symbol, symbol_data.get('margin_rate', 0.1)) - - self.set_commission_rate(symbol, symbol_data.get('commission_rate', float(0.0001))) - - self.set_contract( - symbol=symbol, - name=symbol, - exchange=Exchange(symbol_data.get('exchange', 'LOCAL')), - product=Product(symbol_data.get('product', "期货")), - size=symbol_data.get('symbol_size', 10), - price_tick=symbol_data.get('price_tick', 1) - ) bar_file = symbol_data.get('bar_file', None) @@ -556,9 +229,7 @@ class PortfolioTestingEngine(object): def run_portfolio_test(self, strategy_settings: dict = {}): """ 运行组合回测 - """ - if not self.strategy_start_date: self.write_error(u'回测开始日期未设置。') return @@ -580,18 +251,25 @@ class PortfolioTestingEngine(object): self.write_log(u'开始回放数据') + self.write_log(u'开始回测:{} ~ {}'.format(self.data_start_date, self.data_end_date)) + + if self.mode == 'bar': + self.run_bar_test() + else: + self.run_tick_test() + + def run_bar_test(self): + """使用bar进行组合回测""" testdays = (self.data_end_date - self.data_start_date).days if testdays < 1: self.write_log(u'回测时间不足') return - self.write_log(u'开始回测:{} ~ {}'.format(self.data_start_date, self.data_end_date)) - # 加载数据 for vt_symbol in self.symbol_strategy_map.keys(): symbol, exchange = extract_vt_symbol(vt_symbol) - self.load_csv_to_df(vt_symbol, self.bar_csv_file.get(symbol)) + self.load_bar_csv_to_df(vt_symbol, self.bar_csv_file.get(symbol)) # 为套利合约提取主动 / 被动合约 if exchange == Exchange.SPD: @@ -599,13 +277,13 @@ class PortfolioTestingEngine(object): active_symbol, active_rate, passive_symbol, passive_rate, spd_type = symbol.split('-') active_vt_symbol = '.'.join([active_symbol, self.get_exchange(symbol=active_symbol)]) passive_vt_symbol = '.'.join([passive_symbol, self.get_exchange(symbol=passive_symbol)]) - self.load_csv_to_df(active_vt_symbol, self.bar_csv_file.get(active_symbol)) - self.load_csv_to_df(passive_vt_symbol, self.bar_csv_file.get(passive_symbol)) + self.load_bar_csv_to_df(active_vt_symbol, self.bar_csv_file.get(active_symbol)) + self.load_bar_csv_to_df(passive_vt_symbol, self.bar_csv_file.get(passive_symbol)) except Exception as ex: self.write_error(u'为套利合约提取主动/被动合约出现异常:{}'.format(str(ex))) # 合并数据 - self.comine_df() + self.comine_bar_df() last_trading_day = None bars_dt = None @@ -674,7 +352,7 @@ class PortfolioTestingEngine(object): bars_same_dt = [bar] bars_dt = dt - # 启动交易 + # 更新每日净值 if self.strategy_start_date <= dt <= self.data_end_date: if last_trading_day != bar.trading_day: if last_trading_day is not None: @@ -699,7 +377,7 @@ class PortfolioTestingEngine(object): self.output(u'净值低于0,回测停止') return - self.write_log(u'数据回放完成') + self.write_log(u'bar数据回放完成') if last_trading_day is not None: self.saving_daily_data(datetime.strptime(last_trading_day, '%Y-%m-%d'), self.cur_capital, self.max_net_capital, self.total_commission) @@ -710,1634 +388,122 @@ class PortfolioTestingEngine(object): traceback.print_exc() return - def new_bar(self, bar): - """新的K线""" - self.last_bar.update({bar.vt_symbol: bar}) - self.last_dt = bar.datetime - self.set_price(bar.vt_symbol, bar.close_price) - self.cross_stop_order(bar) # 撮合停止单 - self.cross_limit_order(bar) # 先撮合限价单 + def load_bz2_cache(self, cache_folder, cache_symbol, cache_date): + """加载缓存数据""" + if not os.path.exists(cache_folder): + self.write_error('缓存目录:{}不存在,不能读取'.format(cache_folder)) + return None + cache_folder_year_month = os.path.join(cache_folder, cache_date[:6]) + if not os.path.exists(cache_folder_year_month): + self.write_error('缓存目录:{}不存在,不能读取'.format(cache_folder_year_month)) + return None - # 更新资金曲线(只有持仓时,才更新) - fund_kline = self.get_fund_kline(self.test_name) - if fund_kline is not None and (len(self.long_position_list) > 0 or len(self.short_position_list) > 0): - fund_kline.update_account(self.last_dt, self.net_capital) + cache_file = os.path.join(cache_folder_year_month, '{}_{}.pkb2'.format(cache_symbol, cache_date)) + if not os.path.isfile(cache_file): + cache_file = os.path.join(cache_folder_year_month, '{}_{}.pkz2'.format(cache_symbol, cache_date)) + if not os.path.isfile(cache_file): + self.write_error('缓存文件:{}不存在,不能读取'.format(cache_file)) + return None - for strategy in self.symbol_strategy_map.get(bar.vt_symbol, []): - # 更新策略的资金K线 - fund_kline = self.fund_kline_dict.get(strategy.strategy_name, None) - if fund_kline: - hold_pnl = fund_kline.get_hold_pnl() - if hold_pnl != 0: - fund_kline.update_strategy(dt=self.last_dt, hold_pnl=hold_pnl) + with bz2.BZ2File(cache_file, 'rb') as f: + data = pickle.load(f) + return data - # 推送K线到策略中 - strategy.on_bar(bar) # 推送K线到策略中 + return None - # 到达策略启动日期,启动策略 - if not strategy.trading and self.strategy_start_date < bar.datetime: - strategy.trading = True - strategy.on_start() - self.output(u'{}策略启动交易'.format(strategy.strategy_name)) + def get_day_tick_df(self, test_day): + """获取某一天得所有合约tick""" + tick_data_dict = {} - def load_strategy_class(self): - """ - Load strategy class from source code. - """ - # 加载 vnpy/app/cta_strategy_pro/strategies的所有策略 - path1 = Path(__file__).parent.joinpath("strategies") - self.load_strategy_class_from_folder( - path1, "vnpy.app.cta_strategy_pro.strategies") - - def load_strategy_class_from_folder(self, path: Path, module_name: str = ""): - """ - Load strategy class from certain folder. - """ - for dirpath, dirnames, filenames in os.walk(str(path)): - for filename in filenames: - if filename.endswith(".py"): - strategy_module_name = ".".join( - [module_name, filename.replace(".py", "")]) - elif filename.endswith(".pyd"): - strategy_module_name = ".".join( - [module_name, filename.split(".")[0]]) - else: - continue - self.load_strategy_class_from_module(strategy_module_name) - - def load_strategy_class_from_module(self, module_name: str): - """ - Load/Reload strategy class from module file. - """ - try: - module = importlib.import_module(module_name) - - for name in dir(module): - value = getattr(module, name) - if (isinstance(value, type) and issubclass(value, CtaTemplate) and value is not CtaTemplate): - class_name = value.__name__ - if class_name not in self.classes: - self.write_log(f"加载策略类{module_name}.{class_name}") - else: - self.write_log(f"更新策略类{module_name}.{class_name}") - self.classes[class_name] = value - self.class_module_map[class_name] = module_name - return True - except: # noqa - msg = f"策略文件{module_name}加载失败,触发异常:\n{traceback.format_exc()}" - self.write_error(msg) - self.output(msg) - return False - - def load_strategy(self, strategy_name: str, strategy_setting: dict = None): - """ - 装载回测的策略 - setting是参数设置,包括 - class_name: str, 策略类名字 - vt_symbol: str, 缺省合约 - setting: {}, 策略的参数 - auto_init: True/False, 策略是否自动初始化 - auto_start: True/False, 策略是否自动启动 - """ - - # 获取策略的类名 - class_name = strategy_setting.get('class_name', None) - if class_name is None or strategy_name is None: - self.write_error(u'setting中没有class_name') - return - - # strategy_class => module.strategy_class - if '.' not in class_name: - module_name = self.class_module_map.get(class_name, None) - if module_name: - class_name = module_name + '.' + class_name - self.write_log(u'转换策略为全路径:{}'.format(class_name)) - - # 获取策略类的定义 - strategy_class = import_module_by_str(class_name) - if strategy_class is None: - self.write_error(u'加载策略模块失败:{}'.format(class_name)) - return - - # 处理 vt_symbol - vt_symbol = strategy_setting.get('vt_symbol') - if '.' in vt_symbol: + for vt_symbol in list(self.symbol_strategy_map.keys()): symbol, exchange = extract_vt_symbol(vt_symbol) - else: - symbol = vt_symbol - underly_symbol = get_underlying_symbol(symbol).upper() - exchange = self.get_exchange(f'{underly_symbol}99') - vt_symbol = '.'.join([symbol, exchange.value]) - - # 在期货组合回测,中需要把一般配置的主力合约,更换为指数合约 - if '99' not in symbol and exchange != Exchange.SPD: - underly_symbol = get_underlying_symbol(symbol).upper() - self.write_log(u'更新vt_symbol为指数合约:{}=>{}'.format(vt_symbol, underly_symbol + '99.' + exchange.value)) - vt_symbol = underly_symbol.upper() + '99.' + exchange.value - strategy_setting.update({'vt_symbol': vt_symbol}) - - # 属于自定义套利合约 - if exchange == Exchange.SPD: - symbol_pairs = symbol.split('-') - active_symbol = get_underlying_symbol(symbol_pairs[0]) - passive_symbol = get_underlying_symbol(symbol_pairs[2]) - new_vt_symbol = '-'.join([active_symbol.upper() + '99', - symbol_pairs[1], - passive_symbol.upper() + '99', - symbol_pairs[3], - symbol_pairs[4]]) + '.SPD' - self.write_log(u'更新vt_symbol为指数合约:{}=>{}'.format(vt_symbol, new_vt_symbol)) - vt_symbol = new_vt_symbol - strategy_setting.update({'vt_symbol': vt_symbol}) - - # 取消自动启动 - if 'auto_start' in strategy_setting: - strategy_setting.update({'auto_start': False}) - - # 策略参数设置 - setting = strategy_setting.get('setting', {}) - - # 强制更新回测为True - setting.update({'backtesting': True}) - - # 创建实例 - strategy = strategy_class(self, strategy_name, vt_symbol, setting) - - # 保存到策略实例映射表中 - self.strategies.update({strategy_name: strategy}) - - # 更新vt_symbol合约与策略的订阅关系 - self.subscribe_symbol(strategy_name=strategy_name, vt_symbol=vt_symbol) - - if strategy_setting.get('auto_init', False): - self.write_log(u'自动初始化策略') - strategy.on_init() - - if strategy_setting.get('auto_start', False): - self.write_log(u'自动启动策略') - strategy.on_start() - - if self.acivte_fund_kline: - # 创建策略实例的资金K线 - self.create_fund_kline(name=strategy_name, use_renko=False) - - def subscribe_symbol(self, strategy_name: str, vt_symbol: str, gateway_name: str = '', is_bar: bool = False): - """订阅合约""" - strategy = self.strategies.get(strategy_name, None) - if not strategy: - return False - - # 添加 合约订阅 vt_symbol <=> 策略实例 strategy 映射. - strategies = self.symbol_strategy_map[vt_symbol] - strategies.append(strategy) - return True - - # --------------------------------------------------------------------- - def save_strategy_data(self): - """保存策略数据""" - for strategy in self.strategies.values(): - self.write_log(u'save strategy data') - strategy.save_data() - - def send_order(self, - strategy: CtaTemplate, - vt_symbol: str, - direction: Direction, - offset: Offset, - price: float, - volume: float, - stop: bool, - lock: bool, - order_type: OrderType = OrderType.LIMIT, - gateway_name: str = None): - """发单""" - price_tick = self.get_price_tick(vt_symbol) - price = round_to(price, price_tick) - - if stop: - return self.send_local_stop_order( - strategy=strategy, - vt_symbol=vt_symbol, - direction=direction, - offset=offset, - price=price, - volume=volume, - lock=lock, - gateway_name=gateway_name - ) - else: - return self.send_limit_order( - strategy=strategy, - vt_symbol=vt_symbol, - direction=direction, - offset=offset, - price=price, - volume=volume, - lock=lock, - gateway_name=gateway_name - ) - - def send_limit_order(self, - strategy: CtaTemplate, - vt_symbol: str, - direction: Direction, - offset: Offset, - price: float, - volume: float, - lock: bool, - order_type: OrderType = OrderType.LIMIT, - gateway_name: str = None - ): - self.limit_order_count += 1 - order_id = str(self.limit_order_count) - symbol, exchange = extract_vt_symbol(vt_symbol) - if gateway_name is None: - gateway_name = self.gateway_name - order = OrderData( - gateway_name=gateway_name, - symbol=symbol, - exchange=exchange, - orderid=order_id, - direction=direction, - offset=offset, - type=order_type, - price=round_to(value=price, target=self.get_price_tick(symbol)), - volume=volume, - status=Status.NOTTRADED, - time=str(self.last_dt) - ) - - # 保存到限价单字典中 - self.active_limit_orders[order.vt_orderid] = order - self.limit_orders[order.vt_orderid] = order - self.order_strategy_dict.update({order.vt_orderid: strategy}) - - self.write_log(f'创建限价单:{order.__dict__}') - - return [order.vt_orderid] - - def send_local_stop_order( - self, - strategy: CtaTemplate, - vt_symbol: str, - direction: Direction, - offset: Offset, - price: float, - volume: float, - lock: bool, - gateway_name: str = None): - - """""" - self.stop_order_count += 1 - - stop_order = StopOrder( - vt_symbol=vt_symbol, - direction=direction, - offset=offset, - price=price, - volume=volume, - stop_orderid=f"{STOPORDER_PREFIX}.{self.stop_order_count}", - strategy_name=strategy.strategy_name, - ) - self.write_log(f'创建本地停止单:{stop_order.__dict__}') - self.order_strategy_dict.update({stop_order.stop_orderid: strategy}) - - self.active_stop_orders[stop_order.stop_orderid] = stop_order - self.stop_orders[stop_order.stop_orderid] = stop_order - - return [stop_order.stop_orderid] - - def cancel_order(self, strategy: CtaTemplate, vt_orderid: str): - """撤单""" - if vt_orderid.startswith(STOPORDER_PREFIX): - return self.cancel_stop_order(strategy, vt_orderid) - else: - return self.cancel_limit_order(strategy, vt_orderid) - - def cancel_limit_order(self, strategy: CtaTemplate, vt_orderid: str): - """限价单撤单""" - if vt_orderid in self.active_limit_orders: - order = self.active_limit_orders[vt_orderid] - register_strategy = self.order_strategy_dict.get(vt_orderid, None) - if register_strategy.strategy_name != strategy.strategy_name: - return False - order.status = Status.CANCELLED - order.cancelTime = str(self.last_dt) - self.active_limit_orders.pop(vt_orderid, None) - strategy.on_order(order) - return True - return False - - def cancel_stop_order(self, strategy: CtaTemplate, vt_orderid: str): - """本地停止单撤单""" - if vt_orderid not in self.active_stop_orders: - return False - stop_order = self.active_stop_orders.pop(vt_orderid) - - stop_order.status = StopOrderStatus.CANCELLED - strategy.on_stop_order(stop_order) - return True - - def cancel_all(self, strategy): - """撤销某个策略的所有委托单""" - self.cancel_orders(strategy=strategy) - - def cancel_orders(self, vt_symbol: str = None, offset: Offset = None, strategy: CtaTemplate = None): - """撤销所有单""" - # Symbol参数:指定合约的撤单; - # OFFSET参数:指定Offset的撤单,缺省不填写时,为所有 - # strategy参数: 指定某个策略的单子 - - if len(self.active_limit_orders) > 0: - self.write_log(u'从所有订单中,撤销:开平:{},合约:{},策略:{}' - .format(offset, - vt_symbol if vt_symbol is not None else u'所有', - strategy.strategy_name if strategy else None)) - - for vt_orderid in list(self.active_limit_orders.keys()): - order = self.active_limit_orders.get(vt_orderid, None) - order_strategy = self.order_strategy_dict.get(vt_orderid, None) - if order is None or order_strategy is None: + tick_list = self.load_bz2_cache(cache_folder=self.tick_path, + cache_symbol=symbol, + cache_date=test_day.strftime('%Y%m%d')) + if not tick_list or len(tick_list) == 0: continue - if offset is None: - offset_cond = True - else: - offset_cond = order.offset == offset + symbol_tick_df = pd.DataFrame(tick_list) + # 缓存文件中,datetime字段,已经是datetime格式 + # 暂时根据时间去重,没有汇总volume + symbol_tick_df.drop_duplicates(subset=['datetime'], keep='first', inplace=True) + symbol_tick_df.set_index('datetime', inplace=True) - if vt_symbol is None: - symbol_cond = True - else: - symbol_cond = order.vt_symbol == vt_symbol + tick_data_dict.update({vt_symbol: symbol_tick_df}) - if strategy is None: - strategy_cond = True - else: - strategy_cond = strategy.strategy_name == order_strategy.strategy_name + if len(tick_data_dict) == 0: + return None - if offset_cond and symbol_cond and strategy_cond: - self.write_log( - u'撤销订单:{},{} {}@{}'.format(vt_orderid, order.direction, order.price, order.volume)) - order.status = Status.CANCELLED - order.cancel_time = str(self.last_dt) - del self.active_limit_orders[vt_orderid] - if strategy: - strategy.on_order(order) + tick_df = pd.concat(tick_data_dict, axis=0).swaplevel(0, 1).sort_index() - for stop_orderid in list(self.active_stop_orders.keys()): - order = self.active_stop_orders.get(stop_orderid, None) - order_strategy = self.order_strategy_dict.get(stop_orderid, None) - if order is None or order_strategy is None: - continue + return tick_df - if offset is None: - offset_cond = True - else: - offset_cond = order.offset == offset + def run_tick_test(self): + """运行tick级别组合回测""" + testdays = (self.data_end_date - self.data_start_date).days - if vt_symbol is None: - symbol_cond = True - else: - symbol_cond = order.vt_symbol == vt_symbol - - if strategy is None: - strategy_cond = True - else: - strategy_cond = strategy.strategy_name == order_strategy.strategy_name - - if offset_cond and symbol_cond and strategy_cond: - self.write_log( - u'撤销本地停止单:{},{} {}@{}'.format(stop_orderid, order.direction, order.price, order.volume)) - order.status = Status.CANCELLED - order.cancel_time = str(self.last_dt) - self.active_stop_orders.pop(stop_orderid, None) - if strategy: - strategy.on_stop_order(order) - - def cross_stop_order(self, bar): - """ - Cross stop order with last bar/tick data. - """ - vt_symbol = bar.vt_symbol - for stop_orderid in list(self.active_stop_orders.keys()): - stop_order = self.active_stop_orders[stop_orderid] - strategy = self.order_strategy_dict.get(stop_orderid, None) - if stop_order.vt_symbol != vt_symbol or stop_order is None or strategy is None: - continue - # 若买入方向停止单价格高于等于该价格,则会触发 - long_cross_price = round_to(value=bar.low_price, target=self.get_price_tick(vt_symbol)) - long_cross_price -= self.get_price_tick(vt_symbol) - - # 若卖出方向停止单价格低于等于该价格,则会触发 - short_cross_price = round_to(value=bar.high_price, target=self.get_price_tick(vt_symbol)) - short_cross_price += self.get_price_tick(vt_symbol) - - # 在当前时间点前发出的买入委托可能的最优成交价 - long_best_price = round_to(value=bar.open_price, - target=self.get_price_tick(vt_symbol)) + self.get_price_tick(vt_symbol) - - # 在当前时间点前发出的卖出委托可能的最优成交价 - short_best_price = round_to(value=bar.open_price, - target=self.get_price_tick(vt_symbol)) - self.get_price_tick(vt_symbol) - - # Check whether stop order can be triggered. - long_cross = ( - stop_order.direction == Direction.LONG - and stop_order.price <= long_cross_price - ) - - short_cross = ( - stop_order.direction == Direction.SHORT - and stop_order.price >= short_cross_price - ) - - if not long_cross and not short_cross: - continue - - # Create order data. - self.limit_order_count += 1 - symbol, exchange = extract_vt_symbol(vt_symbol) - order = OrderData( - symbol=symbol, - exchange=exchange, - orderid=str(self.limit_order_count), - direction=stop_order.direction, - offset=stop_order.offset, - price=stop_order.price, - volume=stop_order.volume, - status=Status.ALLTRADED, - gateway_name=self.gateway_name, - ) - order.datetime = self.last_dt - self.write_log(f'停止单被触发:\n{stop_order.__dict__}\n=>委托单{order.__dict__}') - self.limit_orders[order.vt_orderid] = order - - # Create trade data. - if long_cross: - trade_price = max(stop_order.price, long_best_price) - else: - trade_price = min(stop_order.price, short_best_price) - - self.trade_count += 1 - - trade = TradeData( - symbol=order.symbol, - exchange=order.exchange, - orderid=order.orderid, - tradeid=str(self.trade_count), - direction=order.direction, - offset=order.offset, - price=trade_price, - volume=order.volume, - time=self.last_dt.strftime("%Y-%m-%d %H:%M:%S"), - datetime=self.last_dt, - gateway_name=self.gateway_name, - ) - trade.strategy_name = strategy.strategy_name - trade.datetime = self.last_dt - self.write_log(f'停止单触发成交:{trade.__dict__}') - self.trade_dict[trade.vt_tradeid] = trade - self.trades[trade.vt_tradeid] = copy.copy(trade) - - # Update stop order. - stop_order.vt_orderids.append(order.vt_orderid) - stop_order.status = StopOrderStatus.TRIGGERED - - self.active_stop_orders.pop(stop_order.stop_orderid) - - # Push update to strategy. - strategy.on_stop_order(stop_order) - strategy.on_order(order) - self.append_trade(trade) - strategy.on_trade(trade) - - def cross_limit_order(self, bar): - """基于最新数据撮合限价单""" - - vt_symbol = bar.vt_symbol - - # 遍历限价单字典中的所有限价单 - for vt_orderid in list(self.active_limit_orders.keys()): - order = self.active_limit_orders.get(vt_orderid, None) - if order.vt_symbol != vt_symbol: - continue - - strategy = self.order_strategy_dict.get(order.vt_orderid, None) - if strategy is None: - self.write_error(u'找不到vt_orderid:{}对应的策略'.format(order.vt_orderid)) - continue - - buy_cross_price = round_to(value=bar.low_price, - target=self.get_price_tick(vt_symbol)) + self.get_price_tick( - vt_symbol) # 若买入方向限价单价格高于该价格,则会成交 - sell_cross_price = round_to(value=bar.high_price, - target=self.get_price_tick(vt_symbol)) - self.get_price_tick( - vt_symbol) # 若卖出方向限价单价格低于该价格,则会成交 - buy_best_cross_price = round_to(value=bar.open_price, - target=self.get_price_tick(vt_symbol)) + self.get_price_tick( - vt_symbol) # 在当前时间点前发出的买入委托可能的最优成交价 - sell_best_cross_price = round_to(value=bar.open_price, - target=self.get_price_tick(vt_symbol)) - self.get_price_tick( - vt_symbol) # 在当前时间点前发出的卖出委托可能的最优成交价 - - # 判断是否会成交 - buy_cross = order.direction == Direction.LONG and order.price >= buy_cross_price - sell_cross = order.direction == Direction.SHORT and order.price <= sell_cross_price - - # 如果发生了成交 - if buy_cross or sell_cross: - # 推送成交数据 - self.trade_count += 1 # 成交编号自增1 - - trade_id = str(self.trade_count) - symbol, exchange = extract_vt_symbol(vt_symbol) - trade = TradeData( - gateway_name=self.gateway_name, - symbol=symbol, - exchange=exchange, - tradeid=trade_id, - orderid=order.orderid, - direction=order.direction, - offset=order.offset, - volume=order.volume, - time=self.last_dt.strftime("%Y-%m-%d %H:%M:%S"), - datetime=self.last_dt - ) - - # 以买入为例: - # 1. 假设当根K线的OHLC分别为:100, 125, 90, 110 - # 2. 假设在上一根K线结束(也是当前K线开始)的时刻,策略发出的委托为限价105 - # 3. 则在实际中的成交价会是100而不是105,因为委托发出时市场的最优价格是100 - if buy_cross: - trade_price = min(order.price, buy_best_cross_price) - - else: - trade_price = max(order.price, sell_best_cross_price) - trade.price = trade_price - - # 记录该合约来自哪个策略实例 - trade.strategy_name = strategy.strategy_name - - strategy.on_trade(trade) - - for cov_trade in self.convert_spd_trade(trade): - self.trade_dict[cov_trade.vt_tradeid] = cov_trade - self.trades[cov_trade.vt_tradeid] = copy.copy(cov_trade) - self.write_log(u'vt_trade_id:{0}'.format(cov_trade.vt_tradeid)) - - # 更新持仓缓存数据 - pos_buffer = self.pos_holding_dict.get(cov_trade.vt_symbol, None) - if not pos_buffer: - pos_buffer = PositionHolding(self.get_contract(vt_symbol)) - self.pos_holding_dict[cov_trade.vt_symbol] = pos_buffer - pos_buffer.update_trade(cov_trade) - self.write_log(u'{} : crossLimitOrder: TradeId:{}, posBuffer = {}'.format(cov_trade.strategy_name, - cov_trade.tradeid, - pos_buffer.to_str())) - - # 写入交易记录 - self.append_trade(cov_trade) - - # 更新资金曲线 - if 'SPD' not in cov_trade.vt_symbol: - fund_kline = self.get_fund_kline(cov_trade.strategy_name) - if fund_kline: - fund_kline.update_trade(cov_trade) - - # 推送委托数据 - order.traded = order.volume - order.status = Status.ALLTRADED - - strategy.on_order(order) - - # 从字典中删除该限价单 - self.active_limit_orders.pop(vt_orderid, None) - - # 实时计算模式 - self.realtime_calculate() - - def convert_spd_trade(self, trade): - """转换为品种对的交易记录""" - if trade.exchange != Exchange.SPD: - return [trade] - - try: - active_symbol, active_rate, passive_symbol, passive_rate, spd_type = trade.symbol.split('-') - active_rate = int(active_rate) - passive_rate = int(passive_rate) - active_exchange = self.get_exchange(active_symbol) - active_vt_symbol = active_symbol + '.' + active_exchange.value - passive_exchange = self.get_exchange(passive_symbol) - passive_vt_symbol = active_symbol + '.' + passive_exchange.value - # 主动腿成交记录 - act_trade = TradeData(gateway_name=self.gateway_name, - symbol=active_symbol, - exchange=active_exchange, - orderid='spd_' + str(trade.orderid), - tradeid='spd_act_' + str(trade.tradeid), - direction=trade.direction, - offset=trade.offset, - strategy_name=trade.strategy_name, - price=self.get_price(active_vt_symbol), - volume=int(trade.volume * active_rate), - time=trade.time, - datetime=trade.datetime - ) - - # 被动腿成交记录 - # 交易方向与spd合约方向相反 - pas_trade = TradeData(gateway_name=self.gateway_name, - symbol=passive_symbol, - exchange=passive_exchange, - orderid='spd_' + str(trade.orderid), - tradeid='spd_pas_' + str(trade.tradeid), - direction=Direction.LONG if trade.direction == Direction.SHORT else Direction.SHORT, - offset=trade.offset, - strategy_name=trade.strategy_name, - time=trade.time, - datetime=trade.datetime - ) - - # 根据套利合约的类型+主合约的价格,反向推导出被动合约的价格 - - if spd_type == 'BJ': - pas_trade.price = (act_trade.price * active_rate * 100 / trade.price) / passive_rate - else: - pas_trade.price = (act_trade.price * active_rate - trade.price) / passive_rate - - pas_trade.price = round_to(value=pas_trade.price, target=self.get_price_tick(pas_trade.vt_symbol)) - pas_trade.volume = int(trade.volume * passive_rate) - pas_trade.time = trade.time - - # 返回原交易记录,主动腿交易记录,被动腿交易记录 - return [trade, act_trade, pas_trade] - - except Exception as ex: - self.write_error(u'转换主动/被动腿异常:{}'.format(str(ex))) - return [trade] - - def update_pos_buffer(self): - """更新持仓信息,把今仓=>昨仓""" - - for k, v in self.pos_holding_dict.items(): - if v.long_td > 0: - self.write_log(u'调整多单持仓:今仓{}=> 0 昨仓{} => 昨仓:{}'.format(v.long_td, v.long_yd, v.long_pos)) - v.long_td = 0 - v.longYd = v.long_pos - - if v.short_td > 0: - self.write_log(u'调整空单持仓:今仓{}=> 0 昨仓{} => 昨仓:{}'.format(v.short_td, v.short_yd, v.short_pos)) - v.short_td = 0 - v.short_yd = v.short_pos - - def get_data_path(self): - """ - 获取数据保存目录 - :return: - """ - if self.data_path is not None: - data_folder = self.data_path - else: - data_folder = os.path.abspath(os.path.join(os.getcwd(), 'data')) - self.data_path = data_folder - if not os.path.exists(data_folder): - os.makedirs(data_folder) - return data_folder - - def get_logs_path(self): - """ - 获取日志保存目录 - :return: - """ - if self.logs_path is not None: - logs_folder = self.logs_path - else: - logs_folder = os.path.abspath(os.path.join(os.getcwd(), 'log')) - self.logs_path = logs_folder - - if not os.path.exists(logs_folder): - os.makedirs(logs_folder) - - return logs_folder - - def create_logger(self, strategy_name=None, debug=False): - """ - 创建日志 - :param strategy_name 策略实例名称 - :param debug:是否详细记录日志 - :return: - """ - if strategy_name is None: - filename = os.path.abspath(os.path.join(self.get_logs_path(), '{}'.format( - self.test_name if len(self.test_name) > 0 else 'portfolio_test'))) - print(u'create logger:{}'.format(filename)) - self.logger = setup_logger(file_name=filename, - name=self.test_name, - log_level=logging.DEBUG if debug else logging.ERROR, - backtesing=True) - else: - filename = os.path.abspath( - os.path.join(self.get_logs_path(), '{}_{}'.format(self.test_name, str(strategy_name)))) - print(u'create logger:{}'.format(filename)) - self.strategy_loggers[strategy_name] = setup_logger(file_name=filename, - name=str(strategy_name), - log_level=logging.DEBUG if debug else logging.ERROR, - backtesing=True) - - def write_log(self, msg: str, strategy_name: str = None, level: int = logging.DEBUG): - """记录日志""" - # log = str(self.datetime) + ' ' + content - # self.logList.append(log) - - if strategy_name is None: - # 写入本地log日志 - if self.logger: - self.logger.log(msg=msg, level=level) - else: - self.create_logger(debug=self.debug) - else: - if strategy_name in self.strategy_loggers: - self.strategy_loggers[strategy_name].log(msg=msg, level=level) - else: - self.create_logger(strategy_name=strategy_name, debug=self.debug) - - def write_error(self, msg, strategy_name=None): - """记录异常""" - - if strategy_name is None: - if self.logger: - self.logger.error(msg) - else: - self.create_logger(debug=self.debug) - else: - if strategy_name in self.strategy_loggers: - self.strategy_loggers[strategy_name].error(msg) - else: - self.create_logger(strategy_name=strategy_name, debug=self.debug) - try: - self.strategy_loggers[strategy_name].error(msg) - except Exception as ex: - print('{}'.format(datetime.now()), file=sys.stderr) - print('could not create cta logger for {},excption:{},trace:{}'.format(strategy_name, str(ex), - traceback.format_exc())) - print(msg, file=sys.stderr) - - def output(self, content): - """输出内容""" - print(self.test_name + "\t" + content) - - def realtime_calculate(self): - """实时计算交易结果 - 支持多空仓位并存""" - - if len(self.trade_dict) < 1: + if testdays < 1: + self.write_log(u'回测时间不足') return - # 获取所有未处理得成交单 - vt_tradeids = list(self.trade_dict.keys()) + gc_collect_days = 0 - result_list = [] # 保存交易记录 - longid = '' - shortid = '' + # 循环每一天 + for i in range(0, testdays): + test_day = self.data_start_date + timedelta(days=i) - # 对交易记录逐一处理 - for vt_tradeid in vt_tradeids: + combined_df = self.get_day_tick_df(test_day) - trade = self.trade_dict.pop(vt_tradeid, None) - if trade is None: + if combined_df is None: continue - if trade.volume == 0: - continue - # buy trade - if trade.direction == Direction.LONG and trade.offset == Offset.OPEN: - self.write_log(f'{trade.vt_symbol} buy, price:{trade.price},volume:{trade.volume}') - # 放入多单仓位队列 - self.long_position_list.append(trade) - - # cover trade, - elif trade.direction == Direction.LONG and trade.offset == Offset.CLOSE: - g_id = trade.vt_tradeid # 交易组(多个平仓数为一组) - g_result = None # 组合的交易结果 - - cover_volume = trade.volume - self.write_log(f'{trade.vt_symbol} cover:{cover_volume}') - while cover_volume > 0: - # 如果当前没有空单,属于异常行为 - if len(self.short_position_list) == 0: - self.write_error(u'异常!没有空单持仓,不能cover') - raise Exception(u'异常!没有空单持仓,不能cover') - return - - cur_short_pos_list = [s_pos.volume for s_pos in self.short_position_list] - - self.write_log(u'{}当前空单:{}'.format(trade.vt_symbol, cur_short_pos_list)) - if len(cur_short_pos_list) > 3: - a = 1 - # 来自同一策略,同一合约才能撮合 - pop_indexs = [i for i, val in enumerate(self.short_position_list) if - val.vt_symbol == trade.vt_symbol and val.strategy_name == trade.strategy_name] - - if len(pop_indexs) < 1: - self.write_error(u'异常,{}没有对应symbol:{}的空单持仓'.format(trade.strategy_name, trade.vt_symbol)) - raise Exception(u'realtimeCalculate2() Exception,没有对应symbol:{0}的空单持仓'.format(trade.vt_symbol)) - return - - pop_index = pop_indexs[0] - # 从未平仓的空头交易 - open_trade = self.short_position_list.pop(pop_index) - - # 开空volume,不大于平仓volume - if cover_volume >= open_trade.volume: - self.write_log(f'cover volume:{cover_volume}, 满足:{open_trade.volume}') - cover_volume = cover_volume - open_trade.volume - if cover_volume > 0: - self.write_log(u'剩余待平数量:{}'.format(cover_volume)) - - self.write_log( - f'{open_trade.vt_symbol} coverd, price: {trade.price},volume:{open_trade.volume}') - - result = TradingResult(open_price=open_trade.price, - open_datetime=open_trade.datetime, - exit_price=trade.price, - close_datetime=trade.datetime, - volume=-open_trade.volume, - rate=self.get_commission_rate(trade.vt_symbol), - slippage=self.get_slippage(trade.vt_symbol), - size=self.get_size(trade.vt_symbol), - group_id=g_id, - fix_commission=self.get_fix_commission(trade.vt_symbol)) - - t = OrderedDict() - t['gid'] = g_id - t['strategy'] = open_trade.strategy_name - t['vt_symbol'] = open_trade.vt_symbol - t['open_time'] = open_trade.time - t['open_price'] = open_trade.price - t['direction'] = u'Short' - t['close_time'] = trade.time - t['close_price'] = trade.price - t['volume'] = open_trade.volume - t['profit'] = result.pnl - t['commission'] = result.commission - self.trade_pnl_list.append(t) - - # 非自定义套利对,才更新到策略盈亏 - if not open_trade.vt_symbol.endswith('SPD'): - # 更新策略实例的累加盈亏 - self.pnl_strategy_dict.update( - {open_trade.strategy_name: self.pnl_strategy_dict.get(open_trade.strategy_name, - 0) + result.pnl}) - - msg = u'gid:{} {}[{}:开空tid={}:{}]-[{}.平空tid={},{},vol:{}],净盈亏pnl={},手续费:{}' \ - .format(g_id, open_trade.vt_symbol, open_trade.time, shortid, open_trade.price, - trade.time, vt_tradeid, trade.price, - open_trade.volume, result.pnl, result.commission) - - self.write_log(msg) - result_list.append(result) - - if g_result is None: - if cover_volume > 0: - # 属于组合 - g_result = copy.deepcopy(result) - - else: - # 更新组合的数据 - g_result.turnover = g_result.turnover + result.turnover - g_result.commission = g_result.commission + result.commission - g_result.slippage = g_result.slippage + result.slippage - g_result.pnl = g_result.pnl + result.pnl - - # 所有仓位平完 - if cover_volume == 0: - self.write_log(u'所有平空仓位撮合完毕') - g_result.volume = abs(trade.volume) - - # 开空volume,大于平仓volume,需要更新减少tradeDict的数量。 - else: - remain_volume = open_trade.volume - cover_volume - self.write_log(f'{open_trade.vt_symbol} short pos: {open_trade.volume} => {remain_volume}') - - result = TradingResult(open_price=open_trade.price, - open_datetime=open_trade.datetime, - exit_price=trade.price, - close_datetime=trade.datetime, - volume=-cover_volume, - rate=self.get_commission_rate(trade.vt_symbol), - slippage=self.get_slippage(trade.vt_symbol), - size=self.get_size(trade.vt_symbol), - group_id=g_id, - fix_commission=self.get_fix_commission(trade.vt_symbol)) - - t = OrderedDict() - t['gid'] = g_id - t['strategy'] = open_trade.strategy_name - t['vt_symbol'] = open_trade.vt_symbol - t['open_time'] = open_trade.time - t['open_price'] = open_trade.price - t['direction'] = u'Short' - t['close_time'] = trade.time - t['close_price'] = trade.price - t['volume'] = cover_volume - t['profit'] = result.pnl - t['commission'] = result.commission - self.trade_pnl_list.append(t) - - # 非自定义套利对,才更新盈亏 - if not (open_trade.vt_symbol.endswith('SPD') or open_trade.vt_symbol.endswith('SPD99')): - # 更新策略实例的累加盈亏 - self.pnl_strategy_dict.update( - {open_trade.strategy_name: self.pnl_strategy_dict.get(open_trade.strategy_name, - 0) + result.pnl}) - - msg = u'gid:{} {}[{}:开空tid={}:{}]-[{}.平空tid={},{},vol:{}],净盈亏pnl={},手续费:{}' \ - .format(g_id, open_trade.vt_symbol, open_trade.time, shortid, open_trade.price, - trade.time, vt_tradeid, trade.price, - cover_volume, result.pnl, result.commission) - - self.write_log(msg) - - # 更新(减少)开仓单的volume,重新推进开仓单列表中 - open_trade.volume = remain_volume - self.write_log(u'更新(减少)开仓单的volume,重新推进开仓单列表中:{}'.format(open_trade.volume)) - self.short_position_list.append(open_trade) - cur_short_pos_list = [s_pos.volume for s_pos in self.short_position_list] - self.write_log(u'当前空单:{}'.format(cur_short_pos_list)) - - cover_volume = 0 - result_list.append(result) - - if g_result is not None: - # 更新组合的数据 - g_result.turnover = g_result.turnover + result.turnover - g_result.commission = g_result.commission + result.commission - g_result.slippage = g_result.slippage + result.slippage - g_result.pnl = g_result.pnl + result.pnl - g_result.volume = abs(trade.volume) - - if g_result is not None: - self.write_log(u'组合净盈亏:{0}'.format(g_result.pnl)) - - # Short Trade - elif trade.direction == Direction.SHORT and trade.offset == Offset.OPEN: - self.write_log(f'{trade.vt_symbol}, short: price:{trade.price},volume{trade.volume}') - self.short_position_list.append(trade) - continue - - # sell trade - elif trade.direction == Direction.SHORT and trade.offset == Offset.CLOSE: - g_id = trade.vt_tradeid # 交易组(多个平仓数为一组) - g_result = None # 组合的交易结果 - - sell_volume = trade.volume - - while sell_volume > 0: - if len(self.long_position_list) == 0: - self.write_error(f'异常,没有{trade.vt_symbol}的多仓') - raise RuntimeError(u'realtimeCalculate2() Exception,没有开多单') - return - - pop_indexs = [i for i, val in enumerate(self.long_position_list) if - val.vt_symbol == trade.vt_symbol and val.strategy_name == trade.strategy_name] - if len(pop_indexs) < 1: - self.write_error(f'没有{trade.strategy_name}对应的symbol{trade.vt_symbol}多单数据,') - raise RuntimeError( - f'realtimeCalculate2() Exception,没有对应的symbol{trade.vt_symbol}多单数据,') - return - - cur_long_pos_list = [s_pos.volume for s_pos in self.long_position_list] - - self.write_log(u'{}当前多单:{}'.format(trade.vt_symbol, cur_long_pos_list)) - if len(cur_long_pos_list) > 3: - a = 1 - - pop_index = pop_indexs[0] - open_trade = self.long_position_list.pop(pop_index) - # 开多volume,不大于平仓volume - if sell_volume >= open_trade.volume: - self.write_log(f'{open_trade.vt_symbol},Sell Volume:{sell_volume} 满足:{open_trade.volume}') - sell_volume = sell_volume - open_trade.volume - - self.write_log(f'{open_trade.vt_symbol},sell, price:{trade.price},volume:{open_trade.volume}') - - result = TradingResult(open_price=open_trade.price, - open_datetime=open_trade.datetime, - exit_price=trade.price, - close_datetime=trade.datetime, - volume=open_trade.volume, - rate=self.get_commission_rate(trade.vt_symbol), - slippage=self.get_slippage(trade.vt_symbol), - size=self.get_size(trade.vt_symbol), - group_id=g_id, - fix_commission=self.get_fix_commission(trade.vt_symbol)) - - t = OrderedDict() - t['gid'] = g_id - t['strategy'] = open_trade.strategy_name - t['vt_symbol'] = open_trade.vt_symbol - t['open_time'] = open_trade.time - t['open_price'] = open_trade.price - t['direction'] = u'Long' - t['close_time'] = trade.time - t['close_price'] = trade.price - t['volume'] = open_trade.volume - t['profit'] = result.pnl - t['commission'] = result.commission - self.trade_pnl_list.append(t) - - # 非自定义套利对,才更新盈亏 - if not (open_trade.vt_symbol.endswith('SPD') or open_trade.vt_symbol.endswith('SPD99')): - # 更新策略实例的累加盈亏 - self.pnl_strategy_dict.update( - {open_trade.strategy_name: self.pnl_strategy_dict.get(open_trade.strategy_name, - 0) + result.pnl}) - - msg = u'gid:{} {}[{}:开多tid={}:{}]-[{}.平多tid={},{},vol:{}],净盈亏pnl={},手续费:{}' \ - .format(g_id, open_trade.vt_symbol, - open_trade.time, longid, open_trade.price, - trade.time, vt_tradeid, trade.price, - open_trade.volume, result.pnl, result.commission) - - self.write_log(msg) - result_list.append(result) - - if g_result is None: - if sell_volume > 0: - # 属于组合 - g_result = copy.deepcopy(result) - else: - # 更新组合的数据 - g_result.turnover = g_result.turnover + result.turnover - g_result.commission = g_result.commission + result.commission - g_result.slippage = g_result.slippage + result.slippage - g_result.pnl = g_result.pnl + result.pnl - - if sell_volume == 0: - g_result.volume = abs(trade.volume) - - # 开多volume,大于平仓volume,需要更新减少tradeDict的数量。 - else: - remain_volume = open_trade.volume - sell_volume - self.write_log(f'{open_trade.vt_symbol} short pos: {open_trade.volume} => {remain_volume}') - - result = TradingResult(open_price=open_trade.price, - open_datetime=open_trade.datetime, - exit_price=trade.price, - close_datetime=trade.datetime, - volume=sell_volume, - rate=self.get_commission_rate(trade.vt_symbol), - slippage=self.get_slippage(trade.vt_symbol), - size=self.get_size(trade.vt_symbol), - group_id=g_id, - fix_commission=self.get_fix_commission(trade.vt_symbol)) - - t = OrderedDict() - t['gid'] = g_id - t['strategy'] = open_trade.strategy_name - t['vt_symbol'] = open_trade.vt_symbol - t['open_time'] = open_trade.time - t['open_price'] = open_trade.price - t['direction'] = u'Long' - t['close_time'] = trade.time - t['close_price'] = trade.price - t['volume'] = sell_volume - t['profit'] = result.pnl - t['commission'] = result.commission - self.trade_pnl_list.append(t) - - # 非自定义套利对,才更新盈亏 - if not (open_trade.vt_symbol.endswith('SPD') or open_trade.vt_symbol.endswith('SPD99')): - # 更新策略实例的累加盈亏 - self.pnl_strategy_dict.update( - {open_trade.strategy_name: self.pnl_strategy_dict.get(open_trade.strategy_name, - 0) + result.pnl}) - - msg = u'Gid:{} {}[{}:开多tid={}:{}]-[{}.平多tid={},{},vol:{}],净盈亏pnl={},手续费:{}' \ - .format(g_id, open_trade.vt_symbol, open_trade.time, longid, open_trade.price, - trade.time, vt_tradeid, trade.price, sell_volume, result.pnl, - result.commission) - - self.write_log(msg) - - # 减少开多volume,重新推进多单持仓列表中 - open_trade.volume = remain_volume - self.long_position_list.append(open_trade) - - sell_volume = 0 - result_list.append(result) - - if g_result is not None: - # 更新组合的数据 - g_result.turnover = g_result.turnover + result.turnover - g_result.commission = g_result.commission + result.commission - g_result.slippage = g_result.slippage + result.slippage - g_result.pnl = g_result.pnl + result.pnl - g_result.volume = abs(trade.volume) - - if g_result is not None: - self.write_log(u'组合净盈亏:{0}'.format(g_result.pnl)) - - # 计算仓位比例 - occupy_money = 0.0 # 保证金 - occupy_long_money_dict = {} # 多单保证金,key为合约短号,value为保证金 - occupy_short_money_dict = {} # 空单保证金,key为合约短号,value为保证金 - occupy_underly_symbol_set = set() # 所有合约短号 - - long_pos_dict = {} - short_pos_dict = {} - if len(self.long_position_list) > 0: - for t in self.long_position_list: - # 不计算套利合约的持仓占用保证金 - if t.vt_symbol.endswith('SPD') or t.vt_symbol.endswith('SPD99'): - continue - # 当前持仓的保证金 - if self.use_margin: - cur_occupy_money = t.price * abs(t.volume) * self.get_size(t.vt_symbol) * self.get_margin_rate( - t.vt_symbol) - else: - cur_occupy_money = self.get_price(t.vt_symbol) * abs(t.volume) * self.get_size( - t.vt_symbol) * self.get_margin_rate(t.vt_symbol) - - # 更新该合约短号的累计保证金 - underly_symbol = get_underlying_symbol(t.symbol) - occupy_underly_symbol_set.add(underly_symbol) - occupy_long_money_dict.update( - {underly_symbol: occupy_long_money_dict.get(underly_symbol, 0) + cur_occupy_money}) - - if t.vt_symbol in long_pos_dict: - long_pos_dict[t.vt_symbol] += abs(t.volume) - else: - long_pos_dict[t.vt_symbol] = abs(t.volume) - - if len(self.short_position_list) > 0: - for t in self.short_position_list: - # 不计算套利合约的持仓占用保证金 - if t.vt_symbol.endswith('SPD') or t.vt_symbol.endswith('SPD99'): - continue - # 当前空单保证金 - if self.use_margin: - cur_occupy_money = max(self.get_price(t.vt_symbol), t.price) * abs(t.volume) * self.get_size( - t.vt_symbol) * self.get_margin_rate(t.vt_symbol) - else: - cur_occupy_money = self.get_price(t.vt_symbol) * abs(t.volume) * self.get_size( - t.vt_symbol) * self.get_margin_rate(t.vt_symbol) - - # 该合约短号的累计空单保证金 - underly_symbol = get_underlying_symbol(t.symbol) - occupy_underly_symbol_set.add(underly_symbol) - occupy_short_money_dict.update( - {underly_symbol: occupy_short_money_dict.get(underly_symbol, 0) + cur_occupy_money}) - - if t.vt_symbol in short_pos_dict: - short_pos_dict[t.vt_symbol] += abs(t.volume) - else: - short_pos_dict[t.vt_symbol] = abs(t.volume) - - # 计算多空的保证金累加(对锁的取最大值) - for underly_symbol in occupy_underly_symbol_set: - occupy_money += max(occupy_long_money_dict.get(underly_symbol, 0), - occupy_short_money_dict.get(underly_symbol, 0)) - - # 可用资金 = 当前净值 - 占用保证金 - self.avaliable = self.net_capital - occupy_money - # 当前保证金占比 - self.percent = round(float(occupy_money * 100 / self.net_capital), 2) - # 更新最大保证金占比 - self.max_occupy_rate = max(self.max_occupy_rate, self.percent) - - # 检查是否有平交易 - if len(result_list) == 0: - msg = u'' - if len(self.long_position_list) > 0: - msg += u'持多仓{0},'.format(str(long_pos_dict)) - - if len(self.short_position_list) > 0: - msg += u'持空仓{0},'.format(str(short_pos_dict)) - - msg += u'资金占用:{0},仓位:{1}%%'.format(occupy_money, self.percent) - - self.write_log(msg) - return - - # 对交易结果汇总统计 - for result in result_list: - if result.pnl > 0: - self.winning_result += 1 - self.total_winning += result.pnl - else: - self.losing_result += 1 - self.total_losing += result.pnl - self.cur_capital += result.pnl - self.max_capital = max(self.cur_capital, self.max_capital) - self.net_capital = max(self.net_capital, self.cur_capital) - self.max_net_capital = max(self.net_capital, self.max_net_capital) - # self.maxVolume = max(self.maxVolume, result.volume) - drawdown = self.net_capital - self.max_net_capital - drawdown_rate = round(float(drawdown * 100 / self.max_net_capital), 4) - - self.pnl_list.append(result.pnl) - self.time_list.append(result.close_datetime) - self.capital_list.append(self.cur_capital) - self.drawdown_list.append(drawdown) - self.drawdown_rate_list.append(drawdown_rate) - - self.total_trade_count += 1 - self.total_turnover += result.turnover - self.total_commission += result.commission - self.total_slippage += result.slippage - - msg = u'[gid:{}] {} 交易盈亏:{},交易手续费:{}回撤:{}/{},账号平仓权益:{},持仓权益:{},累计手续费:{}' \ - .format(result.group_id, result.close_datetime, result.pnl, result.commission, drawdown, - drawdown_rate, self.cur_capital, self.net_capital, self.total_commission) - - self.write_log(msg) - - # 重新计算一次avaliable - self.avaliable = self.net_capital - occupy_money - self.percent = round(float(occupy_money * 100 / self.net_capital), 2) - - def saving_daily_data(self, d, c, m, commission, benchmark=0): - """保存每日数据""" - dict = {} - dict['date'] = d.strftime('%Y/%m/%d') # 日期 - dict['capital'] = c # 当前平仓净值 - dict['max_capital'] = m # 之前得最高净值 - today_holding_profit = 0 # 持仓浮盈 - long_pos_occupy_money = 0 - short_pos_occupy_money = 0 - strategy_pnl = {} - for strategy in self.strategies.keys(): - strategy_pnl.update({strategy: self.pnl_strategy_dict.get(strategy, 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 - - positionMsg = "" - for longpos in self.long_position_list: - # 不计算套利合约的持仓盈亏 - if longpos.vt_symbol.endswith('SPD') or longpos.vt_symbol.endswith('SPD99'): - continue - symbol = longpos.vt_symbol - # 计算持仓浮盈浮亏/占用保证金 - holding_profit = 0 - last_price = self.get_price(symbol) - if last_price is not None: - holding_profit = (last_price - longpos.price) * longpos.volume * self.get_size(symbol) - long_pos_occupy_money += last_price * abs(longpos.volume) * self.get_size( - symbol) * self.get_margin_rate(symbol) - - # 账号的持仓盈亏 - today_holding_profit += holding_profit - - # 计算每个策略实例的持仓盈亏 - strategy_pnl.update({longpos.strategy_name: strategy_pnl.get(longpos.strategy_name, 0) + holding_profit}) - - positionMsg += "{},long,p={},v={},m={};".format(symbol, longpos.price, longpos.volume, holding_profit) - - for shortpos in self.short_position_list: - # 不计算套利合约的持仓盈亏 - if shortpos.vt_symbol.endswith('SPD') or shortpos.vt_symbol.endswith('SPD99'): - continue - symbol = shortpos.vt_symbol - # 计算持仓浮盈浮亏/占用保证金 - holding_profit = 0 - last_price = self.get_price(symbol) - if last_price is not None: - holding_profit = (shortpos.price - last_price) * shortpos.volume * self.get_size(symbol) - short_pos_occupy_money += last_price * abs(shortpos.volume) * self.get_size( - symbol) * self.get_margin_rate(symbol) - - # 账号的持仓盈亏 - today_holding_profit += holding_profit - # 计算每个策略实例的持仓盈亏 - strategy_pnl.update({shortpos.strategy_name: strategy_pnl.get(shortpos.strategy_name, 0) + holding_profit}) - - positionMsg += "{},short,p={},v={},m={};".format(symbol, shortpos.price, shortpos.volume, holding_profit) - - dict['net'] = c + today_holding_profit # 当日净值(含持仓盈亏) - dict['rate'] = (c + today_holding_profit) / self.init_capital - dict['occupy_money'] = max(long_pos_occupy_money, short_pos_occupy_money) - dict['occupy_rate'] = dict['occupy_money'] / dict['capital'] - dict['commission'] = commission - dict['benchmark'] = benchmark - - dict.update(strategy_pnl) - - self.daily_list.append(dict) - - # 更新每日浮动净值 - self.net_capital = dict['net'] - - # 更新最大初次持仓浮盈净值 - if dict['net'] > self.max_net_capital: - self.max_net_capital = dict['net'] - self.max_net_capital_time = dict['date'] - drawdown_rate = round((float(self.max_net_capital - dict['net']) * 100) / self.max_net_capital, 4) - if drawdown_rate > self.daily_max_drawdown_rate: - self.daily_max_drawdown_rate = drawdown_rate - self.max_drawdown_rate_time = dict['date'] - - self.write_log(u'{}: net={}, capital={} max={} margin={} commission={}, pos: {}' - .format( - dict['date'], - dict['net'], c, m, today_holding_profit, commission, - positionMsg)) - - # --------------------------------------------------------------------- - def export_trade_result(self, is_plot_daily=False): - """ - 导出交易结果(开仓-》平仓, 平仓收益) - 导出每日净值结果表 - :return: - """ - if len(self.trade_pnl_list) == 0: - self.write_log('no traded records') - return - - s = self.test_name.replace('&', '') - s = s.replace(' ', '') - trade_list_csv_file = os.path.abspath(os.path.join(self.get_logs_path(), '{}_trade_list.csv'.format(s))) - - self.write_log(u'save trade records to:{}'.format(trade_list_csv_file)) - import csv - csv_write_file = open(trade_list_csv_file, 'w', encoding='utf8', newline='') - - fieldnames = ['gid', 'strategy', 'vt_symbol', 'open_time', 'open_price', 'direction', 'close_time', - 'close_price', - 'volume', 'profit', 'commission'] - writer = csv.DictWriter(f=csv_write_file, fieldnames=fieldnames, dialect='excel') - writer.writeheader() - - for row in self.trade_pnl_list: - writer.writerow(row) - - # 导出每日净值记录表 - if not self.daily_list: - return - - if self.daily_report_name == '': - daily_csv_file = os.path.abspath(os.path.join(self.get_logs_path(), '{}_daily_list.csv'.format(s))) - else: - daily_csv_file = self.daily_report_name - self.write_log(u'save daily records to:{}'.format(daily_csv_file)) - - csv_write_file2 = open(daily_csv_file, 'w', encoding='utf8', newline='') - fieldnames = ['date', 'capital', 'net', 'max_capital', 'rate', 'commission', 'long_money', 'short_money', - 'occupy_money', 'occupy_rate', 'today_margin_long', 'today_margin_short', 'benchmark'] - fieldnames.extend(sorted(self.strategies.keys())) - writer2 = csv.DictWriter(f=csv_write_file2, fieldnames=fieldnames, dialect='excel') - writer2.writeheader() - - for row in self.daily_list: - writer2.writerow(row) - - if is_plot_daily: - # 生成净值曲线图片 - df = pd.DataFrame(self.daily_list) - df = df.set_index('date') - from vnpy.trader.utility import display_dual_axis - plot_file = os.path.abspath(os.path.join(self.get_logs_path(), '{}_plot.png'.format(s))) - - # 双坐标输出,左侧坐标是净值(比率),右侧是各策略的实际资金收益曲线 - display_dual_axis(df=df, columns1=['rate'], columns2=list(self.strategies.keys()), image_name=plot_file) - - return - - def get_result(self): - # 返回回测结果 - d = {} - d['init_capital'] = self.init_capital - d['profit'] = self.cur_capital - self.init_capital - d['max_capital'] = self.max_net_capital # 取消原 maxCapital - - if len(self.pnl_list) == 0: - return {}, [], [] - - d['max_pnl'] = max(self.pnl_list) - d['min_pnl'] = min(self.pnl_list) - - d['max_occupy_rate'] = self.max_occupy_rate - d['total_trade_count'] = self.total_trade_count - d['total_turnover'] = self.total_turnover - d['total_commission'] = self.total_commission - d['total_slippage'] = self.total_slippage - d['time_list'] = self.time_list - d['pnl_list'] = self.pnl_list - d['capital_list'] = self.capital_list - d['drawdown_list'] = self.drawdown_list - d['drawdown_rate_list'] = self.drawdown_rate_list # 净值最大回撤率列表 - d['winning_rate'] = round(100 * self.winning_result / len(self.pnl_list), 4) - - average_winning = 0 # 这里把数据都初始化为0 - average_losing = 0 - profit_loss_ratio = 0 - - if self.winning_result: - average_winning = self.total_winning / self.winning_result # 平均每笔盈利 - if self.losing_result: - average_losing = self.total_losing / self.losing_result # 平均每笔亏损 - if average_losing: - profit_loss_ratio = -average_winning / average_losing # 盈亏比 - - d['average_winning'] = average_winning - d['average_losing'] = average_losing - d['profit_loss_ratio'] = profit_loss_ratio - - # 计算Sharp - if not self.daily_list: - return {}, [], [] - - capital_net_list = [] - capital_list = [] - for row in self.daily_list: - capital_net_list.append(row['net']) - capital_list.append(row['capital']) - - capital = pd.Series(capital_net_list) - log_returns = np.log(capital).diff().fillna(0) - sharpe = (log_returns.mean() * 252) / (log_returns.std() * np.sqrt(252)) - d['sharpe'] = sharpe - - return d, capital_net_list, capital_list - - def show_backtesting_result(self, is_plot_daily=False): - """显示回测结果""" - - d, daily_net_capital, daily_capital = self.get_result() - - if len(d) == 0: - self.output(u'无交易结果') - return {}, '' - - # 导出交易清单 - self.export_trade_result(is_plot_daily) - - result_info = OrderedDict() - - # 输出 - self.output('-' * 30) - result_info.update({u'第一笔交易': str(d['time_list'][0])}) - self.output(u'第一笔交易:\t%s' % d['time_list'][0]) - - result_info.update({u'最后一笔交易': str(d['time_list'][-1])}) - self.output(u'最后一笔交易:\t%s' % d['time_list'][-1]) - - result_info.update({u'总交易次数': d['total_trade_count']}) - self.output(u'总交易次数:\t%s' % format_number(d['total_trade_count'])) - - result_info.update({u'期初资金': d['init_capital']}) - self.output(u'期初资金:\t%s' % format_number(d['init_capital'])) - - result_info.update({u'总盈亏': d['profit']}) - self.output(u'总盈亏:\t%s' % format_number(d['profit'])) - - result_info.update({u'资金最高净值': d['max_capital']}) - self.output(u'资金最高净值:\t%s' % format_number(d['max_capital'])) - - result_info.update({u'资金最高净值时间': str(self.max_net_capital_time)}) - self.output(u'资金最高净值时间:\t%s' % self.max_net_capital_time) - - result_info.update({u'每笔最大盈利': d['max_pnl']}) - self.output(u'每笔最大盈利:\t%s' % format_number(d['max_pnl'])) - - result_info.update({u'每笔最大亏损': d['min_pnl']}) - self.output(u'每笔最大亏损:\t%s' % format_number(d['min_pnl'])) - - result_info.update({u'净值最大回撤': min(d['drawdown_list'])}) - self.output(u'净值最大回撤: \t%s' % format_number(min(d['drawdown_list']))) - - result_info.update({u'净值最大回撤率': self.daily_max_drawdown_rate}) - self.output(u'净值最大回撤率: \t%s' % format_number(self.daily_max_drawdown_rate)) - - result_info.update({u'净值最大回撤时间': str(self.max_drawdown_rate_time)}) - self.output(u'净值最大回撤时间:\t%s' % self.max_drawdown_rate_time) - - result_info.update({u'胜率': d['winning_rate']}) - self.output(u'胜率:\t%s' % format_number(d['winning_rate'])) - - result_info.update({u'盈利交易平均值': d['average_winning']}) - self.output(u'盈利交易平均值\t%s' % format_number(d['average_winning'])) - - result_info.update({u'亏损交易平均值': d['average_losing']}) - self.output(u'亏损交易平均值\t%s' % format_number(d['average_losing'])) - - result_info.update({u'盈亏比': d['profit_loss_ratio']}) - self.output(u'盈亏比:\t%s' % format_number(d['profit_loss_ratio'])) - - result_info.update({u'最大资金占比': d['max_occupy_rate']}) - self.output(u'最大资金占比:\t%s' % format_number(d['max_occupy_rate'])) - - result_info.update({u'平均每笔盈利': d['profit'] / d['total_trade_count']}) - self.output(u'平均每笔盈利:\t%s' % format_number(d['profit'] / d['total_trade_count'])) - - result_info.update({u'平均每笔滑点成本': d['total_slippage'] / d['total_trade_count']}) - self.output(u'平均每笔滑点成本:\t%s' % format_number(d['total_slippage'] / d['total_trade_count'])) - - result_info.update({u'平均每笔佣金': d['total_commission'] / d['total_trade_count']}) - self.output(u'平均每笔佣金:\t%s' % format_number(d['total_commission'] / d['total_trade_count'])) - - result_info.update({u'Sharpe Ratio': d['sharpe']}) - self.output(u'Sharpe Ratio:\t%s' % format_number(d['sharpe'])) - - return result_info - - def put_strategy_event(self, strategy: CtaTemplate): - """发送策略更新事件,回测中忽略""" - pass - - def clear_backtesting_result(self): - """清空之前回测的结果""" - # 清空限价单相关 - self.limit_order_count = 0 - self.limit_orders.clear() - self.active_limit_orders.clear() - - # 清空成交相关 - self.trade_count = 0 - self.trade_dict.clear() - self.trades.clear() - self.trade_pnl_list = [] - - def append_trade(self, trade: TradeData): - # 根据策略名称,写入 logs\test_name_straetgy_name_trade.csv文件 - strategy_name = getattr(trade, 'strategy', self.test_name) - trade_fields = ['symbol', 'exchange', 'vt_symbol', 'tradeid', 'vt_tradeid', 'orderid', 'vt_orderid', - 'direction', - 'offset', 'price', 'volume', 'time'] - - d = OrderedDict() - try: - for k in trade_fields: - if k in ['exchange', 'direction', 'offset']: - d[k] = getattr(trade, k).value - else: - d[k] = getattr(trade, k, '') - - trade_file = os.path.abspath(os.path.join(self.get_logs_path(), '{}_trade.csv'.format(strategy_name))) - self.append_data(file_name=trade_file, dict_data=d) - except Exception as ex: - self.write_error(u'写入交易记录csv出错:{},{}'.format(str(ex), traceback.format_exc())) - - # 保存记录相关 - def append_data(self, file_name: str, dict_data: OrderedDict, field_names: list = None): - """ - 添加数据到csv文件中 - :param file_name: csv的文件全路径 - :param dict_data: OrderedDict - :return: - """ - if field_names is None or field_names == []: - dict_fieldnames = list(dict_data.keys()) - else: - dict_fieldnames = field_names - - try: - if not os.path.exists(file_name): - self.write_log(u'create csv file:{}'.format(file_name)) - with open(file_name, 'a', encoding='utf8', newline='') as csvWriteFile: - writer = csv.DictWriter(f=csvWriteFile, fieldnames=dict_fieldnames, dialect='excel') - self.write_log(u'write csv header:{}'.format(dict_fieldnames)) - writer.writeheader() - writer.writerow(dict_data) - else: - with open(file_name, 'a', encoding='utf8', newline='') as csvWriteFile: - writer = csv.DictWriter(f=csvWriteFile, fieldnames=dict_fieldnames, dialect='excel', - extrasaction='ignore') - writer.writerow(dict_data) - except Exception as ex: - self.write_error(u'append_data exception:{}'.format(str(ex))) - - -######################################################################## -class TradingResult(object): - """每笔交易的结果""" - - def __init__(self, open_price, open_datetime, exit_price, close_datetime, volume, rate, slippage, size, group_id, - fix_commission=0.0): - """Constructor""" - self.open_price = open_price # 开仓价格 - self.exit_price = exit_price # 平仓价格 - - self.open_datetime = open_datetime # 开仓时间datetime - self.close_datetime = close_datetime # 平仓时间 - - self.volume = volume # 交易数量(+/-代表方向) - self.group_id = group_id # 主交易ID(针对多手平仓) - - self.turnover = (self.open_price + self.exit_price) * size * abs(volume) # 成交金额 - if fix_commission > 0: - self.commission = fix_commission * abs(self.volume) - else: - self.commission = abs(self.turnover * rate) # 手续费成本 - self.slippage = slippage * 2 * size * abs(volume) # 滑点成本 - self.pnl = ((self.exit_price - self.open_price) * volume * size - - self.commission - self.slippage) # 净盈亏 + try: + for (dt, vt_symbol), bar_data in combined_df.iterrows(): + symbol, exchange = extract_vt_symbol(vt_symbol) + tick = TickData( + gateway_name='backtesting', + symbol=symbol, + exchange=exchange, + datetime=dt, + date=dt.strftime('%Y-%m-%d'), + time=dt.strftime('%H:%M:%S.%f'), + trading_day=test_day.strftime('%Y-%m-%d'), + last_price=bar_data['price'], + volume=bar_data['volume'] + ) + + self.new_tick(tick) + + # 结束一个交易日后,更新每日净值 + self.saving_daily_data(test_day, + self.cur_capital, + self.max_net_capital, + self.total_commission) + + self.cancel_orders() + # 更新持仓缓存 + self.update_pos_buffer() + + gc_collect_days += 1 + if gc_collect_days >= 10: + # 执行内存回收 + gc.collect() + sleep(1) + gc_collect_days = 0 + + if self.net_capital < 0: + self.write_error(u'净值低于0,回测停止') + self.output(u'净值低于0,回测停止') + return + + except Exception as ex: + self.write_error(u'回测异常导致停止:{}'.format(str(ex))) + self.write_error(u'{},{}'.format(str(ex), traceback.format_exc())) + print(str(ex), file=sys.stderr) + traceback.print_exc() + return + + self.write_log(u'tick数据回放完成') def single_test(test_setting: dict, strategy_setting: dict): @@ -2346,27 +512,19 @@ def single_test(test_setting: dict, strategy_setting: dict): : test_setting, 组合回测所需的配置,包括合约信息,数据bar信息,回测时间,资金等。 :strategy_setting, dict, 一个或多个策略配置 """ - # 创建事件引擎 - from vnpy.event.engine import EventEngine - event_engine = EventEngine() - event_engine.start() - # 创建组合回测引擎 - engine = PortfolioTestingEngine(event_engine) + engine = PortfolioTestingEngine() engine.prepare_env(test_setting) try: engine.run_portfolio_test(strategy_setting) # 回测结果,保存 - result_info = engine.show_backtesting_result(is_plot_daily=test_setting.get('is_plot_daily', False)) + engine.show_backtesting_result() except Exception as ex: print('组合回测异常{}'.format(str(ex))) traceback.print_exc() return False - if event_engine: - event_engine.stop() - print('测试结束') return True diff --git a/vnpy/app/cta_strategy_pro/strategies/turtle_signal_strategy.py b/vnpy/app/cta_strategy_pro/strategies/turtle_signal_strategy.py index 0e79cd6a..87497ab6 100644 --- a/vnpy/app/cta_strategy_pro/strategies/turtle_signal_strategy.py +++ b/vnpy/app/cta_strategy_pro/strategies/turtle_signal_strategy.py @@ -2,7 +2,6 @@ from vnpy.app.cta_strategy_pro import ( CtaTemplate, StopOrder, Direction, - Offset, TickData, BarData, TradeData, @@ -112,7 +111,7 @@ class TurtleSignalStrategy(CtaTemplate): self.send_short_orders(self.entry_down) cover_price = min(self.short_stop, self.exit_up) - ret = self.cover(cover_price, abs(self.pos), True) + self.cover(cover_price, abs(self.pos), True) self.put_event() @@ -149,7 +148,7 @@ class TurtleSignalStrategy(CtaTemplate): if self.pos >= 4: return - if self.cur_mi_price <= price - self.atr_value/2: + if self.cur_mi_price <= price - self.atr_value / 2: return t = self.pos / self.fixed_size diff --git a/vnpy/app/cta_strategy_pro/strategies/turtle_signal_strategy_v2.py b/vnpy/app/cta_strategy_pro/strategies/turtle_signal_strategy_v2.py index 56830643..d414e065 100644 --- a/vnpy/app/cta_strategy_pro/strategies/turtle_signal_strategy_v2.py +++ b/vnpy/app/cta_strategy_pro/strategies/turtle_signal_strategy_v2.py @@ -2,7 +2,6 @@ from vnpy.app.cta_strategy_pro import ( CtaTemplate, StopOrder, Direction, - Offset, TickData, BarData, TradeData, @@ -10,7 +9,6 @@ from vnpy.app.cta_strategy_pro import ( BarGenerator, ArrayManager, ) - from vnpy.trader.utility import round_to @@ -189,7 +187,7 @@ class TurtleSignalStrategy_v2(CtaTemplate): self.write_log(f'买入委托编号:{refs}') if t == 1 and self.cur_mi_price > price: - buy_price = round_to(price + self.atr_value * 0.5 , self.symbol_price_tick) + buy_price = round_to(price + self.atr_value * 0.5, self.symbol_price_tick) self.write_log(u'发出做多停止单,触发价格为: {}'.format(buy_price)) refs = self.buy(buy_price, self.invest_pos, True) if len(refs) > 0: diff --git a/vnpy/app/cta_strategy_pro/template.py b/vnpy/app/cta_strategy_pro/template.py index f1731777..1f24f9d0 100644 --- a/vnpy/app/cta_strategy_pro/template.py +++ b/vnpy/app/cta_strategy_pro/template.py @@ -3,7 +3,6 @@ import os import uuid import bz2 import pickle -import copy import traceback from abc import ABC @@ -11,12 +10,12 @@ from copy import copy from typing import Any, Callable from logging import INFO, ERROR from datetime import datetime -from vnpy.trader.constant import Interval, Direction, Offset, Status +from vnpy.trader.constant import Interval, Direction, Offset, Status, OrderType from vnpy.trader.object import BarData, TickData, OrderData, TradeData -from vnpy.trader.utility import virtual, append_data, extract_vt_symbol,get_underlying_symbol +from vnpy.trader.utility import virtual, append_data, extract_vt_symbol, get_underlying_symbol from .base import StopOrder, EngineType -from .cta_grid_trade import CtaGrid, CtaGridTrade +from .cta_grid_trade import CtaGrid, CtaGridTrade, LOCK_GRID from .cta_position import CtaPosition @@ -27,6 +26,11 @@ class CtaTemplate(ABC): parameters = [] variables = [] + # 保存委托单编号和相关委托单的字典 + # key为委托单编号 + # value为该合约相关的委托单 + active_orders = {} + def __init__( self, cta_engine: Any, @@ -46,11 +50,6 @@ class CtaTemplate(ABC): self.tick_dict = {} # 记录所有on_tick传入最新tick - # 保存委托单编号和相关委托单的字典 - # key为委托单编号 - # value为该合约相关的委托单 - self.active_orders = {} - # Copy a new variables list here to avoid duplicate insert when multiple # strategy instances are created with the same strategy class. self.variables = copy(self.variables) @@ -187,11 +186,15 @@ class CtaTemplate(ABC): pass def buy(self, price: float, volume: float, stop: bool = False, lock: bool = False, - vt_symbol: str = '', order_time: datetime = None, grid: CtaGrid = None): + vt_symbol: str = '', order_type: OrderType = OrderType.LIMIT, + order_time: datetime = None, grid: CtaGrid = None): """ Send buy order to open a long position. """ - + if OrderType in [OrderType.FAK, OrderType.FOK]: + if self.is_upper_limit(vt_symbol): + self.write_error(u'涨停价不做FAK/FOK委托') + return [] return self.send_order(vt_symbol=vt_symbol, direction=Direction.LONG, offset=Offset.OPEN, @@ -199,14 +202,20 @@ class CtaTemplate(ABC): volume=volume, stop=stop, lock=lock, + order_type=order_type, order_time=order_time, grid=grid) def sell(self, price: float, volume: float, stop: bool = False, lock: bool = False, - vt_symbol: str = '', order_time: datetime = None, grid: CtaGrid = None): + vt_symbol: str = '', order_type: OrderType = OrderType.LIMIT, + order_time: datetime = None, grid: CtaGrid = None): """ Send sell order to close a long position. """ + if OrderType in [OrderType.FAK, OrderType.FOK]: + if self.is_lower_limit(vt_symbol): + self.write_error(u'跌停价不做FAK/FOK sell委托') + return [] return self.send_order(vt_symbol=vt_symbol, direction=Direction.SHORT, offset=Offset.CLOSE, @@ -214,14 +223,20 @@ class CtaTemplate(ABC): volume=volume, stop=stop, lock=lock, + order_type=order_type, order_time=order_time, grid=grid) def short(self, price: float, volume: float, stop: bool = False, lock: bool = False, - vt_symbol: str = '', order_time: datetime = None, grid: CtaGrid = None): + vt_symbol: str = '', order_type: OrderType = OrderType.LIMIT, + order_time: datetime = None, grid: CtaGrid = None): """ Send short order to open as short position. """ + if OrderType in [OrderType.FAK, OrderType.FOK]: + if self.is_lower_limit(vt_symbol): + self.write_error(u'跌停价不做FAK/FOK short委托') + return [] return self.send_order(vt_symbol=vt_symbol, direction=Direction.SHORT, offset=Offset.OPEN, @@ -229,14 +244,20 @@ class CtaTemplate(ABC): volume=volume, stop=stop, lock=lock, + order_type=order_type, order_time=order_time, grid=grid) def cover(self, price: float, volume: float, stop: bool = False, lock: bool = False, - vt_symbol: str = '', order_time: datetime = None, grid: CtaGrid = None): + vt_symbol: str = '', order_type: OrderType = OrderType.LIMIT, + order_time: datetime = None, grid: CtaGrid = None): """ Send cover order to close a short position. """ + if OrderType in [OrderType.FAK, OrderType.FOK]: + if self.is_upper_limit(vt_symbol): + self.write_error(u'涨停价不做FAK/FOK cover委托') + return [] return self.send_order(vt_symbol=vt_symbol, direction=Direction.LONG, offset=Offset.CLOSE, @@ -244,6 +265,7 @@ class CtaTemplate(ABC): volume=volume, stop=stop, lock=lock, + order_type=order_type, order_time=order_time, grid=grid) @@ -256,6 +278,7 @@ class CtaTemplate(ABC): volume: float, stop: bool = False, lock: bool = False, + order_type: OrderType = OrderType.LIMIT, order_time: datetime = None, grid: CtaGrid = None ): @@ -268,8 +291,17 @@ class CtaTemplate(ABC): if not self.trading: return [] + vt_orderids = self.cta_engine.send_order( - self, vt_symbol, direction, offset, price, volume, stop, lock + strategy=self, + vt_symbol=vt_symbol, + direction=direction, + offset=offset, + price=price, + volume=volume, + stop=stop, + lock=lock, + order_type=order_type ) if order_time is None: @@ -277,11 +309,12 @@ class CtaTemplate(ABC): for vt_orderid in vt_orderids: d = { - 'direction': direction.value, - 'offset': offset.value, + 'direction': direction, + 'offset': offset, 'vt_symbol': vt_symbol, 'price': price, 'volume': volume, + 'order_type': order_type, 'traded': 0, 'order_time': order_time, 'status': Status.SUBMITTING @@ -526,6 +559,33 @@ class CtaProTemplate(CtaTemplate): 增强模板 """ + idx_symbol = None # 指数合约 + + price_tick = 1 # 商品的最小价格跳动 + symbol_size = 10 # 商品得合约乘数 + margin_rate = 0.1 # 商品的保证金 + + cur_datetime = None # 当前Tick时间 + cur_mi_tick = None # 最新的主力合约tick( vt_symbol) + cur_99_tick = None # 最新得指数合约tick( idx_symbol) + + cur_mi_price = None # 当前价(主力合约 vt_symbol) + cur_99_price = None # 当前价(tick时,根据tick更新,onBar回测时,根据bar.close更新) + + last_minute = None # 最后的分钟,用于on_tick内每分钟处理的逻辑 + cancel_seconds = 120 # 撤单时间(秒) + + # 资金相关 + max_invest_rate = 0.1 # 最大仓位(0~1) + max_invest_margin = 0 # 资金上限 0,不限制 + max_invest_pos = 0 # 单向头寸数量上限 0,不限制 + + position = None # 仓位组件 + policy = None # 事务执行组件 + gt = None # 网格交易组件 + + klines = {} # K线字典: kline_name: kline + backtesting = False # 逻辑过程日志 @@ -539,23 +599,6 @@ class CtaProTemplate(CtaTemplate): cta_engine, strategy_name, vt_symbol, setting ) - self.idx_symbol = None # 指数合约 - - self.price_tick = 1 # 商品的最小价格跳动 - self.symbol_size = 10 # 商品得合约乘数 - - self.cur_datetime = None # 当前Tick时间 - - self.cur_mi_tick = None # 最新的主力合约tick( vt_symbol) - self.cur_99_tick = None # 最新得指数合约tick( idx_symbol) - - self.cur_mi_price = None # 当前价(主力合约 vt_symbol) - self.cur_99_price = None # 当前价(tick时,根据tick更新,onBar回测时,根据bar.close更新) - - self.cancel_seconds = 120 # 撤单时间(秒) - - self.klines = {} # K线字典: kline_name: kline - # 增加仓位管理模块 self.position = CtaPosition(strategy=self) @@ -584,6 +627,7 @@ class CtaProTemplate(CtaTemplate): self.write_log(f'指数合约:{self.idx_symbol}, 主力合约:{self.vt_symbol}') self.price_tick = self.cta_engine.get_price_tick(self.vt_symbol) self.symbol_size = self.cta_engine.get_size(self.vt_symbol) + self.margin_rate = self.cta_engine.get_margin_rate(self.vt_symbol) def save_klines_to_cache(self, kline_names: list = []): """ @@ -602,7 +646,7 @@ class CtaProTemplate(CtaTemplate): klines = {} for kline_name in kline_names: kline = self.klines.get(kline_name, None) - #if kline: + # if kline: # kline.strategy = None # kline.cb_on_bar = None klines.update({kline_name: kline}) @@ -729,12 +773,11 @@ class CtaProTemplate(CtaTemplate): self.position.pos = self.position.long_pos + self.position.short_pos - self.write_log( - u'{}加载持久化数据完成,多单:{},空单:{},共:{}手' - .format(self.strategy_name, - self.position.long_pos, - abs(self.position.short_pos), - self.position.pos)) + self.write_log(u'{}加载持久化数据完成,多单:{},空单:{},共:{}手' + .format(self.strategy_name, + self.position.long_pos, + abs(self.position.short_pos), + self.position.pos)) self.pos = self.position.pos self.gt.save() self.display_grids() @@ -846,9 +889,9 @@ class CtaProTemplate(CtaTemplate): grid = copy.copy(none_mi_grid) # 委托卖出非主力合约 - order_ids = self.sell(price=none_mi_price, volume=none_mi_grid.volume, vt_symbol=none_mi_symbol, - grid=none_mi_grid) - if len(order_ids) > 0: + vt_orderids = self.sell(price=none_mi_price, volume=none_mi_grid.volume, vt_symbol=none_mi_symbol, + grid=none_mi_grid) + if len(vt_orderids) > 0: self.write_log(f'切换合约,委托卖出非主力合约{none_mi_symbol}持仓:{none_mi_grid.volume}') # 添加买入主力合约 @@ -856,9 +899,9 @@ class CtaProTemplate(CtaTemplate): grid.snapshot.update({'mi_symbol': self.vt_symbol, 'open_price': self.cur_mi_price}) self.gt.dn_grids.append(grid) - order_ids = self.buy(price=self.cur_mi_price + 5 * self.price_tick, - volume=grid.volume, vt_symbol=self.vt_symbol, grid=grid) - if len(order_ids) > 0: + vt_orderids = self.buy(price=self.cur_mi_price + 5 * self.price_tick, + volume=grid.volume, vt_symbol=self.vt_symbol, grid=grid) + if len(vt_orderids) > 0: self.write_log(u'切换合约,委托买入主力合约:{},价格:{},数量:{}' .format(self.vt_symbol, self.cur_mi_price, grid.volume)) self.gt.save() @@ -908,17 +951,17 @@ class CtaProTemplate(CtaTemplate): grid = copy.copy(none_mi_grid) # 委托平空非主力合约 - order_ids = self.cover(price=none_mi_price, volume=none_mi_grid.volume, vt_symbol=self.vt_symbol, - grid=none_mi_grid) - if len(order_ids) > 0: + vt_orderids = self.cover(price=none_mi_price, volume=none_mi_grid.volume, vt_symbol=self.vt_symbol, + grid=none_mi_grid) + if len(vt_orderids) > 0: self.write_log(f'委托平空非主力合约{none_mi_symbol}持仓:{none_mi_grid.volume}') # 添加卖出主力合约 grid.id = str(uuid.uuid1()) grid.snapshot.update({'mi_symbol': self.vt_symbol, 'open_price': self.cur_mi_price}) self.gt.up_grids.append(grid) - order_ids = self.short(price=self.cur_mi_price, volume=grid.volume, vt_symbol=self.vt_symbol, grid=grid) - if len(order_ids) > 0: + vt_orderids = self.short(price=self.cur_mi_price, volume=grid.volume, vt_symbol=self.vt_symbol, grid=grid) + if len(vt_orderids) > 0: self.write_log(f'委托做空主力合约:{self.vt_symbol},价格:{self.cur_mi_price},数量:{grid.volume}') self.gt.save() else: @@ -996,3 +1039,1148 @@ class CtaProTemplate(CtaTemplate): if self.backtesting: return self.cta_engine.send_wechat(msg=msg, strategy=self) + + +class CtaProFutureTemplate(CtaProTemplate): + """期货交易增强版模板""" + + order_type = OrderType.LIMIT + activate_fak = False + activate_today_lock = False + + def __init__(self, cta_engine, strategy_name, vt_symbol, setting): + """""" + super().__init__(cta_engine, strategy_name, vt_symbol, setting) + + self.parameters.append('activate_fak') + self.parameters.append('activate_today_lock') + + def update_setting(self, setting: dict): + """更新配置参数""" + super().update_setting(setting) + + # 实盘时,判断是否激活使用FAK模式 + if not self.backtesting: + if self.activate_fak: + self.order_type = OrderType.FAK + + def load_policy(self): + """加载policy""" + if self.policy: + self.write_log(u'load_policy(),初始化Policy') + + self.policy.load() + self.write_log(u'Policy:{}'.format(self.policy.toJson())) + + def on_start(self): + """启动策略(必须由用户继承实现)""" + self.write_log(u'启动') + self.trading = True + self.put_event() + + # ---------------------------------------------------------------------- + def on_stop(self): + """停止策略(必须由用户继承实现)""" + self.active_orders.clear() + self.pos = 0 + self.entrust = 0 + + self.write_log(u'停止') + self.put_event() + + def on_trade(self, trade: TradeData): + """交易更新""" + self.write_log(u'{},交易更新:{},当前持仓:{} ' + .format(self.cur_datetime, + trade.__dict__, + self.position.pos)) + + dist_record = dict() + if self.backtesting: + dist_record['datetime'] = trade.time + else: + dist_record['datetime'] = ' '.join([self.cur_datetime.strftime('%Y-%m-%d'), trade.time]) + dist_record['volume'] = trade.volume + dist_record['price'] = trade.price + dist_record['symbol'] = trade.vt_symbol + + if trade.direction == Direction.LONG and trade.offset == Offset.OPEN: + dist_record['operation'] = 'buy' + self.position.open_pos(trade.direction, volume=trade.volume) + dist_record['long_pos'] = self.position.long_pos + dist_record['short_pos'] = self.position.short_pos + + if trade.direction == Direction.SHORT and trade.offset == Offset.OPEN: + dist_record['operation'] = 'short' + self.position.open_pos(trade.direction, volume=trade.volume) + dist_record['long_pos'] = self.position.long_pos + dist_record['short_pos'] = self.position.short_pos + + if trade.direction == Direction.LONG and trade.offset != Offset.OPEN: + dist_record['operation'] = 'cover' + self.position.close_pos(trade.direction, volume=trade.volume) + dist_record['long_pos'] = self.position.long_pos + dist_record['short_pos'] = self.position.short_pos + + if trade.direction == Direction.SHORT and trade.offset != Offset.OPEN: + dist_record['operation'] = 'sell' + self.position.close_pos(trade.direction, volume=trade.volume) + dist_record['long_pos'] = self.position.long_pos + dist_record['short_pos'] = self.position.short_pos + + self.save_dist(dist_record) + self.pos = self.position.pos + + def on_order(self, order: OrderData): + """报单更新""" + # 未执行的订单中,存在是异常,删除 + self.write_log(u'{}报单更新,{}'.format(self.cur_datetime, order.__dict__)) + + if order.vt_orderid in self.active_orders: + + if order.volume == order.traded and order.status in [Status.ALLTRADED]: + self.on_order_all_traded(order) + + elif order.offset == Offset.OPEN and order.status in [Status.CANCELLED]: + # 开仓委托单被撤销 + self.on_order_open_canceled(order) + + elif order.offset != Offset.OPEN and order.status in [Status.CANCELLED]: + # 平仓委托单被撤销 + self.on_order_close_canceled(order) + + elif order.status == Status.REJECTED: + if order.offset == Offset.OPEN: + self.write_error(u'{}委托单开{}被拒,price:{},total:{},traded:{},status:{}' + .format(order.vt_symbol, order.direction, order.price, order.volume, + order.traded, order.status)) + self.on_order_open_canceled(order) + else: + self.write_error(u'OnOrder({})委托单平{}被拒,price:{},total:{},traded:{},status:{}' + .format(order.vt_symbol, order.direction, order.price, order.volume, + order.traded, order.status)) + self.on_order_close_canceled(order) + else: + self.write_log(u'委托单未完成,total:{},traded:{},tradeStatus:{}' + .format(order.volume, order.traded, order.status)) + else: + self.write_error(u'委托单{}不在策略的未完成订单列表中:{}'.format(order.vt_orderid, self.active_orders)) + + def on_order_all_traded(self, order: OrderData): + """ + 订单全部成交 + :param order: + :return: + """ + self.write_log(u'{},委托单:{}全部完成'.format(order.time, order.vt_orderid)) + order_info = self.active_orders[order.vt_orderid] + + # 平空仓完成(cover) + if order_info['direction'] == Direction.LONG.value and order.offset != Offset.OPEN: + self.write_log(u'{}平空仓完成(cover),委托价格:{}'.format(order.vt_symbol, order.price)) + # 通过vt_orderid,找到对应的网格 + grid = order_info.get('grid', None) + if grid is not None: + # 移除当前委托单 + if order.vt_orderid in grid.order_ids: + grid.order_ids.remove(order.vt_orderid) + + # 网格的所有委托单已经执行完毕 + if len(grid.order_ids) == 0: + grid.order_status = False + grid.traded_volume = 0 + + # 平仓完毕(cover, sell) + if order.offset != Offset.OPEN: + grid.open_status = False + grid.close_status = True + + self.write_log(f'{grid.direction.value}单已平仓完毕,order_price:{order.price}' + + f',volume:{order.volume}') + + self.write_log(f'移除网格:{grid.to_json()}') + self.gt.remove_grids_by_ids(direction=grid.direction, ids=[grid.id]) + + # 开仓完毕( buy, short) + else: + grid.open_status = True + self.write_log(f'{grid.direction.value}单已开仓完毕,order_price:{order.price}' + + f',volume:{order.volume}') + + # 网格的所有委托单部分执行完毕 + else: + old_traded_volume = grid.traded_volume + grid.traded_volume += order.volume + + self.write_log(f'{grid.direction.value}单部分{order.offset}仓,' + + f'网格volume:{grid.volume}, traded_volume:{old_traded_volume}=>{grid.traded_volume}') + + self.write_log(f'剩余委托单号:{grid.order_ids}') + + # 在策略得活动订单中,移除 + self.active_orders.pop(order.vt_orderid, None) + + def on_order_open_canceled(self, order: OrderData): + """ + 委托开仓单撤销 + 如果是FAK模式,重新修改价格,再提交 + FAK用于实盘,需要增加涨跌停判断 + :param order: + :return: + """ + self.write_log(u'委托开仓单撤销:{}'.format(order.__dict__)) + + if not self.trading: + if not self.backtesting: + self.write_error(u'当前不允许交易') + return + + if order.vt_orderid not in self.active_orders: + self.write_error(u'{}不在未完成的委托单中{}。'.format(order.vt_orderid, self.active_orders)) + return + + # 直接更新“未完成委托单”,更新volume,retry次数 + old_order = self.active_orders[order.vt_orderid] + self.write_log(u'{} 委托信息:{}'.format(order.vt_orderid, old_order)) + old_order['traded'] = order.traded + order_vt_symbol = copy.copy(old_order['vt_symbol']) + order_volume = old_order['volume'] - old_order['traded'] + if order_volume <= 0: + msg = u'{} {}{}需重新开仓数量为{},不再开仓' \ + .format(self.strategy_name, + order.vt_orderid, + order_vt_symbol, + order_volume) + self.write_error(msg) + + self.write_log(u'移除:{}'.format(order.vt_orderid)) + self.active_orders.pop(order.vt_orderid, None) + return + + order_price = old_order['price'] + order_type = old_order.get('order_type', OrderType.LIMIT) + order_retry = old_order.get('retry', 0) + grid = old_order.get('grid', None) + if order_retry > 20: + # 这里超过20次尝试失败后,不再尝试,发出告警信息 + msg = u'{} {}/{}手, 重试开仓次数{}>20' \ + .format(self.strategy_name, + order_vt_symbol, + order_volume, + order_retry) + self.write_error(msg) + self.send_wechat(msg) + + if grid: + if order.vt_orderid in grid.order_ids: + grid.order_ids.remove(order.vt_orderid) + + # 网格的所有委托单已经执行完毕 + if len(grid.order_ids) == 0: + grid.order_status = False + + self.gt.save() + self.write_log(u'网格信息更新:{}'.format(grid.__dict__)) + + self.write_log(u'移除:{}'.format(order.vt_orderid)) + self.active_orders.pop(order.vt_orderid, None) + return + + order_retry += 1 + + # FAK 重新开单 + if old_order['direction'] == Direction.LONG and order_type == OrderType.FAK: + # 更新网格交易器 + + self.write_log(u'FAK模式,需要重新发送buy委托.grid:{}'.format(grid.__dict__)) + # 更新委托平仓价 + buy_price = max(self.cur_mi_tick.ask_price_1, self.cur_mi_tick.last_price, order_price) + self.price_tick + # 不能超过涨停价 + if self.cur_mi_tick.limit_up > 0 and buy_price > self.cur_mi_tick.limit_up: + buy_price = self.cur_mi_tick.limit_up + + if self.is_upper_limit(self.vt_symbol): + self.write_log(u'{}涨停,不做buy'.format(self.vt_symbol)) + return + + # 发送委托 + vt_orderids = self.buy(price=buy_price, + volume=order_volume, + vt_symbol=self.vt_symbol, + order_type=OrderType.FAK, + order_time=self.cur_datetime, + grid=grid) + if not vt_orderids: + self.write_error(u'重新提交{} {}手开多单,价格:{},失败'. + format(self.vt_symbol, order_volume, buy_price)) + return + + # 更新retry的次数 + for vt_orderid in vt_orderids: + info = self.active_orders.get(vt_orderid, None) + info.update({'retry': order_retry}) + + self.gt.save() + # 删除旧的委托记录 + self.write_log(u'移除旧的委托记录:{}'.format(order.vt_orderid)) + self.active_orders.pop(order.vt_orderid, None) + + elif old_order['direction'] == Direction.SHORT and order_type == OrderType.FAK: + + self.write_log(u'FAK模式,需要重新发送short委托.grid:{}'.format(grid.__dict__)) + short_price = min(self.cur_mi_tick.bid_price_1, self.cur_mi_tick.last_price, order_price) - self.price_tick + # 不能超过跌停价 + if self.cur_mi_tick.limit_down > 0 and short_price < self.cur_mi_tick.limit_down: + short_price = self.cur_mi_tick.limit_down + + if self.is_lower_limit(self.vt_symbol): + self.write_log(u'{}跌停,不做short'.format(self.vt_symbol)) + return + + # 发送委托 + vt_orderids = self.short(price=short_price, + volume=order_volume, + vt_symbol=self.vt_symbol, + order_type=OrderType.FAK, + order_time=self.cur_datetime, + grid=grid) + + if not vt_orderids: + self.write_error( + u'重新提交{} {}手开空单,价格:{}, 失败'.format(self.vt_symbol, order_volume, short_price)) + return + + # 更新retry的次数 + for vt_orderid in vt_orderids: + info = self.active_orders.get(vt_orderid, None) + info.update({'retry': order_retry}) + + self.gt.save() + # 删除旧的委托记录 + self.write_log(u'移除旧的委托记录:{}'.format(order.vt_orderid)) + self.active_orders.pop(order.vt_orderid, None) + else: + pre_status = old_order.get('status', Status.NOTTRADED) + old_order.update({'status': Status.CANCELLED}) + self.write_log(u'委托单状态:{}=>{}'.format(pre_status, old_order.get('status'))) + if grid: + if order.vt_orderid in grid.order_ids: + grid.order_ids.remove(order.vt_orderid) + + if not grid.order_ids: + grid.order_status = False + + self.gt.save() + self.active_orders.update({order.vt_orderid: old_order}) + + self.display_grids() + + def on_order_close_canceled(self, order: OrderData): + """委托平仓单撤销""" + self.write_log(u'委托平仓单撤销:{}'.format(order.__dict__)) + + if order.vt_orderid not in self.active_orders: + self.write_error(u'{}不在未完成的委托单中:{}。'.format(order.vt_orderid, self.active_orders)) + return + + if not self.trading: + self.write_error(u'当前不允许交易') + return + + # 直接更新“未完成委托单”,更新volume,Retry次数 + old_order = self.active_orders[order.vt_orderid] + self.write_log(u'{} 订单信息:{}'.format(order.vt_orderid, old_order)) + old_order['traded'] = order.traded + # order_time = old_order['order_time'] + order_symbol = copy.copy(old_order['symbol']) + order_volume = old_order['volume'] - old_order['traded'] + if order_volume <= 0: + msg = u'{} {}{}重新平仓数量为{},不再平仓' \ + .format(self.strategy_name, order.vt_orderid, order_symbol, order_volume) + self.write_error(msg) + self.send_wechat(msg) + self.write_log(u'活动订单移除:{}'.format(order.vt_orderid)) + self.active_orders.pop(order.vt_orderid, None) + return + + order_price = old_order['price'] + order_type = old_order.get('order_type', OrderType.LIMIT) + order_retry = old_order['retry'] + grid = old_order.get('grid', None) + if order_retry > 20: + msg = u'{} 平仓撤单 {}/{}手, 重试平仓次数{}>20' \ + .format(self.strategy_name, order_symbol, order_volume, order_retry) + self.write_error(msg) + self.send_wechat(msg) + if grid: + if order.vt_orderid in grid.order_ids: + grid.order_ids.remove(order.vt_orderid) + if not grid.order_ids: + grid.order_status = False + self.gt.save() + self.write_log(u'更新网格=>{}'.format(grid.__dict__)) + + self.write_log(u'移除活动订单:{}'.format(order.vt_orderid)) + self.active_orders.pop(order.vt_orderid, None) + return + + order_retry += 1 + + if old_order['direction'] == Direction.LONG and order_type == OrderType.FAK: + self.write_log(u'FAK模式,需要重新发送cover委托.grid:{}'.format(grid.__dict__)) + # 更新委托平仓价 + cover_tick = self.tick_dict.get(order_symbol, self.cur_mi_tick) + cover_price = max(cover_tick.ask_price_1, cover_tick.last_price, order_price) + self.price_tick + # 不能超过涨停价 + if cover_tick.limit_up > 0 and cover_price > cover_tick.limit_up: + cover_price = cover_tick.limit_up + + if self.is_upper_limit(order_symbol): + self.write_log(u'{}涨停,不做cover'.format(order_symbol)) + return + + # 发送委托 + vt_orderids = self.cover(price=cover_price, + volume=order_volume, + vt_symbol=order_symbol, + order_type=OrderType.FAK, + order_time=self.cur_datetime, + grid=grid) + if not vt_orderids: + self.write_error(u'重新提交{} {}手平空单{}失败'.format(order_symbol, order_volume, cover_price)) + return + + for vt_orderid in vt_orderids: + info = self.active_orders.get(vt_orderid) + info.update({'retry': order_retry}) + + self.gt.save() + self.write_log(u'移除活动订单:{}'.format(order.vt_orderid)) + self.active_orders.pop(order.vt_orderi, None) + + elif old_order['direction'] == Direction.SHORT and order_type == OrderType.FAK: + self.write_log(u'FAK模式,需要重新发送sell委托.grid:{}'.format(grid.__dict__)) + sell_tick = self.tick_dict.get(order_symbol, self.cur_mi_tick) + sell_price = min(sell_tick.bid_price_1, sell_tick.last_price, order_price) - self.price_tick + + # 不能超过跌停价 + if sell_tick.limit_down > 0 and sell_price < sell_tick.limit_down: + sell_price = sell_tick.limit_down + + if self.is_lower_limit(order_symbol): + self.write_log(u'{}涨停,不做sell'.format(order_symbol)) + return + + # 发送委托 + vt_orderids = self.sell(price=sell_price, + volume=order_volume, + vt_symbol=order_symbol, + order_type=OrderType.FAK, + order_time=self.cur_datetime, + grid=grid) + + if not vt_orderids: + self.write_error(u'重新提交{} {}手平多单{}失败'.format(order_symbol, order_volume, sell_price)) + return + + for vt_orderid in vt_orderids: + info = self.active_orders.get(vt_orderid) + info.update({'retry': order_retry}) + + self.gt.save() + + self.write_log(u'移除活动订单:{}'.format(order.vt_orderid)) + self.active_orders.pop(order.vt_orderid, None) + + else: + pre_status = old_order.get('status', Status.NOTTRADED) + old_order.update({'status': Status.CANCELLED}) + self.write_log(u'委托单状态:{}=>{}'.format(pre_status, old_order.get('status'))) + if grid: + if order.vt_orderid in grid.order_ids: + grid.order_ids.remove(order.vt_orderid) + if len(grid.order_ids) == 0: + grid.order_status = False + self.gt.save() + self.active_orders.update({order.vt_orderid: old_order}) + + self.display_grids() + + def on_stop_order(self, stop_order: StopOrder): + self.write_log(f'停止单触发:{stop_order.__dict__}') + + def cancel_all_orders(self): + """ + 重载撤销所有正在进行得委托 + :return: + """ + self.write_log(u'撤销所有正在进行得委托') + self.tns_cancel_logic(dt=datetime.now(), force=True, reopen=False) + + def tns_cancel_logic(self, dt, force=False, reopen=False): + "撤单逻辑""" + if len(self.active_orders) < 1: + self.entrust = 0 + return + + canceled_ids = [] + + for vt_orderid in list(self.active_orders.keys()): + order_info = self.active_orders[vt_orderid] + order_symbol = order_info.get('symbol', self.vt_symbol) + order_time = order_info['order_time'] + order_volume = order_info['volume'] - order_info['traded'] + # order_price = order_info['price'] + # order_direction = order_info['direction'] + # order_offset = order_info['offset'] + order_grid = order_info['grid'] + order_status = order_info.get('status', Status.NOTTRADED) + order_type = order_info.get('order_type', OrderType.LIMIT) + over_seconds = (dt - order_time).total_seconds() + + # 只处理未成交的限价委托单 + if order_status in [Status.NOTTRADED] and (order_type == OrderType.LIMIT or '.SPD' in order_symbol): + if over_seconds > self.cancel_seconds or force: # 超过设置的时间还未成交 + self.write_log(u'超时{}秒未成交,取消委托单:vt_orderid:{},order:{}' + .format(over_seconds, vt_orderid, order_info)) + order_info.update({'status': Status.CANCELING}) + self.active_orders.update({vt_orderid: order_info}) + ret = self.cancel_order(str(vt_orderid)) + if not ret: + self.write_log(u'撤单失败,更新状态为撤单成功') + order_info.update({'status': Status.CANCELLED}) + self.active_orders.update({vt_orderid: order_info}) + + continue + + # 处理状态为‘撤销’的委托单 + elif order_status == Status.CANCELLED: + self.write_log(u'委托单{}已成功撤单,删除{}'.format(vt_orderid, order_info)) + canceled_ids.append(vt_orderid) + + if reopen: + # 撤销的委托单,属于开仓类,需要重新委托 + if order_info['offset'] == Offset.OPEN: + self.write_log(u'超时撤单后,重新开仓') + # 开空委托单 + if order_info['direction'] == Direction.SHORT: + short_price = self.cur_mi_price - self.price_tick + if order_grid.volume != order_volume and order_volume > 0: + self.write_log( + u'网格volume:{},order_volume:{}不一致,修正'.format(order_grid.volume, order_volume)) + order_grid.volume = order_volume + + self.write_log(u'重新提交{}开空委托,开空价{},v:{}'.format(order_symbol, short_price, order_volume)) + vt_orderids = self.short(price=short_price, + volume=order_volume, + vt_symbol=order_symbol, + order_type=order_type, + order_time=self.cur_datetime, + grid=order_grid) + + if len(vt_orderids) > 0: + self.write_log(u'委托成功,orderid:{}'.format(vt_orderids)) + order_grid.snapshot.update({'open_price': short_price}) + else: + self.write_error(u'撤单后,重新委托开空仓失败') + else: + buy_price = self.cur_mi_price + self.price_tick + if order_grid.volume != order_volume and order_volume > 0: + self.write_log( + u'网格volume:{},order_volume:{}不一致,修正'.format(order_grid.volume, order_volume)) + order_grid.volume = order_volume + + self.write_log(u'重新提交{}开多委托,开多价{},v:{}'.format(order_symbol, buy_price, order_volume)) + vt_orderids = self.buy(price=buy_price, + volume=order_volume, + vt_symbol=order_symbol, + order_type=order_type, + order_time=self.cur_datetime, + grid=order_grid) + + if len(vt_orderids) > 0: + self.write_log(u'委托成功,orderids:{}'.format(vt_orderids)) + order_grid.snapshot.update({'open_price': buy_price}) + else: + self.write_error(u'撤单后,重新委托开多仓失败') + else: + # 属于平多委托单 + if order_info['direction'] == Direction.SHORT: + sell_price = self.cur_mi_price - self.price_tick + self.write_log(u'重新提交{}平多委托,{},v:{}'.format(order_symbol, sell_price, order_volume)) + vt_orderids = self.sell(price=sell_price, + volume=order_volume, + vt_symbol=order_symbol, + order_type=order_type, + order_time=self.cur_datetime, + grid=order_grid) + if len(vt_orderids) > 0: + self.write_log(u'委托成功,orderids:{}'.format(vt_orderids)) + else: + self.write_error(u'撤单后,重新委托平多仓失败') + # 属于平空委托单 + else: + cover_price = self.cur_mi_price + self.price_tick + self.write_log(u'重新提交{}平空委托,委托价{},v:{}'.format(order_symbol, cover_price, order_volume)) + vt_orderids = self.cover(price=cover_price, + volume=order_volume, + vt_symbol=order_symbol, + order_type=order_type, + order_time=self.cur_datetime, + grid=order_grid) + if len(vt_orderids) > 0: + self.write_log(u'委托成功,orderids:{}'.format(vt_orderids)) + else: + self.write_error(u'撤单后,重新委托平空仓失败') + else: + if order_info['offset'] == Offset.OPEN \ + and order_grid \ + and len(order_grid.order_ids) == 0 \ + and order_grid.traded_volume == 0: + self.write_log(u'移除委托网格{}'.format(order_grid.__dict__)) + order_info['grid'] = None + self.gt.remove_grids_by_ids(direction=order_grid.direction, ids=[order_grid.id]) + + # 删除撤单的订单 + for vt_orderid in canceled_ids: + self.write_log(u'删除orderID:{0}'.format(vt_orderid)) + self.active_orders.pop(vt_orderid, None) + + if len(self.active_orders) == 0: + self.entrust = 0 + + def tns_close_long_pos(self, grid): + """ + 事务平多单仓位 + 1.来源自止损止盈平仓 + 逻辑: 如果当前账号昨仓满足平仓数量,直接平仓,如果不满足,则创建锁仓网格. + :param 平仓网格 + :return: + """ + self.write_log(u'执行事务平多仓位:{}'.format(grid.toJson())) + + # 平仓网格得合约 + sell_symbol = grid.snapshot.get('mi_symbol', self.vt_symbol) + # 从cta engine获取当前账号中,sell_symbol的持仓情况 + grid_pos = self.cta_engine.get_position_holding(vt_symbol=sell_symbol) + if grid_pos is None: + self.write_error(u'无法获取{}得持仓信息'.format(sell_symbol)) + return False + + # 不需要日内锁仓,或者昨仓可以满足,发出委托卖出单 + if (grid_pos.long_yd >= grid.volume > 0 and grid_pos.long_td == 0 and grid_pos.short_td == 0) \ + or not self.activate_today_lock: + if self.activate_today_lock: + self.write_log(u'昨仓多单:{},没有今仓,满足条件,直接平昨仓'.format(grid_pos.long_yd)) + + sell_price = self.cta_engine.get_price(sell_symbol) + if sell_price is None: + self.write_error(f'暂时不能获取{sell_symbol}价格,不能平仓') + return False + + # 发出平多委托 + if grid.traded_volume > 0: + grid.volume -= grid.traded_volume + grid.traded_volume = 0 + + vt_orderids = self.sell(price=sell_price, + volume=grid.volume, + vt_symbol=sell_symbol, + order_type=self.order_type, + order_time=self.cur_datetime, + grid=grid) + if len(vt_orderids) == 0: + self.write_error(u'多单平仓委托失败') + return False + else: + self.write_log(u'多单平仓委托成功,编号:{}'.format(vt_orderids)) + return True + + # 当前没有昨仓,采用锁仓处理 + else: + self.write_log(u'昨仓多单:{}不满足条件,创建对锁仓'.format(grid_pos.longYd)) + dist_record = dict() + dist_record['datetime'] = self.cur_datetime + dist_record['symbol'] = sell_symbol + dist_record['price'] = self.cur_mi_price + dist_record['volume'] = grid.volume + dist_record['operation'] = 'add short lock[long]' + self.save_dist(dist_record) + + # 创建一个对锁网格 + lock_grid = copy.copy(grid) + # 网格类型, => 锁仓格 + lock_grid.type = LOCK_GRID + lock_grid.id = str(uuid.uuid1()) + lock_grid.direction = Direction.SHORT + lock_grid.open_status = False + lock_grid.order_status = False + lock_grid.order_ids = [] + + vt_orderids = self.short(self.cur_mi_price, + volume=lock_grid.volume, + vt_symbol=self.vt_symbol, + order_type=self.order_type, + order_time=self.cur_datetime, + grid=lock_grid) + + if len(vt_orderids) > 0: + # 原做多网格得类型,设置为锁仓格 + grid.type = LOCK_GRID + self.write_log(u'委托创建对锁单(空单)成功,委托编号:{},{},p:{},v:{}' + .format(vt_orderids, + sell_symbol, + self.cur_mi_price, + lock_grid.volume)) + lock_grid.snapshot.update({'mi_symbol': self.vt_symbol, 'open_price': self.cur_mi_price}) + self.gt.up_grids.append(lock_grid) + return True + else: + self.write_error(u'未能委托对锁单(空单)') + return False + + def tns_close_short_pos(self, grid): + """ + 事务平空单仓位 + 1.来源自止损止盈平仓 + 2.来源自换仓 + 逻辑: 如果当前账号昨仓满足平仓数量,直接平仓,如果不满足,则创建锁仓网格. + :param 平仓网格 + :return: + """ + self.write_log(u'执行事务平空仓位:{}'.format(grid.toJson())) + # 平仓网格得合约 + cover_symbol = grid.snapshot.get('mi_symbol', self.vt_symbol) + # vt_symbol => holding position + grid_pos = self.cta_engine.get_position_holding(cover_symbol) + if grid_pos is None: + self.write_error(u'无法获取{}得持仓信息'.format(cover_symbol)) + return False + + # 昨仓可以满足,发出委托卖出单 + if (grid_pos.short_yd >= grid.volume > 0 and grid_pos.long_td == 0 and grid_pos.short_td == 0) \ + or not self.activate_today_lock: + if self.activate_today_lock: + self.write_log(u'昨仓空单:{},没有今仓, 满足条件,直接平昨仓'.format(grid_pos.short_yd)) + + cover_price = self.cta_engine.get_price(cover_symbol) + if cover_price is None: + self.write_error(f'暂时没有{cover_symbol}行情,不能执行平仓') + return False + + # 发出cover委托 + if grid.traded_volume > 0: + grid.volume -= grid.traded_volume + grid.traded_volume = 0 + + vt_orderids = self.cover(price=cover_price, + volume=grid.volume, + vt_symbol=cover_symbol, + order_type=self.order_type, + order_time=self.cur_datetime, + grid=grid) + if len(vt_orderids) == 0: + self.write_error(u'空单平仓委托失败') + return False + else: + self.write_log(u'空单平仓委托成功,编号:{}'.format(vt_orderids)) + return True + + # 当前没有昨仓,采用锁仓处理 + else: + self.write_log(u'昨仓空单:{}不满足条件,建立对锁仓'.format(grid_pos.shortYd)) + dist_record = dict() + dist_record['datetime'] = self.cur_datetime + dist_record['symbol'] = cover_symbol + dist_record['price'] = self.cur_mi_price + dist_record['volume'] = grid.volume + dist_record['operation'] = 'add long lock[short]' + self.save_dist(dist_record) + # 创建一个对锁网格 + lock_grid = copy.copy(grid) + # 网格类型, => 锁仓格 + lock_grid.type = LOCK_GRID + lock_grid.id = str(uuid.uuid1()) + lock_grid.direction = Direction.LONG + lock_grid.open_status = False + lock_grid.order_status = False + lock_grid.order_ids = [] + + vt_orderids = self.buy(price=self.cur_mi_price, + volume=lock_grid.volume, + vt_symbol=cover_symbol, + order_type=self.order_type, + grid=lock_grid) + + if len(vt_orderids) > 0: + # 原做空网格得类型,设置为锁仓格 + grid.type = LOCK_GRID + self.write_log(u'委托创建对锁单(多单)成功,委托编号:{},{},p:{},v:{}' + .format(vt_orderids, + self.vt_symbol, + self.cur_mi_price, + lock_grid.volume)) + lock_grid.snapshot.update({'mi_symbol': self.vt_symbol, 'open_price': self.cur_mi_price}) + self.gt.dn_grids.append(lock_grid) + return True + else: + self.write_error(u'未能委托对锁单(多单)') + return False + + def tns_open_from_lock(self, open_symbol, open_volume, grid_type, open_direction): + """ + 从锁仓单中,获取已开的网格(对手仓设置为止损) + 1, 检查多空锁仓单中,是否有满足数量得昨仓, + 2, 定位到需求网格, + :param open_symbol: 开仓合约(主力合约) + :param open_volume: + :param grid_type 更新网格的类型 + :param open_direction: 开仓方向 + :return: None, 保留的格 + """ + + # 检查多单得对锁格 + locked_long_grids = self.gt.get_opened_grids_within_types(direction=Direction.LONG, types=[LOCK_GRID]) + if len(locked_long_grids) == 0: + return None + locked_long_dict = {} + + for g in locked_long_grids: + symbol = g.snapshot.get('mi_symbol', self.vt_symbol) + + if g.order_status or g.order_ids: + self.write_log(u'当前对锁格:{}存在委托,不纳入计算'.format(g.toJson())) + continue + + if symbol != open_symbol: + self.write_log(u'不处理symbol不一致: 委托请求:{}, Grid mi Symbol:{}'.format(open_symbol, symbol)) + continue + + volume = g.volume - g.traded_volume + locked_long_dict.update({symbol: locked_long_dict.get(symbol, 0) + volume}) + + locked_long_volume = locked_long_dict.get(open_symbol, 0) + if locked_long_volume < open_volume: + self.write_log(u'锁单中,没有足够得多单:{},需求:{}'.format(locked_long_volume, open_volume)) + return None + + # 空单对锁格 + locked_short_grids = self.gt.get_opened_grids_within_types(direction=Direction.SHORT, types=[LOCK_GRID]) + if len(locked_short_grids) == 0: + return None + + locked_short_dict = {} + + for g in locked_short_grids: + symbol = g.snapshot.get('mi_symbol', self.vt_symbol) + if g.order_status or g.order_ids: + self.write_log(u'当前对锁格:{}存在委托,不进行解锁'.format(g.toJson())) + continue + if symbol != open_symbol: + self.write_log(u'不处理symbol不一致: 委托请求:{}, Grid mi Symbol:{}'.format(open_symbol, symbol)) + continue + volume = g.volume - g.traded_volume + locked_short_dict.update({symbol: locked_short_dict.get(symbol, 0) + volume}) + + locked_short_volume = locked_short_dict.get(open_symbol, 0) + + if locked_short_volume < open_volume: + self.write_log(u'锁单中,没有足够得空单:{},需求:{}'.format(locked_short_volume, open_volume)) + return None + + # 检查空单昨仓是否满足 + symbol_pos = self.cta_engine.get_position_holding(open_symbol) + if (open_direction == Direction.LONG and symbol_pos.short_yd < open_volume) \ + or (open_direction == Direction.SHORT and symbol_pos.long_yd < open_volume): + self.write_log(u'昨仓数量,多单:{},空单:{},不满足:{}' + .format(symbol_pos.long_yd, symbol_pos.short_yd, open_volume)) + return None + + # 合并/抽离出 满足open_volume得多格, + target_long_grid = None + remove_long_grid_ids = [] + for g in sorted(locked_long_grids, key=lambda grid: grid.volume): + if g.orderStatus or len(g.orderRef) > 0: + continue + if target_long_grid is None: + target_long_grid = g + if g.volume == open_volume: + self.write_log(u'第一个网格持仓数量一致:g.volume:{},open_volume:{}' + .format(g.volume, open_volume)) + break + elif g.volume > open_volume: + self.write_log(u'第一个网格持仓数量大于需求:g.volume:{},open_volume:{}' + .format(g.volume, open_volume)) + remain_grid = copy.copy(g) + g.volume = open_volume + remain_grid.volume -= open_volume + remain_grid.id = uuid.uuid1() + self.gt.dn_grids.append(remain_grid) + self.write_log(u'添加剩余仓位到新多单网格:g.volume:{}' + .format(remain_grid.volume)) + break + else: + if g.volume <= open_volume - target_long_grid.volume: + self.write_log(u'网格持仓数量:g.volume:{},open_volume:{},保留格:{}' + .format(g.volume, + open_volume, + target_long_grid.volume)) + target_long_grid.volume += g.volume + g.volume = 0 + self.write_log(u'计划移除:{}'.format(g.id)) + remove_long_grid_ids.append(g.id) + else: + self.write_log(u'转移前网格持仓数量:g.volume:{},open_volume:{},保留格:{}' + .format(g.volume, + open_volume, + target_long_grid.volume)) + g.volume -= (open_volume - target_long_grid.volume) + target_long_grid.volume = open_volume + self.write_log(u'转移后网格持仓数量:g.volume:{},open_volume:{},保留格:{}' + .format(g.volume, + open_volume, + target_long_grid.volume)) + + break + + target_short_grid = None + remove_short_grid_ids = [] + for g in sorted(locked_short_grids, key=lambda grid: grid.volume): + if g.order_status or g.order_ids: + continue + if target_short_grid is None: + target_short_grid = g + if g.volume == open_volume: + self.write_log(u'第一个空单网格持仓数量满足需求:g.volume:{},open_volume:{}' + .format(g.volume, open_volume)) + break + elif g.volume > open_volume: + self.write_log(u'第一个空单网格持仓数量大于需求:g.volume:{},open_volume:{}' + .format(g.volume, open_volume)) + remain_grid = copy.copy(g) + g.volume = open_volume + remain_grid.volume -= open_volume + remain_grid.id = uuid.uuid1() + self.gt.up_grids.append(remain_grid) + self.write_log(u'添加剩余仓位到新空单网格:g.volume:{}' + .format(remain_grid.volume)) + break + else: + if g.volume <= open_volume - target_short_grid.volume: + target_short_grid.volume += g.volume + g.volume = 0 + remove_short_grid_ids.append(g.id) + else: + self.write_log(u'转移前空单网格持仓数量:g.volume:{},open_volume:{},保留格:{}' + .format(g.volume, + open_volume, + target_short_grid.volume)) + g.volume -= (open_volume - target_short_grid.volume) + target_short_grid.volume = open_volume + self.write_log(u'转移后空单网格持仓数量:g.volume:{},open_volume:{},保留格:{}' + .format(g.volume, + open_volume, + target_short_grid.volume)) + + break + + if target_long_grid.volume is None or target_short_grid is None: + self.write_log(u'未能定位多单网格和空单网格,不能解锁') + return None + + # 移除volume为0的网格 + self.gt.remove_grids_by_ids(direction=Direction.LONG, ids=remove_long_grid_ids) + self.gt.remove_grids_by_ids(direction=Direction.SHORT, ids=remove_short_grid_ids) + + if open_direction == Direction.LONG: + self.write_log(u'保留多单,对空单:{}平仓'.format(target_short_grid.id)) + # 对空单目标网格进行平仓 + cover_price = self.cta_engine.get_price(open_symbol) + # 使用止损价作为平仓 + self.write_log(u'空单止损价 :{} =>{}'.format(target_short_grid.stop_price, cover_price - 10 * self.price_tick)) + target_short_grid.stop_price = cover_price - 10 * self.price_tick + # 更新对锁格类型=>指定类型 + self.write_log(u'空单类型 :{} =>{}'.format(target_short_grid.type, grid_type)) + target_short_grid.type = grid_type + # 返回保留的多单网格 + return target_long_grid + + else: + self.write_log(u'保留空单,对多单平仓') + sell_price = self.cta_engine.get_price(open_symbol) + # # 使用止损价作为平仓 + self.write_log(u'多单止损价 :{} =>{}'.format(target_short_grid.stop_price, sell_price + 10 * self.price_tick)) + target_long_grid.stop_price = sell_price + 10 * self.price_tick + # 更新对锁格类型=>指定类型 + self.write_log(u'多单类型 :{} =>{}'.format(target_short_grid.type, grid_type)) + target_long_grid.type = grid_type + # 返回保留的空单网格 + return target_short_grid + + def tns_close_locked_grids(self, grid_type): + """ + 事务对所有对锁网格进行平仓 + :return: + """ + # 正在委托时,不处理 + if self.entrust != 0: + return + + # 多单得对锁格 + locked_long_grids = self.gt.get_opened_grids_within_types(direction=Direction.LONG, types=[LOCK_GRID]) + if len(locked_long_grids) == 0: + return + locked_long_dict = {} + + for g in locked_long_grids: + vt_symbol = g.snapshot.get('mi_symbol', self.vt_symbol) + volume = g.volume - g.traded_volume + locked_long_dict.update({vt_symbol: locked_long_dict.get(vt_symbol, 0) + volume}) + if g.orderStatus or g.order_ids: + self.write_log(u'当前对锁格:{}存在委托,不进行解锁'.format(g.toJson())) + return + + locked_long_volume = sum(locked_long_dict.values(), 0) + + # 空单对锁格 + locked_short_grids = self.gt.get_opened_grids_within_types(direction=Direction.SHORT, types=[LOCK_GRID]) + if len(locked_short_grids) == 0: + return + + locked_short_dict = {} + + for g in locked_short_grids: + vt_symbol = g.snapshot.get('mi_symbol', self.vt_symbol) + volume = g.volume - g.traded_volume + locked_short_dict.update({vt_symbol: locked_short_dict.get(vt_symbol, 0) + volume}) + if g.orderStatus or g.order_ids: + self.write_log(u'当前对锁格:{}存在委托,不进行解锁'.format(g.toJson())) + return + + locked_short_volume = sum(locked_short_dict.values(), 0) + + # debug info + self.write_log(u'多单对锁格:{}'.format([g.toJson() for g in locked_long_grids])) + self.write_log(u'空单对锁格:{}'.format([g.toJson() for g in locked_short_grids])) + + if locked_long_volume != locked_short_volume: + self.write_error(u'对锁格多空数量不一致,不能解锁.\n多:{},\n空:{}' + .format(locked_long_volume, locked_short_volume)) + return + + # 检查所有品种得昨仓是否满足数量 + for vt_symbol, volume in locked_long_dict.items(): + pos = self.cta_engine.get_position_holding(vt_symbol, None) + if pos is None: + self.write_error(u'{} 没有获取{}得持仓信息,不能解锁') + return + + # 检查多空单得昨单能否满足 + if pos.long_yd < volume or pos.short_yd < volume: + self.write_error(u'{}持仓昨仓多单:{},空单:{},不满足解锁数量:{}' + .format(vt_symbol, pos.long_yd, pos.short_td, volume)) + return + + if pos.long_td > 0 or pos.short_td > 0: + self.write_log(u'{}存在今多仓:{},空仓{},不满足解锁条件'.format(vt_symbol, pos.long_td, pos.short_td)) + return + + price = self.cta_engine.get_price(vt_symbol) + if price is None: + self.write_error(u'{}价格不在tick_dict缓存中,不能解锁'.format(vt_symbol)) + + # 所有合约价格和仓位都满足同时解开 + for g in locked_long_grids: + dist_record = dict() + dist_record['datetime'] = self.cur_datetime + dist_record['symbol'] = self.vt_symbol + dist_record['price'] = self.cur_mi_price + dist_record['volume'] = g.volume + dist_record['operation'] = 'close lock[long]' + self.save_dist(dist_record) + + # 通过切换回普通网格,提升止损价的方式实现平仓 + self.write_log( + u'网格 从锁仓 {}=>{},提升止损价{}=>{}进行离场'.format(LOCK_GRID, grid_type, g.stop_price, + self.cur_99_price / 2)) + g.type = grid_type + g.stop_price = self.cur_99_price / 2 + + for g in locked_short_grids: + dist_record = dict() + dist_record['datetime'] = self.cur_datetime + dist_record['symbol'] = self.vt_symbol + dist_record['price'] = self.cur_mi_price + + dist_record['volume'] = g.volume + dist_record['operation'] = 'close lock[short]' + self.save_dist(dist_record) + + # 通过切换回普通网格,提升止损价的方式实现平仓 + self.write_log(u'网格 从锁仓 {}=>{},提升止损价{}=>{}进行离场'.format(LOCK_GRID, grid_type, g.stop_price, + self.cur_99_price * 2)) + g.type = grid_type + g.stop_price = self.cur_99_price * 2 + + def grid_check_stop(self): + """ + 网格逐一止损/止盈检查 (根据指数价格进行止损止盈) + :return: + """ + if self.entrust != 0: + return + + if not self.trading: + if not self.backtesting: + self.write_error(u'当前不允许交易') + return + + # 多单网格逐一止损/止盈检查: + long_grids = self.gt.get_opened_grids_without_types(direction=Direction.LONG, types=[LOCK_GRID]) + + for g in long_grids: + # 满足离场条件,或者碰到止损价格 + if g.stop_price > 0 and g.stop_price > self.cur_99_price \ + and g.openStatus and not g.orderStatus: + dist_record = dict() + dist_record['datetime'] = self.cur_datetime + dist_record['symbol'] = self.idx_symbol + dist_record['volume'] = g.volume + dist_record['price'] = self.cur_99_price + dist_record['operation'] = 'stop leave' + dist_record['signals'] = '{}<{}'.format(self.cur_99_price, g.stop_price) + # 止损离场 + self.write_log(u'{} 指数价:{} 触发多单止损线{},{}当前价:{}。指数开仓价:{},主力开仓价:{},v:{}'. + format(self.cur_datetime, self.cur_99_price, g.stop_price, self.vt_symbol, + self.cur_mi_price, + g.open_price, g.snapshot.get('open_price'), g.volume)) + self.save_dist(dist_record) + + if self.tns_close_long_pos(g): + self.write_log(u'多单止盈/止损委托成功') + else: + self.write_error(u'多单止损委托失败') + + # 空单网格止损检查 + short_grids = self.gt.get_opened_grids_without_types(direction=Direction.SHORT, types=[LOCK_GRID]) + for g in short_grids: + if g.stop_price > 0 and g.stop_price < self.cur_99_price \ + and g.openStatus and not g.orderStatus: + dist_record = dict() + dist_record['datetime'] = self.cur_datetime + dist_record['symbol'] = self.idx_symbol + dist_record['volume'] = g.volume + dist_record['price'] = self.cur_99_price + dist_record['operation'] = 'stop leave' + dist_record['signals'] = '{}<{}'.format(self.cur_99_price, g.stop_price) + # 网格止损 + self.write_log(u'{} 指数价:{} 触发空单止损线:{},{}最新价:{}。指数开仓价:{},主力开仓价:{},v:{}'. + format(self.cur_datetime, self.cur_99_price, g.stop_price, self.vt_symbol, + self.cur_mi_price, + g.open_price, g.snapshot.get('open_price'), g.volume)) + self.save_dist(dist_record) + + if self.tns_close_short_pos(g): + self.write_log(u'空单止盈/止损委托成功') + else: + self.write_error(u'委托空单平仓失败') diff --git a/vnpy/app/risk_manager/engine.py b/vnpy/app/risk_manager/engine.py index ede4e115..1087944a 100644 --- a/vnpy/app/risk_manager/engine.py +++ b/vnpy/app/risk_manager/engine.py @@ -1,5 +1,5 @@ """""" - +import logging from copy import copy from collections import defaultdict from datetime import datetime @@ -182,20 +182,24 @@ class RiskManagerEngine(BaseEngine): if vt_accountid: account = self.account_dict.get(vt_accountid, None) if account: - return account.balance, \ - account.available, \ - round(account.frozen * 100 / (account.balance + 0.01), 2), \ - self.percent_limit + return ( + account.balance, + account.available, + round(account.frozen * 100 / (account.balance + 0.01), 2), + self.percent_limit + ) if len(self.account_dict.values()) > 0: account = list(self.account_dict.values())[0] - return account.balance, \ - account.available, \ - round(account.frozen * 100 / (account.balance + 0.01), 2), \ - self.percent_limit + return ( + account.balance, + account.available, + round(account.frozen * 100 / (account.balance + 0.01), 2), + self.percent_limit + ) else: return 0, 0, 0, 0 - def write_log(self, msg: str): + def write_log(self, msg: str, source: str = "", level: int = logging.DEBUG): """""" log = LogData(msg=msg, gateway_name="RiskManager") event = Event(type=EVENT_LOG, data=log) diff --git a/vnpy/chart/__init__.py b/vnpy/chart/__init__.py index 4d090ff4..53d72138 100644 --- a/vnpy/chart/__init__.py +++ b/vnpy/chart/__init__.py @@ -1,2 +1,2 @@ -from .widget import ChartWidget +from .widget import ChartWidget, KlineWidget from .item import CandleItem, VolumeItem diff --git a/vnpy/chart/widget.py b/vnpy/chart/widget.py index 8d6ff343..7e924e2e 100644 --- a/vnpy/chart/widget.py +++ b/vnpy/chart/widget.py @@ -1,5 +1,5 @@ from typing import List, Dict, Type - +from collections import deque import pyqtgraph as pg from vnpy.trader.ui import QtGui, QtWidgets, QtCore @@ -11,8 +11,7 @@ from .base import ( to_int, NORMAL_FONT ) from .axis import DatetimeAxis -from .item import ChartItem - +from .item import ChartItem, CandleItem, VolumeItem pg.setConfigOptions(antialias=True) @@ -21,10 +20,10 @@ class ChartWidget(pg.PlotWidget): """""" MIN_BAR_COUNT = 100 - def __init__(self, parent: QtWidgets.QWidget = None): + def __init__(self, parent: QtWidgets.QWidget = None, title: str = "ChartWidget of vn.py"): """""" super().__init__(parent) - + self.title = title self._manager: BarManager = BarManager() self._plots: Dict[str, pg.PlotItem] = {} @@ -34,14 +33,14 @@ class ChartWidget(pg.PlotWidget): self._first_plot: pg.PlotItem = None self._cursor: ChartCursor = None - self._right_ix: int = 0 # Index of most right data - self._bar_count: int = self.MIN_BAR_COUNT # Total bar visible in chart + self._right_ix: int = 0 # Index of most right data + self._bar_count: int = self.MIN_BAR_COUNT # Total bar visible in chart self._init_ui() def _init_ui(self) -> None: """""" - self.setWindowTitle("ChartWidget of vn.py") + self.setWindowTitle(self.title) self._layout = pg.GraphicsLayout() self._layout.setContentsMargins(10, 10, 10, 10) @@ -59,11 +58,11 @@ class ChartWidget(pg.PlotWidget): self, self._manager, self._plots, self._item_plot_map) def add_plot( - self, - plot_name: str, - minimum_height: int = 80, - maximum_height: int = None, - hide_x_axis: bool = False + self, + plot_name: str, + minimum_height: int = 80, + maximum_height: int = None, + hide_x_axis: bool = False ) -> None: """ Add plot area. @@ -111,20 +110,23 @@ class ChartWidget(pg.PlotWidget): self._layout.addItem(plot) def add_item( - self, - item_class: Type[ChartItem], - item_name: str, - plot_name: str + self, + item_class: Type[ChartItem], + item_name: str, + plot_name: str ): """ Add chart item. """ + # 创建显示的对象,蜡烛图,bar图,散点,线等 item = item_class(self._manager) self._items[item_name] = item + # 获取设置的显示区域,例如主图/volume/附图等 plot = self._plots.get(plot_name) plot.addItem(item) + # 绑定显示对象与显示区域关系 self._item_plot_map[item] = plot def get_plot(self, plot_name: str) -> pg.PlotItem: @@ -173,6 +175,7 @@ class ChartWidget(pg.PlotWidget): for item in self._items.values(): item.update_bar(bar) + # 刷新显示区域的最高/最低值 self._update_plot_limits() if self._right_ix >= (self._manager.get_count() - self._bar_count / 2): @@ -306,11 +309,11 @@ class ChartCursor(QtCore.QObject): """""" def __init__( - self, - widget: ChartWidget, - manager: BarManager, - plots: Dict[str, pg.GraphicsObject], - item_plot_map: Dict[ChartItem, pg.GraphicsObject] + self, + widget: ChartWidget, + manager: BarManager, + plots: Dict[str, pg.GraphicsObject], + item_plot_map: Dict[ChartItem, pg.GraphicsObject] ): """""" super().__init__() @@ -480,7 +483,9 @@ class ChartCursor(QtCore.QObject): buf[plot] += ("\n\n" + item_info_text) for plot_name, plot in self._plots.items(): - plot_info_text = buf[plot] + plot_info_text = buf.get(plot, None) + if not plot_info_text: + continue info = self._infos[plot_name] info.setText(plot_info_text) info.show() @@ -532,3 +537,124 @@ class ChartCursor(QtCore.QObject): for label in list(self._y_labels.values()) + [self._x_label]: label.hide() + + +class KlineWidget(ChartWidget): + """ k线widget,支持多widget;主图/volume/附图""" + clsId = 0 + + def __init__(self, parent: QtWidgets.QWidget = None, + title: str = "kline", + display_volume: bool = False, + display_sub: bool = False): + + super().__init__(parent, title) + + KlineWidget.clsId += 1 + self.windowId = str(KlineWidget.clsId) + + # 所有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_volume = display_volume + 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.main_plot_name = f'{self.windowId}_main' + self.volume_plot_name = f'{self.windowId}_volume' + self.sub_plot_name = f'{self.windowId}_sub' + + self.main_plot = None + self.volume_plot = None + self.sub_plot = None + if self.display_volume or self.display_sub: + self.add_plot(self.main_plot_name, hide_x_axis=True) # 主图 + self.add_item(CandleItem, "candle", self.main_plot_name) # 往主图区域,加入 + if self.display_volume: + self.add_plot(self.volume_plot_name, maximum_height=60) # volume 附图 + self.add_item(VolumeItem, "volume", self.volume_plot_name) + self.volume_plot = self.get_plot(self.volume_plot_name) + if self.display_sub: + self.add_plot(self.sub_plot_name, maximum_height=180) # 附图 + self.sub_plot = self.get_plot(self.sub_plot_name) + + else: + self.add_plot(self.main_plot_name, hide_x_axis=False) # 主图 + self.add_item(CandleItem, "candle", self.main_plot_name) # 往主图区域,加入 + self.add_cursor() + self.main_plot = self.get_plot(self.main_plot_name) + + def add_indicator(self, indicator: str, is_main: bool = True): + """ + 新增指标信号图 + :param indicator: 指标/信号的名称,如ma10, + :param is_main: 是否为主图 + :return: + """ + if is_main: + + if indicator in self.main_indicator_plots: + self.main_plot.removeItem(self.main_indicator_plots[indicator]) # 存在该指标/信号,先移除原有画布 + + self.main_indicator_plots[indicator] = self.main_plot.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.sub_plot.removeItem(self.sub_indicator_plots[indicator]) # 若存在该指标/信号,先移除原有的附图画布 + self.sub_indicator_plots[indicator] = self.sub_plot.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 clear_indicator(self, main=True): + """清空指标图形""" + # 清空信号图 + if main: + for indicator in self.main_indicator_plots: + self.main_plot.removeItem(self.main_indicator_plots[indicator]) + self.main_indicator_data = {} + self.main_indicator_plots = {} + else: + for indicator in self.sub_indicator_plots: + self.sub_plot.removeItem(self.sub_indicator_plots[indicator]) + self.sub_indicator_data = {} + self.sub_indicator_plots = {} + + def plot_indicator(self, datas: dict, 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) diff --git a/vnpy/data/renko/config.py b/vnpy/data/renko/config.py index e2159d29..548f299b 100644 --- a/vnpy/data/renko/config.py +++ b/vnpy/data/renko/config.py @@ -3,5 +3,5 @@ HEIGHT_LIST = [3, 5, 10, 'K3', 'K5', 'K10'] -FUTURE_RENKO_DB_NAME = 'FutureRenko_Db' -STOCK_RENKO_DB_NAME = 'StockRenko_Db' +FUTURE_RENKO_DB_NAME = 'FutureRenko' +STOCK_RENKO_DB_NAME = 'StockRenko' diff --git a/vnpy/gateway/ctp/ctp_gateway.py b/vnpy/gateway/ctp/ctp_gateway.py index 1804fa0e..46aef7eb 100644 --- a/vnpy/gateway/ctp/ctp_gateway.py +++ b/vnpy/gateway/ctp/ctp_gateway.py @@ -144,7 +144,6 @@ OPTIONTYPE_CTP2VT = { MAX_FLOAT = sys.float_info.max - symbol_exchange_map = {} symbol_name_map = {} symbol_size_map = {} @@ -223,7 +222,7 @@ class CtpGateway(BaseGateway): self.combiner_conf_dict = c.get_config() if len(self.combiner_conf_dict) > 0: self.write_log(u'加载的自定义价差/价比配置:{}'.format(self.combiner_conf_dict)) - except Exception as ex: # noqa + except Exception as ex: # noqa pass if not self.td_api: self.td_api = CtpTdApi(self) @@ -790,8 +789,14 @@ class CtpTdApi(TdApi): account.close_profit = data['CloseProfit'] account.holding_profit = data['PositionProfit'] account.trading_day = str(data['TradingDay']) - if '-' not in account.trading_day and len(account.trading_day)== 8: - account.trading_day = account.trading_day[0:4] + '-' + account.trading_day[4:6] + '-' + account.trading_day[6:8] + if '-' not in account.trading_day and len(account.trading_day) == 8: + account.trading_day = '-'.join( + [ + account.trading_day[0:4], + account.trading_day[4:6], + account.trading_day[6:8] + ] + ) self.gateway.on_account(account) @@ -1121,6 +1126,7 @@ def adjust_price(price: float) -> float: price = 0 return price + class TdxMdApi(): """ 通达信数据行情API实现 @@ -1745,38 +1751,58 @@ class TickCombiner(object): self.gateway.on_tick(spread_tick) if self.is_ratio: - ratio_tick = TickData(gateway_name=self.gateway_name, - symbol=self.symbol, - exchange=Exchange.SPD, - datetime=tick.datetime) + ratio_tick = TickData( + gateway_name=self.gateway_name, + symbol=self.symbol, + exchange=Exchange.SPD, + datetime=tick.datetime + ) ratio_tick.trading_day = tick.trading_day ratio_tick.date = tick.date ratio_tick.time = tick.time # 比率tick - ratio_tick.ask_price_1 = round_to(target=self.price_tick, - value=100 * self.last_leg1_tick.ask_price_1 * self.leg1_ratio / ( - self.last_leg2_tick.bid_price_1 * self.leg2_ratio)) - ratio_tick.ask_volume_1 = min(self.last_leg1_tick.ask_volume_1, self.last_leg2_tick.bid_volume_1) + ratio_tick.ask_price_1 = 100 * self.last_leg1_tick.ask_price_1 * self.leg1_ratio \ + / (self.last_leg2_tick.bid_price_1 * self.leg2_ratio) # noqa + ratio_tick.ask_price_1 = round_to( + target=self.price_tick, + value=ratio_tick.ask_price_1 + ) + + ratio_tick.ask_volume_1 = min(self.last_leg1_tick.ask_volume_1, self.last_leg2_tick.bid_volume_1) + ratio_tick.bid_price_1 = 100 * self.last_leg1_tick.bid_price_1 * self.leg1_ratio \ + / (self.last_leg2_tick.ask_price_1 * self.leg2_ratio) # noqa + ratio_tick.bid_price_1 = round_to( + target=self.price_tick, + value=ratio_tick.bid_price_1 + ) - ratio_tick.bid_price_1 = round_to(target=self.price_tick, - value=100 * self.last_leg1_tick.bid_price_1 * self.leg1_ratio / ( - self.last_leg2_tick.ask_price_1 * self.leg2_ratio)) ratio_tick.bid_volume_1 = min(self.last_leg1_tick.bid_volume_1, self.last_leg2_tick.ask_volume_1) - ratio_tick.lastPrice = round_to(target=self.price_tick, - value=(ratio_tick.ask_price_1 + ratio_tick.bid_price_1) / 2) + ratio_tick.last_price = (ratio_tick.ask_price_1 + ratio_tick.bid_price_1) / 2 + ratio_tick.last_price = round_to( + target=self.price_tick, + value=ratio_tick.last_price + ) # 昨收盘价 if self.last_leg2_tick.pre_close > 0 and self.last_leg1_tick.pre_close > 0: - ratio_tick.pre_close = round_to(target=self.price_tick, - value=100 * self.last_leg1_tick.pre_close * self.leg1_ratio / ( - self.last_leg2_tick.pre_close * self.leg2_ratio)) + ratio_tick.pre_close = 100 * self.last_leg1_tick.pre_close * self.leg1_ratio / ( + self.last_leg2_tick.pre_close * self.leg2_ratio) # noqa + ratio_tick.pre_close = round_to( + target=self.price_tick, + value=ratio_tick.pre_close + ) + # 开盘价 if self.last_leg2_tick.open_price > 0 and self.last_leg1_tick.open_price > 0: - ratio_tick.open_price = round_to(target=self.price_tick, - value=100 * self.last_leg1_tick.open_price * self.leg1_ratio / ( - self.last_leg2_tick.open_price * self.leg2_ratio)) + ratio_tick.open_price = 100 * self.last_leg1_tick.open_price * self.leg1_ratio / ( + self.last_leg2_tick.open_price * self.leg2_ratio) # noqa + ratio_tick.open_price = round_to( + target=self.price_tick, + value=ratio_tick.open_price + ) + # 最高价 if self.ratio_high: self.ratio_high = max(self.ratio_high, ratio_tick.ask_price_1) diff --git a/vnpy/task/__init__.py b/vnpy/task/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/vnpy/task/celery_app.py b/vnpy/task/celery_app.py index b45503d4..4161f153 100644 --- a/vnpy/task/celery_app.py +++ b/vnpy/task/celery_app.py @@ -40,7 +40,7 @@ print(u'Celery 使用redis配置:\nbroker:{}\nbackend:{}'.format(broker, backend app = Celery('vnpy_task', broker=broker) # 动态导入task目录下子任务 -app.conf.CELERY_IMPORTS = ['vnpy.task.celery_app.worker_started'] +# app.conf.CELERY_IMPORTS = ['vnpy.task.celery_app.worker_started'] def worker_started(): diff --git a/vnpy/task/celery_config.json b/vnpy/task/celery_config.json index bbb236ef..ee8fbed9 100644 --- a/vnpy/task/celery_config.json +++ b/vnpy/task/celery_config.json @@ -1,4 +1,4 @@ { - "celery_broker": "amqp://admin:admin@192.168.0.202:5672//", - "celery_backend": "amqp://admin:admin@192.168.0.202:5672//" + "celery_broker": "amqp://admin:admin@127.0.0.1:5672//", + "celery_backend": "amqp://admin:admin@127.0.0.1:5672//" } diff --git a/vnpy/trader/ui/widget.py b/vnpy/trader/ui/widget.py index deae03bb..bb67aea4 100644 --- a/vnpy/trader/ui/widget.py +++ b/vnpy/trader/ui/widget.py @@ -24,7 +24,6 @@ from ..object import OrderRequest, SubscribeRequest from ..utility import load_json, save_json from ..setting import SETTING_FILENAME, SETTINGS - COLOR_LONG = QtGui.QColor("red") COLOR_SHORT = QtGui.QColor("green") COLOR_BID = QtGui.QColor(255, 174, 201) @@ -640,7 +639,7 @@ class TradingWidget(QtWidgets.QWidget): form1.addRow("方向", self.direction_combo) form1.addRow("开平", self.offset_combo) form1.addRow("类型", self.order_type_combo) - form1.addRow( self.checkFixed, self.price_line) + form1.addRow(self.checkFixed, self.price_line) form1.addRow("数量", self.volume_line) form1.addRow("接口", self.gateway_combo) form1.addRow(send_button) @@ -899,8 +898,6 @@ class TradingWidget(QtWidgets.QWidget): except Exception as ex: self.main_engine.write_log(u'tradingWg.autoFillSymbol exception:{}'.format(str(ex))) - - #---------------------------------------------------------------------- def close_position(self, cell): """根据持仓信息自动填写交易组件""" try: @@ -926,13 +923,14 @@ class TradingWidget(QtWidgets.QWidget): self.volume_line.setText(str(pos.volume)) if pos.direction in [Direction.LONG, Direction.NET]: - self.direction_combo.setCurrentText(Direction.SHORT) + self.direction_combo.setCurrentText(Direction.SHORT.value) else: - self.direction_combo.setCurrentText(Direction.LONG) + self.direction_combo.setCurrentText(Direction.LONG.value) except Exception as ex: self.main_engine.write_log(u'tradingWg.closePosition exception:{}'.format(str(ex))) + class ActiveOrderMonitor(OrderMonitor): """ Monitor which shows active order only. diff --git a/vnpy/trader/utility.py b/vnpy/trader/utility.py index 40f52b41..fb7daa95 100644 --- a/vnpy/trader/utility.py +++ b/vnpy/trader/utility.py @@ -394,7 +394,7 @@ def save_df_to_excel(file_name, sheet_name, df): import openpyxl from openpyxl.utils.dataframe import dataframe_to_rows # from openpyxl.drawing.image import Image - except: # noqa + except: # noqa print(u'can not import openpyxl', file=sys.stderr) if 'openpyxl' not in sys.modules: @@ -407,7 +407,7 @@ def save_df_to_excel(file_name, sheet_name, df): try: # 读取文件 wb = openpyxl.load_workbook(file_name) - except: # noqa + except: # noqa # 创建一个excel workbook wb = openpyxl.Workbook() ws = wb.active @@ -416,7 +416,7 @@ def save_df_to_excel(file_name, sheet_name, df): # 定位WorkSheet if ws is None: ws = wb[sheet_name] - except: # noqa + except: # noqa # 创建一个WorkSheet ws = wb.create_sheet() ws.title = sheet_name @@ -450,7 +450,7 @@ def save_text_to_excel(file_name, sheet_name, text): import openpyxl # from openpyxl.utils.dataframe import dataframe_to_rows # from openpyxl.drawing.image import Image - except: # noqa + except: # noqa print(u'can not import openpyxl', file=sys.stderr) if 'openpyxl' not in sys.modules: @@ -461,7 +461,7 @@ def save_text_to_excel(file_name, sheet_name, text): try: # 读取文件 wb = openpyxl.load_workbook(file_name) - except: # noqa + except: # noqa # 创建一个excel workbook wb = openpyxl.Workbook() ws = wb.active @@ -470,7 +470,7 @@ def save_text_to_excel(file_name, sheet_name, text): # 定位WorkSheet if ws is None: ws = wb[sheet_name] - except: # noqa + except: # noqa # 创建一个WorkSheet ws = wb.create_sheet() ws.title = sheet_name @@ -516,7 +516,7 @@ def save_images_to_excel(file_name, sheet_name, image_names): try: # 读取文件 wb = openpyxl.load_workbook(file_name) - except: # noqa + except: # noqa # 创建一个excel workbook wb = openpyxl.Workbook() ws = wb.active @@ -612,6 +612,7 @@ def display_dual_axis(df, columns1, columns2=[], invert_yaxis1=False, invert_yax else: plt.show() + class BarGenerator: """ For: @@ -624,11 +625,11 @@ class BarGenerator: """ def __init__( - self, - on_bar: Callable, - window: int = 0, - on_window_bar: Callable = None, - interval: Interval = Interval.MINUTE + self, + on_bar: Callable, + window: int = 0, + on_window_bar: Callable = None, + interval: Interval = Interval.MINUTE ): """Constructor""" self.bar = None @@ -1225,7 +1226,7 @@ def get_bars(csv_file: str, symbol: str, exchange: Exchange, start_date: datetime = None, - end_date: datetime = None,): + end_date: datetime = None, ): """ 获取bar 数据存储目录: 项目/bar_data diff --git a/win_clean_celery_jobs.bat b/win_clean_celery_jobs.bat new file mode 100644 index 00000000..f883e898 --- /dev/null +++ b/win_clean_celery_jobs.bat @@ -0,0 +1 @@ +celery -A vnpy.task.celery_app purge diff --git a/win_start_celery_worker.bat b/win_start_celery_worker.bat new file mode 100644 index 00000000..8380838a --- /dev/null +++ b/win_start_celery_worker.bat @@ -0,0 +1 @@ +celery worker -c 2 -A vnpy.task.celery_app -P eventlet -l debug -f celery.log