通过前面几节的实践,我们已经对股票数据的获取,pandasData数据的格式处理,到bactrader的DATAS结构以及里面的data的数据结构,以及在init和next函数中如何读取和计算都有了比较清晰的认知。我们通过一个最简的回测系统,验证了添加2个策略,添加多个股票data,以及将1个策略同时应用于多个股票的功能,也封装了运行函数从而实现多策略和多股票的循环回测。
接着,我们需要仔细看一下Strategy这个策略类。
01_策略类(Strategy)
001_从最简回测系统开始
还是先运行一下数据准备的代码,在notebook的workspace里获取5支股票的数据。
# Notebook 数据准备
import akshare as ak
import pandas as pddef get_df_from_stock(code1):stock_df = ak.stock_zh_a_hist(symbol=code1, period="daily", adjust="qfq")stock_df.rename(columns={'日期':'date', '开盘':'open', '收盘':'close','最高':'high', '最低':'low', '成交量':'volume',},inplace=True)stock_df.index = pd.to_datetime(stock_df.date)stock_df['openinterest'] = 0stock_df = stock_df[['open','high','low','close','volume','openinterest']]return stock_dfmyStockList = ['001287','002179','600860','300233','002774']
df_stock_list = []for x in myStockList:df_tmp = get_df_from_stock(x)df_stock_list.append(df_tmp)
然后是我们的最简回测代码,这个是确认可以运行成功并绘制出图像的,只不过它不产出任何的文本信息,开始和结束的资产打印是run_main_plot函数在cerebro.run()前后通过语句print(cerebro.broker.getvalue())出来的,与SmaCross_s1这个类没有关系。
from datetime import datetime
import backtrader as btclass SmaCross_s1(bt.Strategy): # 双均线策略def __init__(self):self.fast_ma = bt.indicators.SMA(self.data.close, period=5)self.slow_ma = bt.indicators.SMA(self.data.close, period=20)self.crossover = bt.indicators.CrossOver(self.fast_ma, self.slow_ma)def next(self):if not self.position:if self.crossover > 0:self.buy() else:if self.crossover < 0:self.close()run_main_plot_01(SmaCross_s1,df_stock_list[2], myStockList[2])
002_Strategy类进阶
从最简策略看到,只要有__init__()和next()函数,就可以进行策略的回测,就会有交易发生,就可以有图像输出,但Strategy类可不止这么一点东西。
比如说买了什么,买了多少,订单怎么被执行的,一买一卖交易后盈亏多少,回测什么时候开始的,开始前做了哪些预备工作,什么时候结束的,结束时可以做哪些操作等。
点击进入源码,查看outlook,可以看到Strategy下的所有函数:
我们可以先忽略掉下滑线开头的(内部)函数,以及一系列的getxxx函数,然后再看一下源码,发现里面有一些虚函数等着用户去重写,例如
def start(self):'''Called right before the backtesting is about to be started.'''passdef stop(self):'''Called right before the backtesting is about to be stopped'''passdef notify_cashvalue(self, cash, value):'''Receives the current fund value, value status of the strategy's broker'''passdef notify_fund(self, cash, value, fundvalue, shares):'''Receives the current cash, value, fundvalue and fund shares'''passdef notify_order(self, order):'''Receives an order whenever there has been a change in one'''passdef notify_trade(self, trade):'''Receives a trade whenever there has been a change in one'''passdef notify_store(self, msg, *args, **kwargs):'''Receives a notification from a store provider'''pass
接着,我们再通过AI助手“请提供一个backtrader的典型strategy类,包括一般所需要的函数重写”得到一个比较全面的程序代码,再进行分类:
(1)params:全局参数,可选,用于更改交易策略中变量/参数的值,可用于参数调优。
(2)log:日志,可选,用于记录策略的执行日志。
(3)init:用于初始化交易策略,在其中声明的指标会在next()方法调用之前进行计算。
(4)notify_order,可选,用于跟踪交易订单(order)的状态。
(5)notify_trade,可选,用于跟踪交易的状态,已平仓的交易将报告毛利和净利润。
(6)next,必选,用于制定交易策略的函数,策略模块最核心的部分
(7)stop, 可选,用于回测结束后输出结果
A_参数params与log
参数params
一般情况而言,这里的参数主要是给指标使用的,比如前面的双均线策略,就有2个参数,可以把它们放到Params里,然后在init中给indicators的参数使用。
class SmaCross_s1(bt.Strategy):params = (('fast', 5),('slow', 10),)def __init__(self):self.fast_ma = bt.indicators.SMA(self.data.close, period=self.params.fast)self.slow_ma = bt.indicators.SMA(self.data.close, period=self.params.slow)
这种方式跟股票软件公式里的参数设置功能类似,比如下面这个KDJ的公式,就放了三个参数N,M1,M2,这样在使用时,用户可以在不更改公式的情况下,直接设置参数更改。
而在backtrader里,设置了参数后还可以进行参数优化,也就是对多种参数的组合回测。
##### 参数及默认值设置
params = (('myparam', 20),
)################# 参数优化时使用 optstrategy
cerebro.optstrategy(TestOpt1, myparam=range(15,30) )
一般情况下参数的名称设置要根据指标来,比如上面的双均线,参数名就是“fast”和"slow",又比如MACD指标的参数,就可以设置为period_me1,period_me2,period_signal,对于内置的指标indicators,可以直接从官网的indicators-Reference而去查找到对应的例如MACD下的Params。
除了指标的参数以外,由于params是全局的,我们就可以利用它来记录或计算一些东西,比如说将来我们会有很多的回测策略,也很有可能连续对多个策略多支股票进行循环回测,那么就需要在最后输出列表中显示出每一次的股票名称以及策略名称,这样的情况下,我们可以直接给所有的策略都添加一个strategy_name的参数。
class SmaCross_s1(bt.Strategy):params = (('strategy_name','双均线策略'), # 用参数记录策略的name('fast', 5),('slow', 10),)
记录log
这里AI提供的示例代码我们可以直接用,都不需要修改。
class MyStrategy(bt.Strategy):params = (('maperiod', 15),('printlog', False),)def log(self, txt, dt=None, doprint=False):''' Logging function for this strategy'''if self.params.printlog or doprint:dt = dt or self.datas[0].datetime.date(0)print(f'{dt.isoformat()}, {txt}')
代码中设置了一个printlog的参数,用于双重控制是否打印,另一重控制在log函数的参数doprint中。对于文本输出,有些时候我们很需要它,比如要看每笔成交的价格,手续费多少,每次交易的收益等,但有些时候用不到它,如果进行了10支股票的5个策略的回测,有50组结果,这个时候我们需要的是一张统计表,而不需要每笔详单。因此,就需要对log进行参数设置,可以打印也可以在不需要的时候不打印。
根据log的代码,可知它其实就是把需要显示的字符串打印出来,在字符串前面加上了发生的日期,也就是datetime.date(),大多数情况下,使用默认的dt=None就可以了,但在两个策略或多个股票data的情况下,注意log的时候要给出dt=xxx,否则可能报错。
B_订单notify_order
Order订单模块和Broker经纪商模块是交易相关的核心模块。
- Cerebro是Backtrader的关键控制中心,是大脑。
- Strategy是大脑的神经,基于数据和分析做出最终的决策。
- Order 订单将Strategy的做出的决策转换为由券商(Broker)执行操作的消息。
在Strategy类中,通过三种方式进行订单Order的操作,前面类的成员方法截图上也已经看到过
- 创建订单 - 函数主要有 3 个:买入 buy() 、卖出 sell()、平仓 close()
- 取消订单 - 在strategy中的为cancel()
- 通知 - Strategy中定义notify_order函数用于跟踪订单的状态信息
关于订单状态有以下这些:
- Order.Created:订单已被创建;
- Order.Submitted:订单已被传递给经纪商 Broker;
- Order.Accepted:订单已被经纪商接收;
- Order.Partial:订单已被部分成交;
- Order.Complete:订单已成交;
- Order.Rejected:订单已被经纪商拒绝;
- Order.Margin:执行该订单需要追加保证金,并且先前接受的订单已从系统中删除;
- Order.Cancelled (or Order.Canceled):确认订单已经被撤销;
- Order.Expired:订单已到期,其已经从系统中删除 。
看到这里,我们是否发现这个与真实的股票/期货交易是一致的?
以股票交易为例,感觉要上涨了(看多)进行买入(买多)操作,先点击下单按钮,再点击买按钮,然后输入价格,输入数量(A股最小买入单位是手=100股),最后点击红色的买入按钮(Submitted),这个订单就开始生成,但如果你买入的时候资金不足什么的,这个订单就创建失败(Rejected),也就是被拒绝了,你需要重新填价格或数量。
接着,订单再次提交没问题就会被券商接受(Accepted),此时就可以在“当日委托”以及“撤单”页面上看到这个订单。在没有成交之前,我们是可以撤单的(Canceled),而能否成交还要看价格到没到,价格到了还要看你排在什么位置,很多时候我们算准了最低价,但这个价位成交量很低是成交不了的。如果成交了(Completed),订单就算完成了。如果没有成交,当天收盘后这些订单会被自动取消(Expired)。其中Margin是期货这类保证金交易特有的状态,股票交易应该用不到。
为此,我们在AI提供的代码上,加了一句self.log('order.status %s'%order.status在每次进入的时候,在submitted/accepted 以及completed里同样加了显示status的语句。
由输出结果可知,每一个成功的buy/sell都要进到notify_order中来3次,第1次是status1-submit,第2次是status2 - Accept,第3次才是status 4- Completed。并且backtrader的默认流程是以当天收盘价下单,但是以第二日的开盘价进行的成交。这个详细的放到交易设置的set_coc里再做介绍。
因此,如果有一个券商接口,也就可以实现自动交易。不过现阶段先不要着急那些东西,先看看在Notify_order中需要打印哪些东西?对于submitted/accepted而言,真实的交易是会收到弹窗提示的,但没有实质性的数据在这里,所以只需要在Completed的状态里进行一些数据的打印。
from datetime import datetime
import backtrader as btclass SmaCross_s3(bt.Strategy):params = (('fast', 5),('slow', 10),('printlog',True),)def log(self, txt, dt=None, doprint=True):''' Logging function for this strategy'''if self.params.printlog or doprint:dt = dt or self.datas[0].datetime.date(0)print(f'{dt.isoformat()}, {txt}')def __init__(self):self.fast_ma = bt.indicators.SMA(self.data.close, period=self.params.fast)self.slow_ma = bt.indicators.SMA(self.data.close, period=self.params.slow)self.crossover = bt.indicators.CrossOver(self.fast_ma, self.slow_ma)def stop(self):print(self.getposition())def next(self):if not self.position:if self.crossover > 0:self.buy() else:if self.crossover < 0:self.sell()def notify_order(self, order): #记录交易执行情况############################################################ 如果order为submitted/accepted,返回空if order.status in [order.Submitted, order.Accepted]:# Buy/Sell order submitted/accepted to/by broker - Nothing to doreturn# Check if an order has been completed # 指令为buy/sell,报告价格结果# Attention: broker could reject order if not enough cashif order.status in [order.Completed]: # 正常能够完成的定单if order.isbuy():self.log('BUY , Price: %.2f, Cost: %.2f, Comm :%.2f, number :%s ,size :%.2f, %s' %(order.executed.price, #成交价order.executed.value, #成交额order.executed.comm, #佣金order.ref, #订单编号order.executed.size, # 成交量order.data._name)) # 股票名称self.buyprice = order.executed.priceself.buycomm = order.executed.commelse: # Sellself.log('SELL , Price: %.2f, Cost: %.2f, Comm %.2f, number :%s ,size :%.2f, %s' %(order.executed.price, #成交价order.executed.value, #成交额order.executed.comm, #佣金order.ref, #订单编号order.executed.size, # 成交量order.data._name)) # 股票名称self.bar_executed = len(self) # 从执行那根stick开始计数elif order.status in [order.Canceled, order.Margin, order.Rejected]: #钱不够,买/卖超限等会被终止,拒绝等self.log('Order Canceled/Margin/Rejected')self.order = Nonerun_main_plot_01(SmaCross_s3,df_stock_list[2], myStockList[2])-----------------------
Starting Portfolio Value: 100000.00
2024-02-22, BUY , Price: 9.20, Cost: 49891.54, Comm :49.89, number :92 ,size :5422.99, 600860
2024-03-11, SELL , Price: 11.18, Cost: 49891.54, Comm 60.63, number :93 ,size :-5422.99, 600860
2024-03-21, BUY , Price: 11.70, Cost: 54705.66, Comm :54.71, number :94 ,size :4675.70, 600860
2024-03-28, SELL , Price: 10.50, Cost: 54705.66, Comm 49.09, number :95 ,size :-4675.70, 600860
2024-04-10, BUY , Price: 10.53, Cost: 52307.16, Comm :52.31, number :96 ,size :4967.44, 600860
2024-04-11, SELL , Price: 10.06, Cost: 52307.16, Comm 49.97, number :97 ,size :-4967.44, 600860
2024-04-29, BUY , Price: 10.50, Cost: 55982.91, Comm :55.98, number :98 ,size :5331.71, 600860
Final Portfolio Value: 99327.02
在这里我们可以打印价格,费用,佣金,订单号,股票数量,股票名称等。从数据看出,订单号当前都是连号的数字,股票数量还有小数是不符合A股实际情况的,后续需要更改,cost似乎只记录买单的值,而卖单的cost与对应的买单是一样的,或许这里需要使用另外的参数而不是executed.value吧,如果把value翻译为价值,那么同样数量的股票价值相等似乎也说得过去。另外,佣金有点贵了,千分之一啊!
C_交易notify_trade
notify_trade是交易通知,它要比notify_order要简单很多,于是我们还是直接去源码\backtrader\trade.py,把它的代码贴给AI助手进行解析:
这段代码定义了一个名为
Trade
的类,用于跟踪一笔交易的生命周期,包括交易的规模、价格、佣金以及价值等方面的信息。基本属性
ref
:唯一的交易标识符。status
:交易的状态,可以是创建(Created)、打开(Open)、关闭(Closed)。tradeid
:用于标识交易组的 ID,默认为 0。size
:当前交易的规模(正数表示多头,负数表示空头)。price
:当前交易的价格。value
:当前交易的价值。commission
:累计的佣金。pnl
:当前的利润与亏损(毛利润)。pnlcomm
:扣除佣金后的利润与亏损(净收益)。交易状态标志
isclosed
:布尔值,记录最后的更新是否关闭了交易(即规模归零)。isopen
:布尔值,记录任何更新是否打开了交易。justopened
:布尔值,如果交易刚刚打开。时间相关属性
baropen
:交易打开所在的 K 线编号。dtopen
:交易打开时的日期时间(浮点数编码,可以转换为 Python 的datetime
对象)。barclose
:交易关闭所在的 K 线编号。dtclose
:交易关闭时的日期时间(浮点数编码,可以转换为 Python 的datetime
对象)。其他属性
barlen
:交易保持打开状态的 K 线数量。historyon
:布尔值,指示是否记录历史。history
:列表,记录每次更新事件的结果状态和参数。列表的第一个条目是打开事件,最后一个条目是关闭事件。
所以对于trade来说,是开仓后需要平仓才算结束isclosed,它会记录每笔交易的price, size, value, 佣金,因此可以得到pnl毛利润和pnlcomm净利润,同时要注意它的状态是open还是close,因为交易的利润需要closed的才能计算,open的计算的是浮盈或者浮亏,是不确定的。
在notify_trade中,我们可以调用trade类的方法或成员查看交易信息,从下面的输出结果看到,开仓并平仓的会记为1次交易trade,price,comm,pnl,pnlcomm等都有记录,且有开仓的日期以及是总数据中的第几天等;而最后一笔买入没有对应的卖出,不输出trade信息(在程序中return了)。
def notify_trade(self, trade):if not trade.isclosed:returnself.log(f'OPERATION PROFIT, GROSS {trade.pnl:.2f}, NET {trade.pnlcomm:.2f}')print(trade.__str__())----------------------------
2024-02-22, BUY EXECUTED, Price: 9.20, Cost: 49891.54, Comm :49.89, number :115 ,size :5422.99, 600860
2024-03-11, SELL EXECUTED, Price: 11.18, Cost: 49891.54, Comm 60.63, number :116 ,size :-5422.99, 600860
2024-03-11, OPERATION PROFIT, GROSS 10737.53, NET 10627.01 # self.log()的内容ref:67
data:<backtrader.feeds.pandafeed.PandasData object at 0x000002D141D63A50>
tradeid:0
size:0.0
price:9.2 # price
value:0.0
commission:110.52060737527114 # comm
pnl:10737.527114967465 # pnl
pnlcomm:10627.006507592194 # pnlcomm
justopened:False
isopen:False # isopen
isclosed:True # isclosed
baropen:23
dtopen:738938.0 # 开仓日期
barclose:35
dtclose:738956.0 # 平仓日期
barlen:12
historyon:False
history:[]
status:2............
2024-04-29, BUY EXECUTED, Price: 10.50, Cost: 55982.91, Comm :55.98, number :121 ,size :5331.71, 600860
Final Portfolio Value: 99327.02
如果我们对于not trade.isclosed的情况也进行输出,那么输出的结果是这样的:
def notify_trade(self, trade):if not trade.isclosed:print(trade.__str__()) # open 交易也打印信息returnself.log(f'OPERATION PROFIT, GROSS {trade.pnl:.2f}, NET {trade.pnlcomm:.2f}') ---------------------2024-04-29, BUY EXECUTED, Price: 10.50, Cost: 55982.91, Comm :55.98, number :142 ,size :5331.71, 600860
ref:82
data:<backtrader.feeds.pandafeed.PandasData object at 0x000002D1421E43D0>
tradeid:0
size:5331.706119018024
price:10.5
value:55982.91424968925
commission:55.98291424968926
pnl:0.0
pnlcomm:-55.98291424968926
justopened:True
isopen:True # isopen = True
isclosed:False
baropen:68
dtopen:739005.0
barclose:0
dtclose:0.0
barlen:0
historyon:False
history:[]
status:1 # open的trade status为1
Final Portfolio Value: 99327.02
接着,如果在后面的notify_trade中,我们想要在每笔交易完成的时候,打印出参数的配置和收益(其实这些在添加了评价后result里都有,但是参数优化的评价数据类型非常的奇怪,当前的进度还没有到Analyzer评价的部分,因此我们通过参数params,trade以及stop()来进行简单的计算和评价,不论是单个回测还是参数优化都可以得到参数配置和收益。甚至,我们还可以把最后买入但没有卖出的跟当前价格进行比较来计算浮动盈亏,并把浮盈的也暂时计算入成功率。
D_开始与结束start_stop
start()的功能是在回测开始前进行一些工作,一般不使用,stop()是在整个回测结束的时候进行的一些工作。由于在一次回测里可能有很多买入卖出的order和trade,但结束的时候我们希望记录它们的累计收益,成功率,盈亏比等,就需要灵活运用stop()了。
首先,在params里创建几个用于记录交易次数和成功次数的参数
class SmaCross_s4(bt.Strategy):params = (('stra_name','双均线策略s4_打印简单评价'),('fast', 5),('slow', 10),('printlog',False),('tradeCnt',0),('sucessCnt',0),)
其中tradeCnt是交易次数计数,只把closed的交易进行计数,同时在交易closed时计算pnlcomm是否大于0来判断这次的交易是否成功,成功则把sucessCnt计数增加。
def notify_trade(self, trade):self.mytrade = tradeif not trade.isclosed:returnself.params.tradeCnt += 1if trade.pnlcomm > 0:self.params.sucessCnt +=1self.log(f'OPERATION PROFIT, 毛利润 {trade.pnl:.2f}, 净利润 {trade.pnlcomm:.2f}')
最后到stop()函数中来,通过self.mytrade 把最后notify_trade的数据获取到,通过isopen是否为True先确认到目前为止是否有未平仓的交易,计算浮动盈亏,最后把浮动盈亏也计入交易成功率计算。
def stop(self):trade = self.mytradesucessPct = self.params.sucessCnt/self.params.tradeCnt * 100.0if trade.isopen == True: # 有开仓计算浮盈亏winlose_stat = '浮盈' if trade.price<= self.data.close[0] else '浮亏'winlose_val = (self.data.close[0]-trade.price)*trade.sizewinlose_suc_pct = (self.params.sucessCnt+1)/(self.params.tradeCnt+1) if winlose_val >0 \else self.params.sucessCnt/(self.params.tradeCnt+1)self.log("%s,参数(%2d,%2d,%2d), 期末总资金 %.2f 盈利为 %.2f" % (self.params.stra_name,self.params.fast,self.params.slow,0,self.broker.getvalue(),self.broker.getvalue()-100000))self.log("总共交易次数为 %2d ,交易成功率为 %.1f%%," % (self.params.tradeCnt,sucessPct)) self.log("当前开仓状态,%s %.2f,浮动交易成功率为%.1f%%"%(winlose_stat, winlose_val,winlose_suc_pct*100))else: # 已平仓直接输出sucessPct = self.params.sucessCnt/self.params.tradeCntself.log("%s,参数(%2d,%2d,%2d), 期末总资金 %.2f 盈利为 %.2f" % (self.params.stra_name,self.params.fast,self.params.slow,0,self.broker.getvalue(),self.broker.getvalue()-100000))self.log("总共交易次数为 %2d ,交易成功率为 %.1f%%," % (self.params.tradeCnt,sucessPct))
然后运行回测就能得到一个简单的评价结果,例如
run_main_plot_01(SmaCross_s4,df_stock_list[1], myStockList[1])----------------------------
Starting Portfolio Value: 100000.00
2024-01-30, 买入, No.002179, Price: 31.28, Cost: 49337.54, Comm :49.34,size :1577.29
2024-02-02, 卖出, Price: 30.85, Comm 48.66
2024-02-02, OPERATION PROFIT, 毛利润 -678.23, 净利润 -776.232024-02-19, 买入, No.002179, Price: 34.60, Cost: 49958.42, Comm :49.96,size :1443.88
2024-02-29, 卖出, Price: 32.80, Comm 47.36
2024-02-29, OPERATION PROFIT, 毛利润 -2598.99, 净利润 -2696.312024-03-05, 买入, No.002179, Price: 33.51, Cost: 47977.38, Comm :47.98,size :1431.73
2024-03-28, 卖出, Price: 33.17, Comm 47.49
2024-03-28, OPERATION PROFIT, 毛利润 -486.79, 净利润 -582.262024-04-22, 买入, No.002179, Price: 32.30, Cost: 48271.50, Comm :48.27,size :1494.472024-05-10, 双均线策略s4_打印简单评价,参数( 5,10, 0), 期末总资金 100798.80 盈利为 798.80
2024-05-10, 总共交易次数为 3 ,交易成功率为 0.0%,
2024-05-10, 当前开仓状态,浮盈 4901.87,浮动交易成功率为25.0%
Final Portfolio Value: 100798.80
从输出结果上看,一共有3组买入卖出,第4次只有买入,最后打印了策略名称,参数配置和交易次数及成功率等,由于最后1笔只有买入,其trade.isopen为True,于是计算得到买入到当前是浮盈4901块,从而总交易次数为4,成功1次,得到浮动交易成功率为25%。
这里发现了一个问题,也就是如果每个策略用到不同的指标,其参数名会不一样,那么每一个策略的stop()里的最后self.log()中,参数名就会变化,就需要重新写。而我们想偷懒,准备是除了init和next之外全部不会改动,于是基于这个我们把指标参数的设置改为了和股票软件类似的表示方法(N,M1,M2...)这样,指标参数就都定义为p1,p2,p3,p4,一般3个参数足够用了。
003_继承Strategy类与偷懒
前面说到我们想偷懒,准备除了init和next这两个关键函数外,其他的都不改动代码。所以可以简单的用继承和重写来解决。
我们只需要写一个类BaseSt,继承自bt.Strategy,在里面把log, notify_order, notify_trade, stop等其他函数完成重写即可。
然后在新创建策略的时候,让它继承自BaseSt,这样就只需要把聚焦于init和next这些核心就可以了。例如就拿上面的SmaCross_s4稍做一些修改做继承,在mystrategy5的类里,就不需要再写额外的代码,只需要params上稍做修改,init和next里根据指标策略需求制作即可:
class mystrategy5(SmaCross_s4):params = (('stra_name','布林经典策略'),('p1', 20),('p2', 2),('printlog',False),('tradeCnt',0),('sucessCnt',0),)def __init__(self):self.order = NoneBOLL1 = bt.indicators.BBands(self.data,period =self.p.p1, devfactor=self.p.p2)bot = BOLL1.l.bottop = BOLL1.l.topself.crsup = bt.indicators.CrossUp(self.data.close,bot)self.crsdn = bt.indicators.CrossDown(top,self.data.close)def next(self):if not self.position:if self.crsup > 0:self.buy() else:if self.crsdn > 0:self.sell()run_main_plot_01(mystrategy5,df_stock_list[1], myStockList[1])------------------------
Starting Portfolio Value: 100000.00
2022-03-22, 买入, No.002179, Price: 44.52, Cost: 50000.00, Comm :50.00,size :1123.09
2022-06-06, 卖出, Price: 46.42, Comm 52.13
2022-06-06, OPERATION PROFIT, 毛利润 2133.87, 净利润 2031.742022-08-02, 买入, No.002179, Price: 47.08, Cost: 51299.17, Comm :51.30,size :1089.62
2022-09-15, 卖出, Price: 50.28, Comm 54.79
2022-09-15, OPERATION PROFIT, 毛利润 3486.77, 净利润 3380.692022-10-12, 买入, No.002179, Price: 46.18, Cost: 52366.03, Comm :52.37,size :1133.95
2023-06-30, 卖出, Price: 43.75, Comm 49.61
2023-06-30, OPERATION PROFIT, 毛利润 -2755.51, 净利润 -2857.492023-08-02, 买入, No.002179, Price: 42.76, Cost: 51313.47, Comm :51.31,size :1200.03
2023-08-25, 卖出, Price: 43.38, Comm 52.06
2023-08-25, OPERATION PROFIT, 毛利润 744.02, 净利润 640.652023-09-25, 买入, No.002179, Price: 41.90, Cost: 51597.80, Comm :51.60,size :1231.45
2024-02-19, 卖出, Price: 34.60, Comm 42.61
2024-02-19, OPERATION PROFIT, 毛利润 -8989.59, 净利润 -9083.802024-04-16, 买入, No.002179, Price: 32.29, Cost: 47085.06, Comm :47.09,size :1458.19
2024-05-08, 卖出, Price: 34.72, Comm 50.63
2024-05-08, OPERATION PROFIT, 毛利润 3543.41, 净利润 3445.702024-07-10, 买入, No.002179, Price: 36.12, Cost: 48684.40, Comm :48.68,size :1347.852024-09-11, 布林经典策略,参数(20, 2, 0), 期末总资金 100096.68 盈利为 96.68
2024-09-11, 总共交易次数为 6 ,交易成功率为 66.7%,
2024-09-11, 当前开仓状态,浮盈 2587.87,浮动交易成功率为71.4%
Final Portfolio Value: 100096.68
由输出结果可以看到,在这个类里的代码量非常少,但仍然实现了前面的简单评价的功能,这里6组有买入和卖出的交易,1笔只有买入的交易,持仓至今浮盈2587块,浮动交易成功率为71.4%。
如果在参数优化里,由于参数组合比较多而不希望打印出order和trade的详单,则我们可以再做一个baseOptStrategy的类,在其中把notify_order和notify_trade里的log去掉,然后创建策略的时候再继承自这个baseOptStrategy就可以了。看起来我们的偷懒是成功的。