From df102d523731bf1817ce1ba54c6acd47e15286ca Mon Sep 17 00:00:00 2001 From: msincenselee Date: Tue, 14 Jan 2020 13:43:52 +0800 Subject: [PATCH] =?UTF-8?q?[=E5=A2=9E=E5=BC=BA]=20=E6=81=A2=E5=A4=8D?= =?UTF-8?q?=E8=B4=A6=E5=8F=B7=E8=B5=84=E9=87=91=E5=B1=9E=E6=80=A7=EF=BC=8C?= =?UTF-8?q?=E5=A2=9E=E5=8A=A0=E9=A3=8E=E6=8E=A7=EF=BC=88=E4=BB=93=E4=BD=8D?= =?UTF-8?q?=E7=99=BE=E5=88=86=E6=AF=94=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- vnpy/app/cta_strategy_pro/backtesting.py | 1277 ---------------------- vnpy/app/risk_manager/engine.py | 73 +- vnpy/app/risk_manager/ui/widget.py | 4 + vnpy/data/tdx/future_contracts.json | 62 +- vnpy/gateway/ctp/ctp_gateway.py | 8 + vnpy/trader/object.py | 12 +- vnpy/trader/ui/widget.py | 7 +- 7 files changed, 124 insertions(+), 1319 deletions(-) delete mode 100644 vnpy/app/cta_strategy_pro/backtesting.py diff --git a/vnpy/app/cta_strategy_pro/backtesting.py b/vnpy/app/cta_strategy_pro/backtesting.py deleted file mode 100644 index 0c66ead9..00000000 --- a/vnpy/app/cta_strategy_pro/backtesting.py +++ /dev/null @@ -1,1277 +0,0 @@ -from collections import defaultdict -from datetime import date, datetime, timedelta -from typing import Callable -from itertools import product -from functools import lru_cache -from time import time -import multiprocessing -import random - -import numpy as np -import matplotlib.pyplot as plt -import seaborn as sns -from pandas import DataFrame -from deap import creator, base, tools, algorithms - -from vnpy.trader.constant import (Direction, Offset, Exchange, - Interval, Status) -from vnpy.trader.database import database_manager -from vnpy.trader.object import OrderData, TradeData, BarData, TickData -from vnpy.trader.utility import round_to - -from .base import ( - BacktestingMode, - EngineType, - STOPORDER_PREFIX, - StopOrder, - StopOrderStatus, - INTERVAL_DELTA_MAP -) -from .template import CtaTemplate - -sns.set_style("whitegrid") -creator.create("FitnessMax", base.Fitness, weights=(1.0,)) -creator.create("Individual", list, fitness=creator.FitnessMax) - - -class OptimizationSetting: - """ - Setting for runnning optimization. - """ - - def __init__(self): - """""" - self.params = {} - self.target_name = "" - - def add_parameter( - self, name: str, start: float, end: float = None, step: float = None - ): - """""" - if not end and not step: - self.params[name] = [start] - return - - if start >= end: - print("参数优化起始点必须小于终止点") - return - - if step <= 0: - print("参数优化步进必须大于0") - return - - value = start - value_list = [] - - while value <= end: - value_list.append(value) - value += step - - self.params[name] = value_list - - def set_target(self, target_name: str): - """""" - self.target_name = target_name - - def generate_setting(self): - """""" - keys = self.params.keys() - values = self.params.values() - products = list(product(*values)) - - settings = [] - for p in products: - setting = dict(zip(keys, p)) - settings.append(setting) - - return settings - - def generate_setting_ga(self): - """""" - settings_ga = [] - settings = self.generate_setting() - for d in settings: - param = [tuple(i) for i in d.items()] - settings_ga.append(param) - return settings_ga - - -class BacktestingEngine: - """""" - - engine_type = EngineType.BACKTESTING - gateway_name = "BACKTESTING" - - def __init__(self): - """""" - self.vt_symbol = "" - self.symbol = "" - self.exchange = None - self.start = None - self.end = None - self.rate = 0 - self.slippage = 0 - self.size = 1 - self.pricetick = 0 - self.capital = 1_000_000 - self.mode = BacktestingMode.BAR - self.inverse = False - - self.strategy_class = None - self.strategy = None - self.tick: TickData - self.bar: BarData - self.datetime = None - - self.interval = None - self.days = 0 - self.callback = None - self.history_data = [] - - self.stop_order_count = 0 - self.stop_orders = {} - self.active_stop_orders = {} - - self.limit_order_count = 0 - self.limit_orders = {} - self.active_limit_orders = {} - - self.trade_count = 0 - self.trades = {} - - self.logs = [] - - self.daily_results = {} - self.daily_df = None - - def clear_data(self): - """ - Clear all data of last backtesting. - """ - self.strategy = None - self.tick = None - self.bar = None - self.datetime = None - - self.stop_order_count = 0 - self.stop_orders.clear() - self.active_stop_orders.clear() - - self.limit_order_count = 0 - self.limit_orders.clear() - self.active_limit_orders.clear() - - self.trade_count = 0 - self.trades.clear() - - self.logs.clear() - self.daily_results.clear() - - def set_parameters( - self, - vt_symbol: str, - interval: Interval, - start: datetime, - rate: float, - slippage: float, - size: float, - pricetick: float, - capital: int = 0, - end: datetime = None, - mode: BacktestingMode = BacktestingMode.BAR, - inverse: bool = False - ): - """""" - self.mode = mode - self.vt_symbol = vt_symbol - self.interval = Interval(interval) - self.rate = rate - self.slippage = slippage - self.size = size - self.pricetick = pricetick - self.start = start - - self.symbol, exchange_str = self.vt_symbol.split(".") - self.exchange = Exchange(exchange_str) - - self.capital = capital - self.end = end - self.mode = mode - self.inverse = inverse - - def add_strategy(self, strategy_class: type, setting: dict): - """""" - self.strategy_class = strategy_class - self.strategy = strategy_class( - self, strategy_class.__name__, self.vt_symbol, setting - ) - - def load_data(self): - """""" - self.output("开始加载历史数据") - - if not self.end: - self.end = datetime.now() - - if self.start >= self.end: - self.output("起始日期必须小于结束日期") - return - - self.history_data.clear() # Clear previously loaded history data - - # Load 30 days of data each time and allow for progress update - progress_delta = timedelta(days=30) - total_delta = self.end - self.start - interval_delta = INTERVAL_DELTA_MAP[self.interval] - - start = self.start - end = self.start + progress_delta - progress = 0 - - while start < self.end: - end = min(end, self.end) # Make sure end time stays within set range - - if self.mode == BacktestingMode.BAR: - data = load_bar_data( - self.symbol, - self.exchange, - self.interval, - start, - end - ) - else: - data = load_tick_data( - self.symbol, - self.exchange, - start, - end - ) - - self.history_data.extend(data) - - progress += progress_delta / total_delta - progress = min(progress, 1) - progress_bar = "#" * int(progress * 10) - self.output(f"加载进度:{progress_bar} [{progress:.0%}]") - - start = end + interval_delta - end += (progress_delta + interval_delta) - - self.output(f"历史数据加载完成,数据量:{len(self.history_data)}") - - def run_backtesting(self): - """""" - if self.mode == BacktestingMode.BAR: - func = self.new_bar - else: - func = self.new_tick - - self.strategy.on_init() - - # Use the first [days] of history data for initializing strategy - day_count = 0 - ix = 0 - - for ix, data in enumerate(self.history_data): - if self.datetime and data.datetime.day != self.datetime.day: - day_count += 1 - if day_count >= self.days: - break - - self.datetime = data.datetime - self.callback(data) - - self.strategy.inited = True - self.output("策略初始化完成") - - self.strategy.on_start() - self.strategy.trading = True - self.output("开始回放历史数据") - - # Use the rest of history data for running backtesting - for data in self.history_data[ix:]: - func(data) - - self.output("历史数据回放结束") - - def calculate_result(self): - """""" - self.output("开始计算逐日盯市盈亏") - - if not self.trades: - self.output("成交记录为空,无法计算") - return - - # Add trade data into daily reuslt. - for trade in self.trades.values(): - d = trade.datetime.date() - daily_result = self.daily_results[d] - daily_result.add_trade(trade) - - # Calculate daily result by iteration. - pre_close = 0 - start_pos = 0 - - for daily_result in self.daily_results.values(): - daily_result.calculate_pnl( - pre_close, - start_pos, - self.size, - self.rate, - self.slippage, - self.inverse - ) - - pre_close = daily_result.close_price - start_pos = daily_result.end_pos - - # Generate dataframe - results = defaultdict(list) - - for daily_result in self.daily_results.values(): - for key, value in daily_result.__dict__.items(): - results[key].append(value) - - self.daily_df = DataFrame.from_dict(results).set_index("date") - - self.output("逐日盯市盈亏计算完成") - return self.daily_df - - def calculate_statistics(self, df: DataFrame = None, output=True): - """""" - self.output("开始计算策略统计指标") - - # Check DataFrame input exterior - if df is None: - df = self.daily_df - - # Check for init DataFrame - if df is None: - # Set all statistics to 0 if no trade. - start_date = "" - end_date = "" - total_days = 0 - profit_days = 0 - loss_days = 0 - end_balance = 0 - max_drawdown = 0 - max_ddpercent = 0 - max_drawdown_duration = 0 - total_net_pnl = 0 - daily_net_pnl = 0 - total_commission = 0 - daily_commission = 0 - total_slippage = 0 - daily_slippage = 0 - total_turnover = 0 - daily_turnover = 0 - total_trade_count = 0 - daily_trade_count = 0 - total_return = 0 - annual_return = 0 - daily_return = 0 - return_std = 0 - sharpe_ratio = 0 - return_drawdown_ratio = 0 - else: - # Calculate balance related time series data - df["balance"] = df["net_pnl"].cumsum() + self.capital - df["return"] = np.log(df["balance"] / df["balance"].shift(1)).fillna(0) - df["highlevel"] = ( - df["balance"].rolling( - min_periods=1, window=len(df), center=False).max() - ) - df["drawdown"] = df["balance"] - df["highlevel"] - df["ddpercent"] = df["drawdown"] / df["highlevel"] * 100 - - # Calculate statistics value - start_date = df.index[0] - end_date = df.index[-1] - - total_days = len(df) - profit_days = len(df[df["net_pnl"] > 0]) - loss_days = len(df[df["net_pnl"] < 0]) - - end_balance = df["balance"].iloc[-1] - max_drawdown = df["drawdown"].min() - max_ddpercent = df["ddpercent"].min() - max_drawdown_end = df["drawdown"].idxmin() - max_drawdown_start = df["balance"][:max_drawdown_end].argmax() - max_drawdown_duration = (max_drawdown_end - max_drawdown_start).days - - total_net_pnl = df["net_pnl"].sum() - daily_net_pnl = total_net_pnl / total_days - - total_commission = df["commission"].sum() - daily_commission = total_commission / total_days - - total_slippage = df["slippage"].sum() - daily_slippage = total_slippage / total_days - - total_turnover = df["turnover"].sum() - daily_turnover = total_turnover / total_days - - total_trade_count = df["trade_count"].sum() - daily_trade_count = total_trade_count / total_days - - total_return = (end_balance / self.capital - 1) * 100 - annual_return = total_return / total_days * 240 - daily_return = df["return"].mean() * 100 - return_std = df["return"].std() * 100 - - if return_std: - sharpe_ratio = daily_return / return_std * np.sqrt(240) - else: - sharpe_ratio = 0 - - return_drawdown_ratio = -total_return / max_ddpercent - - # Output - if output: - self.output("-" * 30) - self.output(f"首个交易日:\t{start_date}") - self.output(f"最后交易日:\t{end_date}") - - self.output(f"总交易日:\t{total_days}") - self.output(f"盈利交易日:\t{profit_days}") - self.output(f"亏损交易日:\t{loss_days}") - - self.output(f"起始资金:\t{self.capital:,.2f}") - self.output(f"结束资金:\t{end_balance:,.2f}") - - self.output(f"总收益率:\t{total_return:,.2f}%") - self.output(f"年化收益:\t{annual_return:,.2f}%") - self.output(f"最大回撤: \t{max_drawdown:,.2f}") - self.output(f"百分比最大回撤: {max_ddpercent:,.2f}%") - self.output(f"最长回撤天数: \t{max_drawdown_duration}") - - self.output(f"总盈亏:\t{total_net_pnl:,.2f}") - self.output(f"总手续费:\t{total_commission:,.2f}") - self.output(f"总滑点:\t{total_slippage:,.2f}") - self.output(f"总成交金额:\t{total_turnover:,.2f}") - self.output(f"总成交笔数:\t{total_trade_count}") - - self.output(f"日均盈亏:\t{daily_net_pnl:,.2f}") - self.output(f"日均手续费:\t{daily_commission:,.2f}") - self.output(f"日均滑点:\t{daily_slippage:,.2f}") - self.output(f"日均成交金额:\t{daily_turnover:,.2f}") - self.output(f"日均成交笔数:\t{daily_trade_count}") - - self.output(f"日均收益率:\t{daily_return:,.2f}%") - self.output(f"收益标准差:\t{return_std:,.2f}%") - self.output(f"Sharpe Ratio:\t{sharpe_ratio:,.2f}") - self.output(f"收益回撤比:\t{return_drawdown_ratio:,.2f}") - - statistics = { - "start_date": start_date, - "end_date": end_date, - "total_days": total_days, - "profit_days": profit_days, - "loss_days": loss_days, - "capital": self.capital, - "end_balance": end_balance, - "max_drawdown": max_drawdown, - "max_ddpercent": max_ddpercent, - "max_drawdown_duration": max_drawdown_duration, - "total_net_pnl": total_net_pnl, - "daily_net_pnl": daily_net_pnl, - "total_commission": total_commission, - "daily_commission": daily_commission, - "total_slippage": total_slippage, - "daily_slippage": daily_slippage, - "total_turnover": total_turnover, - "daily_turnover": daily_turnover, - "total_trade_count": total_trade_count, - "daily_trade_count": daily_trade_count, - "total_return": total_return, - "annual_return": annual_return, - "daily_return": daily_return, - "return_std": return_std, - "sharpe_ratio": sharpe_ratio, - "return_drawdown_ratio": return_drawdown_ratio, - } - - return statistics - - def show_chart(self, df: DataFrame = None): - """""" - # Check DataFrame input exterior - if df is None: - df = self.daily_df - - # Check for init DataFrame - if df is None: - return - - plt.figure(figsize=(10, 16)) - - balance_plot = plt.subplot(4, 1, 1) - balance_plot.set_title("Balance") - df["balance"].plot(legend=True) - - drawdown_plot = plt.subplot(4, 1, 2) - drawdown_plot.set_title("Drawdown") - drawdown_plot.fill_between(range(len(df)), df["drawdown"].values) - - pnl_plot = plt.subplot(4, 1, 3) - pnl_plot.set_title("Daily Pnl") - df["net_pnl"].plot(kind="bar", legend=False, grid=False, xticks=[]) - - distribution_plot = plt.subplot(4, 1, 4) - distribution_plot.set_title("Daily Pnl Distribution") - df["net_pnl"].hist(bins=50) - - plt.show() - - def run_optimization(self, optimization_setting: OptimizationSetting, output=True): - """""" - # Get optimization setting and target - settings = optimization_setting.generate_setting() - target_name = optimization_setting.target_name - - if not settings: - self.output("优化参数组合为空,请检查") - return - - if not target_name: - self.output("优化目标未设置,请检查") - return - - # Use multiprocessing pool for running backtesting with different setting - pool = multiprocessing.Pool(multiprocessing.cpu_count()) - - results = [] - for setting in settings: - result = (pool.apply_async(optimize, ( - target_name, - self.strategy_class, - setting, - self.vt_symbol, - self.interval, - self.start, - self.rate, - self.slippage, - self.size, - self.pricetick, - self.capital, - self.end, - self.mode, - self.inverse - ))) - results.append(result) - - pool.close() - pool.join() - - # Sort results and output - result_values = [result.get() for result in results] - result_values.sort(reverse=True, key=lambda result: result[1]) - - if output: - for value in result_values: - msg = f"参数:{value[0]}, 目标:{value[1]}" - self.output(msg) - - return result_values - - def run_ga_optimization(self, optimization_setting: OptimizationSetting, population_size=100, ngen_size=30, output=True): - """""" - # Get optimization setting and target - settings = optimization_setting.generate_setting_ga() - target_name = optimization_setting.target_name - - if not settings: - self.output("优化参数组合为空,请检查") - return - - if not target_name: - self.output("优化目标未设置,请检查") - return - - # Define parameter generation function - def generate_parameter(): - """""" - return random.choice(settings) - - def mutate_individual(individual, indpb): - """""" - size = len(individual) - paramlist = generate_parameter() - for i in range(size): - if random.random() < indpb: - individual[i] = paramlist[i] - return individual, - - # Create ga object function - global ga_target_name - global ga_strategy_class - global ga_setting - global ga_vt_symbol - global ga_interval - global ga_start - global ga_rate - global ga_slippage - global ga_size - global ga_pricetick - global ga_capital - global ga_end - global ga_mode - global ga_inverse - - ga_target_name = target_name - ga_strategy_class = self.strategy_class - ga_setting = settings[0] - ga_vt_symbol = self.vt_symbol - ga_interval = self.interval - ga_start = self.start - ga_rate = self.rate - ga_slippage = self.slippage - ga_size = self.size - ga_pricetick = self.pricetick - ga_capital = self.capital - ga_end = self.end - ga_mode = self.mode - ga_inverse = self.inverse - - # Set up genetic algorithem - toolbox = base.Toolbox() - toolbox.register("individual", tools.initIterate, creator.Individual, generate_parameter) - toolbox.register("population", tools.initRepeat, list, toolbox.individual) - toolbox.register("mate", tools.cxTwoPoint) - toolbox.register("mutate", mutate_individual, indpb=1) - toolbox.register("evaluate", ga_optimize) - toolbox.register("select", tools.selNSGA2) - - total_size = len(settings) - pop_size = population_size # number of individuals in each generation - lambda_ = pop_size # number of children to produce at each generation - mu = int(pop_size * 0.8) # number of individuals to select for the next generation - - cxpb = 0.95 # probability that an offspring is produced by crossover - mutpb = 1 - cxpb # probability that an offspring is produced by mutation - ngen = ngen_size # number of generation - - pop = toolbox.population(pop_size) - hof = tools.ParetoFront() # end result of pareto front - - stats = tools.Statistics(lambda ind: ind.fitness.values) - np.set_printoptions(suppress=True) - stats.register("mean", np.mean, axis=0) - stats.register("std", np.std, axis=0) - stats.register("min", np.min, axis=0) - stats.register("max", np.max, axis=0) - - # Multiprocessing is not supported yet. - # pool = multiprocessing.Pool(multiprocessing.cpu_count()) - # toolbox.register("map", pool.map) - - # Run ga optimization - self.output(f"参数优化空间:{total_size}") - self.output(f"每代族群总数:{pop_size}") - self.output(f"优良筛选个数:{mu}") - self.output(f"迭代次数:{ngen}") - self.output(f"交叉概率:{cxpb:.0%}") - self.output(f"突变概率:{mutpb:.0%}") - - start = time() - - algorithms.eaMuPlusLambda( - pop, - toolbox, - mu, - lambda_, - cxpb, - mutpb, - ngen, - stats, - halloffame=hof - ) - - end = time() - cost = int((end - start)) - - self.output(f"遗传算法优化完成,耗时{cost}秒") - - # Return result list - results = [] - - for parameter_values in hof: - setting = dict(parameter_values) - target_value = ga_optimize(parameter_values)[0] - results.append((setting, target_value, {})) - - return results - - def update_daily_close(self, price: float): - """""" - d = self.datetime.date() - - daily_result = self.daily_results.get(d, None) - if daily_result: - daily_result.close_price = price - else: - self.daily_results[d] = DailyResult(d, price) - - def new_bar(self, bar: BarData): - """""" - self.bar = bar - self.datetime = bar.datetime - - self.cross_limit_order() - self.cross_stop_order() - self.strategy.on_bar(bar) - - self.update_daily_close(bar.close_price) - - def new_tick(self, tick: TickData): - """""" - self.tick = tick - self.datetime = tick.datetime - - self.cross_limit_order() - self.cross_stop_order() - self.strategy.on_tick(tick) - - self.update_daily_close(tick.last_price) - - def cross_limit_order(self): - """ - Cross limit order with last bar/tick data. - """ - if self.mode == BacktestingMode.BAR: - long_cross_price = self.bar.low_price - short_cross_price = self.bar.high_price - long_best_price = self.bar.open_price - short_best_price = self.bar.open_price - else: - long_cross_price = self.tick.ask_price_1 - short_cross_price = self.tick.bid_price_1 - long_best_price = long_cross_price - short_best_price = short_cross_price - - for order in list(self.active_limit_orders.values()): - # Push order update with status "not traded" (pending). - if order.status == Status.SUBMITTING: - order.status = Status.NOTTRADED - self.strategy.on_order(order) - - # Check whether limit orders can be filled. - long_cross = ( - order.direction == Direction.LONG - and order.price >= long_cross_price - and long_cross_price > 0 - ) - - short_cross = ( - order.direction == Direction.SHORT - and order.price <= short_cross_price - and short_cross_price > 0 - ) - - if not long_cross and not short_cross: - continue - - # Push order udpate with status "all traded" (filled). - order.traded = order.volume - order.status = Status.ALLTRADED - self.strategy.on_order(order) - - self.active_limit_orders.pop(order.vt_orderid) - - # Push trade update - self.trade_count += 1 - - if long_cross: - trade_price = min(order.price, long_best_price) - pos_change = order.volume - else: - trade_price = max(order.price, short_best_price) - pos_change = -order.volume - - 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.datetime.strftime("%H:%M:%S"), - gateway_name=self.gateway_name, - ) - trade.datetime = self.datetime - - self.strategy.pos += pos_change - self.strategy.on_trade(trade) - - self.trades[trade.vt_tradeid] = trade - - def cross_stop_order(self): - """ - Cross stop order with last bar/tick data. - """ - if self.mode == BacktestingMode.BAR: - long_cross_price = self.bar.high_price - short_cross_price = self.bar.low_price - long_best_price = self.bar.open_price - short_best_price = self.bar.open_price - else: - long_cross_price = self.tick.last_price - short_cross_price = self.tick.last_price - long_best_price = long_cross_price - short_best_price = short_cross_price - - for stop_order in list(self.active_stop_orders.values()): - # 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 - - order = OrderData( - symbol=self.symbol, - exchange=self.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.datetime - - self.limit_orders[order.vt_orderid] = order - - # Create trade data. - if long_cross: - trade_price = max(stop_order.price, long_best_price) - pos_change = order.volume - else: - trade_price = min(stop_order.price, short_best_price) - pos_change = -order.volume - - 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.datetime.strftime("%H:%M:%S"), - gateway_name=self.gateway_name, - ) - trade.datetime = self.datetime - - self.trades[trade.vt_tradeid] = 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. - self.strategy.on_stop_order(stop_order) - self.strategy.on_order(order) - - self.strategy.pos += pos_change - self.strategy.on_trade(trade) - - def load_bar( - self, vt_symbol: str, days: int, interval: Interval, callback: Callable - ): - """""" - self.days = days - self.callback = callback - - def load_tick(self, vt_symbol: str, days: int, callback: Callable): - """""" - self.days = days - self.callback = callback - - def send_order( - self, - strategy: CtaTemplate, - direction: Direction, - offset: Offset, - price: float, - volume: float, - stop: bool, - lock: bool - ): - """""" - price = round_to(price, self.pricetick) - if stop: - vt_orderid = self.send_stop_order(direction, offset, price, volume) - else: - vt_orderid = self.send_limit_order(direction, offset, price, volume) - return [vt_orderid] - - def send_stop_order( - self, - direction: Direction, - offset: Offset, - price: float, - volume: float - ): - """""" - self.stop_order_count += 1 - - stop_order = StopOrder( - vt_symbol=self.vt_symbol, - direction=direction, - offset=offset, - price=price, - volume=volume, - stop_orderid=f"{STOPORDER_PREFIX}.{self.stop_order_count}", - strategy_name=self.strategy.strategy_name, - ) - - 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 send_limit_order( - self, - direction: Direction, - offset: Offset, - price: float, - volume: float - ): - """""" - self.limit_order_count += 1 - - order = OrderData( - symbol=self.symbol, - exchange=self.exchange, - orderid=str(self.limit_order_count), - direction=direction, - offset=offset, - price=price, - volume=volume, - status=Status.SUBMITTING, - gateway_name=self.gateway_name, - ) - order.datetime = self.datetime - - self.active_limit_orders[order.vt_orderid] = order - self.limit_orders[order.vt_orderid] = order - - return order.vt_orderid - - def cancel_order(self, strategy: CtaTemplate, vt_orderid: str): - """ - Cancel order by vt_orderid. - """ - if vt_orderid.startswith(STOPORDER_PREFIX): - self.cancel_stop_order(strategy, vt_orderid) - else: - self.cancel_limit_order(strategy, vt_orderid) - - def cancel_stop_order(self, strategy: CtaTemplate, vt_orderid: str): - """""" - if vt_orderid not in self.active_stop_orders: - return - stop_order = self.active_stop_orders.pop(vt_orderid) - - stop_order.status = StopOrderStatus.CANCELLED - self.strategy.on_stop_order(stop_order) - - def cancel_limit_order(self, strategy: CtaTemplate, vt_orderid: str): - """""" - if vt_orderid not in self.active_limit_orders: - return - order = self.active_limit_orders.pop(vt_orderid) - - order.status = Status.CANCELLED - self.strategy.on_order(order) - - def cancel_all(self, strategy: CtaTemplate): - """ - Cancel all orders, both limit and stop. - """ - vt_orderids = list(self.active_limit_orders.keys()) - for vt_orderid in vt_orderids: - self.cancel_limit_order(strategy, vt_orderid) - - stop_orderids = list(self.active_stop_orders.keys()) - for vt_orderid in stop_orderids: - self.cancel_stop_order(strategy, vt_orderid) - - def write_log(self, msg: str, strategy: CtaTemplate = None): - """ - Write log message. - """ - msg = f"{self.datetime}\t{msg}" - self.logs.append(msg) - - def send_email(self, msg: str, strategy: CtaTemplate = None): - """ - Send email to default receiver. - """ - pass - - def sync_strategy_data(self, strategy: CtaTemplate): - """ - Sync strategy data into json file. - """ - pass - - def get_engine_type(self): - """ - Return engine type. - """ - return self.engine_type - - def put_strategy_event(self, strategy: CtaTemplate): - """ - Put an event to update strategy status. - """ - pass - - def output(self, msg): - """ - Output message of backtesting engine. - """ - print(f"{datetime.now()}\t{msg}") - - def get_all_trades(self): - """ - Return all trade data of current backtesting result. - """ - return list(self.trades.values()) - - def get_all_orders(self): - """ - Return all limit order data of current backtesting result. - """ - return list(self.limit_orders.values()) - - def get_all_daily_results(self): - """ - Return all daily result data. - """ - return list(self.daily_results.values()) - - -class DailyResult: - """""" - - def __init__(self, date: date, close_price: float): - """""" - self.date = date - self.close_price = close_price - self.pre_close = 0 - - self.trades = [] - self.trade_count = 0 - - self.start_pos = 0 - self.end_pos = 0 - - self.turnover = 0 - self.commission = 0 - self.slippage = 0 - - self.trading_pnl = 0 - self.holding_pnl = 0 - self.total_pnl = 0 - self.net_pnl = 0 - - def add_trade(self, trade: TradeData): - """""" - self.trades.append(trade) - - def calculate_pnl( - self, - pre_close: float, - start_pos: float, - size: int, - rate: float, - slippage: float, - inverse: bool - ): - """""" - # If no pre_close provided on the first day, - # use value 1 to avoid zero division error - if pre_close: - self.pre_close = pre_close - else: - self.pre_close = 1 - - # Holding pnl is the pnl from holding position at day start - self.start_pos = start_pos - self.end_pos = start_pos - - if not inverse: # For normal contract - self.holding_pnl = self.start_pos * \ - (self.close_price - self.pre_close) * size - else: # For crypto currency inverse contract - self.holding_pnl = self.start_pos * \ - (1 / self.pre_close - 1 / self.close_price) * size - - # Trading pnl is the pnl from new trade during the day - self.trade_count = len(self.trades) - - for trade in self.trades: - if trade.direction == Direction.LONG: - pos_change = trade.volume - else: - pos_change = -trade.volume - - self.end_pos += pos_change - - # For normal contract - if not inverse: - turnover = trade.volume * size * trade.price - self.trading_pnl += pos_change * \ - (self.close_price - trade.price) * size - self.slippage += trade.volume * size * slippage - # For crypto currency inverse contract - else: - turnover = trade.volume * size / trade.price - self.trading_pnl += pos_change * \ - (1 / trade.price - 1 / self.close_price) * size - self.slippage += trade.volume * size * slippage / (trade.price ** 2) - - self.turnover += turnover - self.commission += turnover * rate - - # Net pnl takes account of commission and slippage cost - self.total_pnl = self.trading_pnl + self.holding_pnl - self.net_pnl = self.total_pnl - self.commission - self.slippage - - -def optimize( - target_name: str, - strategy_class: CtaTemplate, - setting: dict, - vt_symbol: str, - interval: Interval, - start: datetime, - rate: float, - slippage: float, - size: float, - pricetick: float, - capital: int, - end: datetime, - mode: BacktestingMode, - inverse: bool -): - """ - Function for running in multiprocessing.pool - """ - engine = BacktestingEngine() - - engine.set_parameters( - vt_symbol=vt_symbol, - interval=interval, - start=start, - rate=rate, - slippage=slippage, - size=size, - pricetick=pricetick, - capital=capital, - end=end, - mode=mode, - inverse=inverse - ) - - engine.add_strategy(strategy_class, setting) - engine.load_data() - engine.run_backtesting() - engine.calculate_result() - statistics = engine.calculate_statistics(output=False) - - target_value = statistics[target_name] - return (str(setting), target_value, statistics) - - -@lru_cache(maxsize=1000000) -def _ga_optimize(parameter_values: tuple): - """""" - setting = dict(parameter_values) - - result = optimize( - ga_target_name, - ga_strategy_class, - setting, - ga_vt_symbol, - ga_interval, - ga_start, - ga_rate, - ga_slippage, - ga_size, - ga_pricetick, - ga_capital, - ga_end, - ga_mode, - ga_inverse - ) - return (result[1],) - - -def ga_optimize(parameter_values: list): - """""" - return _ga_optimize(tuple(parameter_values)) - - -@lru_cache(maxsize=999) -def load_bar_data( - symbol: str, - exchange: Exchange, - interval: Interval, - start: datetime, - end: datetime -): - """""" - return database_manager.load_bar_data( - symbol, exchange, interval, start, end - ) - - -@lru_cache(maxsize=999) -def load_tick_data( - symbol: str, - exchange: Exchange, - start: datetime, - end: datetime -): - """""" - return database_manager.load_tick_data( - symbol, exchange, start, end - ) - - -# GA related global value -ga_end = None -ga_mode = None -ga_target_name = None -ga_strategy_class = None -ga_setting = None -ga_vt_symbol = None -ga_interval = None -ga_start = None -ga_rate = None -ga_slippage = None -ga_size = None -ga_pricetick = None -ga_capital = None diff --git a/vnpy/app/risk_manager/engine.py b/vnpy/app/risk_manager/engine.py index c6193bc5..37493165 100644 --- a/vnpy/app/risk_manager/engine.py +++ b/vnpy/app/risk_manager/engine.py @@ -1,10 +1,13 @@ """""" +from copy import copy from collections import defaultdict +from datetime import datetime +from vnpy.trader.constant import Offset from vnpy.trader.object import OrderRequest, LogData from vnpy.event import Event, EventEngine, EVENT_TIMER from vnpy.trader.engine import BaseEngine, MainEngine -from vnpy.trader.event import EVENT_TRADE, EVENT_ORDER, EVENT_LOG +from vnpy.trader.event import EVENT_TRADE, EVENT_ORDER, EVENT_LOG, EVENT_ACCOUNT from vnpy.trader.constant import Status from vnpy.trader.utility import load_json, save_json @@ -23,20 +26,28 @@ class RiskManagerEngine(BaseEngine): self.active = False self.order_flow_count = 0 - self.order_flow_limit = 50 + self.order_flow_limit = 500 self.order_flow_clear = 1 self.order_flow_timer = 0 - self.order_size_limit = 100 + self.order_size_limit = 1000 self.trade_count = 0 - self.trade_limit = 1000 + self.trade_limit = 10000 - self.order_cancel_limit = 500 + self.order_cancel_limit = 5000 self.order_cancel_counts = defaultdict(int) - self.active_order_limit = 50 + self.active_order_limit = 500 + + # 总仓位相关(0~100+) + self.percent_limit = 100 # 仓位比例限制 + self.last_over_time = None # 启动风控后,最后一次超过仓位限制的时间 + + self.account_dict = {} # 资金账号信息 + self.gateway_dict = {} # 记录gateway对应的仓位比例 + self.currency_list = [] # 资金账号风控管理得币种 self.load_setting() self.register_event() @@ -66,7 +77,7 @@ class RiskManagerEngine(BaseEngine): self.trade_limit = setting["trade_limit"] self.active_order_limit = setting["active_order_limit"] self.order_cancel_limit = setting["order_cancel_limit"] - + self.percent_limit = setting.get('percent_limit', 100) if self.active: self.write_log("交易风控功能启动") else: @@ -82,6 +93,7 @@ class RiskManagerEngine(BaseEngine): "trade_limit": self.trade_limit, "active_order_limit": self.active_order_limit, "order_cancel_limit": self.order_cancel_limit, + "percent_limit": self.percent_limit } return setting @@ -103,6 +115,7 @@ class RiskManagerEngine(BaseEngine): self.event_engine.register(EVENT_TRADE, self.process_trade_event) self.event_engine.register(EVENT_TIMER, self.process_timer_event) self.event_engine.register(EVENT_ORDER, self.process_order_event) + self.event_engine.register(EVENT_ACCOUNT, self.process_account_event) def process_order_event(self, event: Event): """""" @@ -124,6 +137,47 @@ class RiskManagerEngine(BaseEngine): self.order_flow_count = 0 self.order_flow_timer = 0 + def process_account_event(self, event): + """更新账号资金 + add by Incense + """ + account = event.data + + # 如果有币种过滤,就进行过滤动作 + if len(self.currency_list) > 0: + if account.currency not in self.currency_list: + return + + # 净值为0得不做处理 + if account.balance == 0: + return + + # 保存在dict中 + k = u'{}_{}'.format(account.vt_accountid, account.currency) + self.account_dict.update({k: copy(account)}) + + # 计算当前资金仓位 + if account.balance == 0: + account_percent = 0 + else: + account_percent = round((account.balance - account.available) * 100 / account.balance, 2) + + if not self.active: + return + + # 更新gateway对应的当前资金仓位 + self.gateway_dict.update({account.gateway_name: account_percent}) + + # 判断资金仓位超出 + if account_percent > self.percent_limit: + if self.last_over_time is None or ( + datetime.now() - self.last_over_time).total_seconds() > 60 * 10: + self.last_over_time = datetime.now() + msg = u'账号:{} 今净值:{},保证金占用{} 超过设定:{}' \ + .format(account.vt_accountid, + account.balance, account_percent, self.percent_limit) + self.write_log(msg) + def write_log(self, msg: str): """""" log = LogData(msg=msg, gateway_name="RiskManager") @@ -170,6 +224,11 @@ class RiskManagerEngine(BaseEngine): f"当日{req.symbol}撤单次数{self.order_cancel_counts[req.symbol]},超过限制{self.order_cancel_limit}") return False + # 开仓时,检查是否超过保证金比率 + if req.offset == Offset.OPEN and self.gateway_dict.get(gateway_name, 0) > self.percent_limit: + self.write_log(f'当前资金仓位{self.gateway_dict[gateway_name]}超过仓位限制:{self.percent_limit}') + return False + # Add flow count if pass all checks self.order_flow_count += 1 return True diff --git a/vnpy/app/risk_manager/ui/widget.py b/vnpy/app/risk_manager/ui/widget.py index c9ce1e68..f106a3c6 100644 --- a/vnpy/app/risk_manager/ui/widget.py +++ b/vnpy/app/risk_manager/ui/widget.py @@ -31,6 +31,7 @@ class RiskManager(QtWidgets.QDialog): self.trade_limit_spin = RiskManagerSpinBox() self.active_limit_spin = RiskManagerSpinBox() self.cancel_limit_spin = RiskManagerSpinBox() + self.percent_limit_spin = RiskManagerSpinBox() save_button = QtWidgets.QPushButton("保存") save_button.clicked.connect(self.save_setting) @@ -44,6 +45,7 @@ class RiskManager(QtWidgets.QDialog): form.addRow("总成交上限(笔)", self.trade_limit_spin) form.addRow("活动委托上限(笔)", self.active_limit_spin) form.addRow("合约撤单上限(笔)", self.cancel_limit_spin) + form.addRow("资金仓位上限(%)", self.percent_limit_spin) form.addRow(save_button) self.setLayout(form) @@ -68,6 +70,7 @@ class RiskManager(QtWidgets.QDialog): "trade_limit": self.trade_limit_spin.value(), "active_order_limit": self.active_limit_spin.value(), "order_cancel_limit": self.cancel_limit_spin.value(), + "percent_limit": self.percent_limit_spin.value(), } self.rm_engine.update_setting(setting) @@ -89,6 +92,7 @@ class RiskManager(QtWidgets.QDialog): self.trade_limit_spin.setValue(setting["trade_limit"]) self.active_limit_spin.setValue(setting["active_order_limit"]) self.cancel_limit_spin.setValue(setting["order_cancel_limit"]) + self.percent_limit_spin.setValue(setting.get('percent_limit', 100)) def exec_(self): """""" diff --git a/vnpy/data/tdx/future_contracts.json b/vnpy/data/tdx/future_contracts.json index 712c9477..518ff4fa 100644 --- a/vnpy/data/tdx/future_contracts.json +++ b/vnpy/data/tdx/future_contracts.json @@ -13,7 +13,7 @@ "mi_symbol": "ag2007", "full_symbol": "AG2007", "exchange": "SHFE", - "margin_rate": 0.07, + "margin_rate": 0.08, "symbol_size": 15, "price_tick": 1.0 }, @@ -22,7 +22,7 @@ "mi_symbol": "al2003", "full_symbol": "AL2003", "exchange": "SHFE", - "margin_rate": 0.07, + "margin_rate": 0.1, "symbol_size": 5, "price_tick": 5.0 }, @@ -31,7 +31,7 @@ "mi_symbol": "AP005", "full_symbol": "AP2005", "exchange": "CZCE", - "margin_rate": 0.08, + "margin_rate": 0.07, "symbol_size": 10, "price_tick": 1.0 }, @@ -40,7 +40,7 @@ "mi_symbol": "au2006", "full_symbol": "AU2006", "exchange": "SHFE", - "margin_rate": 0.06, + "margin_rate": 0.08, "symbol_size": 1000, "price_tick": 0.02 }, @@ -67,7 +67,7 @@ "mi_symbol": "bu2006", "full_symbol": "BU2006", "exchange": "SHFE", - "margin_rate": 0.09, + "margin_rate": 0.1, "symbol_size": 10, "price_tick": 2.0 }, @@ -112,7 +112,7 @@ "mi_symbol": "cu2003", "full_symbol": "CU2003", "exchange": "SHFE", - "margin_rate": 0.07, + "margin_rate": 0.09, "symbol_size": 5, "price_tick": 10.0 }, @@ -139,7 +139,7 @@ "mi_symbol": "eg2005", "full_symbol": "EG2005", "exchange": "DCE", - "margin_rate": 0.06, + "margin_rate": 0.05, "symbol_size": 10, "price_tick": 1.0 }, @@ -148,9 +148,9 @@ "mi_symbol": "fb2005", "full_symbol": "FB2005", "exchange": "DCE", - "margin_rate": 0.1, - "symbol_size": 10, - "price_tick": 0.5 + "margin_rate": 0.2, + "symbol_size": 500, + "price_tick": 0.05 }, "FG": { "underlying_symbol": "FG", @@ -166,7 +166,7 @@ "mi_symbol": "fu2005", "full_symbol": "FU2005", "exchange": "SHFE", - "margin_rate": 0.1, + "margin_rate": 0.2, "symbol_size": 10, "price_tick": 1.0 }, @@ -175,7 +175,7 @@ "mi_symbol": "hc2005", "full_symbol": "HC2005", "exchange": "SHFE", - "margin_rate": 0.08, + "margin_rate": 0.1, "symbol_size": 10, "price_tick": 1.0 }, @@ -184,7 +184,7 @@ "mi_symbol": "i2005", "full_symbol": "I2005", "exchange": "DCE", - "margin_rate": 0.08, + "margin_rate": 0.05, "symbol_size": 100, "price_tick": 0.5 }, @@ -193,7 +193,7 @@ "mi_symbol": "IC2003", "full_symbol": "IC2003", "exchange": "CFFEX", - "margin_rate": 0.12, + "margin_rate": 0.1, "symbol_size": 200, "price_tick": 0.2 }, @@ -220,7 +220,7 @@ "mi_symbol": "j2005", "full_symbol": "J2005", "exchange": "DCE", - "margin_rate": 0.08, + "margin_rate": 0.05, "symbol_size": 100, "price_tick": 0.5 }, @@ -229,7 +229,7 @@ "mi_symbol": "jd2005", "full_symbol": "JD2005", "exchange": "DCE", - "margin_rate": 0.07, + "margin_rate": 0.08, "symbol_size": 10, "price_tick": 1.0 }, @@ -238,7 +238,7 @@ "mi_symbol": "jm2005", "full_symbol": "JM2005", "exchange": "DCE", - "margin_rate": 0.08, + "margin_rate": 0.05, "symbol_size": 60, "price_tick": 0.5 }, @@ -328,7 +328,7 @@ "mi_symbol": "pb2003", "full_symbol": "PB2003", "exchange": "SHFE", - "margin_rate": 0.07, + "margin_rate": 0.1, "symbol_size": 5, "price_tick": 5.0 }, @@ -355,7 +355,7 @@ "mi_symbol": "rb2005", "full_symbol": "RB2005", "exchange": "SHFE", - "margin_rate": 0.08, + "margin_rate": 0.1, "symbol_size": 10, "price_tick": 1.0 }, @@ -373,7 +373,7 @@ "mi_symbol": "RM005", "full_symbol": "RM2005", "exchange": "CZCE", - "margin_rate": 0.06, + "margin_rate": 0.05, "symbol_size": 10, "price_tick": 1.0 }, @@ -391,7 +391,7 @@ "mi_symbol": "RS011", "full_symbol": "RS2011", "exchange": "CZCE", - "margin_rate": 0.2, + "margin_rate": 0.05, "symbol_size": 10, "price_tick": 1.0 }, @@ -400,7 +400,7 @@ "mi_symbol": "ru2005", "full_symbol": "RU2005", "exchange": "SHFE", - "margin_rate": 0.09, + "margin_rate": 0.1, "symbol_size": 10, "price_tick": 5.0 }, @@ -418,7 +418,7 @@ "mi_symbol": "sc2003", "full_symbol": "SC2003", "exchange": "INE", - "margin_rate": 0.07, + "margin_rate": 0.05, "symbol_size": 1000, "price_tick": 0.1 }, @@ -427,7 +427,7 @@ "mi_symbol": "SF005", "full_symbol": "SF2005", "exchange": "CZCE", - "margin_rate": 0.07, + "margin_rate": 0.05, "symbol_size": 5, "price_tick": 2.0 }, @@ -436,7 +436,7 @@ "mi_symbol": "SM005", "full_symbol": "SM2005", "exchange": "CZCE", - "margin_rate": 0.07, + "margin_rate": 0.05, "symbol_size": 5, "price_tick": 2.0 }, @@ -445,7 +445,7 @@ "mi_symbol": "sn2006", "full_symbol": "SN2006", "exchange": "SHFE", - "margin_rate": 0.08, + "margin_rate": 0.09, "symbol_size": 1, "price_tick": 10.0 }, @@ -490,7 +490,7 @@ "mi_symbol": "TA005", "full_symbol": "TA2005", "exchange": "CZCE", - "margin_rate": 0.06, + "margin_rate": 0.05, "symbol_size": 5, "price_tick": 2.0 }, @@ -535,7 +535,7 @@ "mi_symbol": "WH011", "full_symbol": "WH2011", "exchange": "CZCE", - "margin_rate": 0.07, + "margin_rate": 0.05, "symbol_size": 20, "price_tick": 1.0 }, @@ -544,7 +544,7 @@ "mi_symbol": "wr2012", "full_symbol": "WR2012", "exchange": "SHFE", - "margin_rate": 0.08, + "margin_rate": 0.2, "symbol_size": 10, "price_tick": 1.0 }, @@ -562,7 +562,7 @@ "mi_symbol": "ZC005", "full_symbol": "ZC2005", "exchange": "CZCE", - "margin_rate": 0.06, + "margin_rate": 0.05, "symbol_size": 100, "price_tick": 0.2 }, @@ -571,7 +571,7 @@ "mi_symbol": "zn2003", "full_symbol": "ZN2003", "exchange": "SHFE", - "margin_rate": 0.07, + "margin_rate": 0.1, "symbol_size": 5, "price_tick": 5.0 } diff --git a/vnpy/gateway/ctp/ctp_gateway.py b/vnpy/gateway/ctp/ctp_gateway.py index 01f24d37..218b65f6 100644 --- a/vnpy/gateway/ctp/ctp_gateway.py +++ b/vnpy/gateway/ctp/ctp_gateway.py @@ -772,11 +772,19 @@ class CtpTdApi(TdApi): account = AccountData( accountid=data["AccountID"], + pre_balance=data['PreBalance'], balance=data["Balance"], frozen=data["FrozenMargin"] + data["FrozenCash"] + data["FrozenCommission"], gateway_name=self.gateway_name ) account.available = data["Available"] + account.commission = data['Commission'] + account.margin = data['CurrMargin'] + 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] self.gateway.on_account(account) diff --git a/vnpy/trader/object.py b/vnpy/trader/object.py index b10dedeb..04b2943c 100644 --- a/vnpy/trader/object.py +++ b/vnpy/trader/object.py @@ -220,9 +220,15 @@ class AccountData(BaseData): """ accountid: str - - balance: float = 0 - frozen: float = 0 + pre_balance: float = 0 # 昨净值 + balance: float = 0 # 当前净值 + frozen: float = 0 # 冻结资金 + currency: str = "" # 币种 + commission: float = 0 # 手续费 + margin: float = 0 # 使用保证金 + close_profit: float = 0 # 平仓盈亏 + holding_profit: float = 0 # 持仓盈亏 + trading_day: str = "" # 当前交易日 def __post_init__(self): """""" diff --git a/vnpy/trader/ui/widget.py b/vnpy/trader/ui/widget.py index c5322c4e..f88ed165 100644 --- a/vnpy/trader/ui/widget.py +++ b/vnpy/trader/ui/widget.py @@ -480,9 +480,14 @@ class AccountMonitor(BaseMonitor): headers = { "accountid": {"display": "账号", "cell": BaseCell, "update": False}, - "balance": {"display": "余额", "cell": BaseCell, "update": True}, + "pre_balance": {"display": "昨净值", "cell": BaseCell, "update": False}, + "balance": {"display": "净值", "cell": BaseCell, "update": True}, "frozen": {"display": "冻结", "cell": BaseCell, "update": True}, + "margin": {"display": "保证金", "cell": BaseCell, "update": True}, "available": {"display": "可用", "cell": BaseCell, "update": True}, + "commission": {"display": "手续费", "cell": BaseCell, "update": True}, + "close_profit": {"display": "平仓收益", "cell": BaseCell, "update": True}, + "holding_profit": {"display": "持仓收益", "cell": BaseCell, "update": True}, "gateway_name": {"display": "接口", "cell": BaseCell, "update": False}, }