简单策略回测详解

简单策略回测详解

这篇文章介绍了QA回测的一个示例。

QA回测的核心是两个类

  1. QA_BacktestBroker
  2. QA_Account

回测数据的引入/迭代

  1. QA.QA_fetch_stock_day_adv
  2. QA.QA_fetch_stock_min_adv

指标的计算

DataStruct.add_func

对于账户的灵活运用

QA_Account
QA_Risk
QA_Portfolio
QA_PortfolioView
QA_User

Step1 初始化账户,初始化回测broker

Python
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
import QUANTAXIS as QA
Account = QA.QA_Account()
Broker = QA.QA_BacktestBroker()

# 打印账户的信息
try:
from pprint import pprint as print
except:
pass
print(Account.message)

# 结果⬇️
{'account_cookie': 'Acc_bOh7r5ic',
'allow_sellopen': False,
'allow_t0': False,
'broker': 'backtest',
'cash': [1000000],
'commission_coeff': 0.00025,
'current_time': 'None',
'history': [],
'init_assets': {'cash': 1000000, 'hold': {}},
'margin_level': False,
'market_type': 'stock_cn',
'portfolio_cookie': None,
'quantaxis_version': '1.1.3.dev4',
'running_environment': 'backtest',
'running_time': '2018-09-30 10:29:53.562806',
'source': 'account',
'strategy_name': None,
'tax_coeff': 0.001,
'trade_index': [],
'user_cookie': None}

Account类

QA_Account在初始化的时候,可以自己指定很多信息:

Python
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
QA_Account(
strategy_name = None,
user_cookei = None,
market_type = MARKET_TYPE.STOCK_CN,
frequency = FREQUENCY.DAY,
broker = BROKER_TYPE.BACKTEST,
portfolio_cookie = None,
account_cookie = None,
sell_available = {},
init_assets = 1000000,
cash = None, # 默认为初始资产
history = None,
commission_coeff = 0.00025,
tax_coeff = 0.0015,
margin_level = False,
allow_to = False,
allow_sellopen = False
)

上面参数的含义:

  • 策略名称:strategy_name = None,
  • 用户cookie: user_cookei = None,
  • 市场类别:market_type = MARKET_TYPE.STOCK_CN, 默认A股
  • 账户级别:frequency = FREQUENCY.DAY, 默认日线
  • BROKER类:broker = BROKER_TYPE.BACKTEST, 默认回测
  • 组合cookie:portfolio_cookie = None,
  • 账户cookie:account_cookie = None,
  • 可卖股票数:sell_available = {},
  • 初始资产:init_assets = 1000000,
  • 可以现金cash = None, # 默认为初始资产
  • 交易历史:history = None,
  • 交易佣金:commission_coeff = 0.00025,
  • 印花税率:tax_coeff = 0.0015,
  • 保证金比例:margin_level = False,
  • 是否运行T+0交易:allow_to = False,
  • 是否允许卖空开仓:allow_sellopen = False

重设账户初始资金:

Python
1
2
3
4
5
Account.reset_assets(200000)
Account.account_cookie = 'JCSC_EXAMPLE'
Account.init_assets

Out[8]: {'cash': 200000, 'hold': {}}

Step2:引入回测的市场数据

引入的方法非常简答,直接使用QA_fetch_stock_adv系列即可:

  1. code可以是多种多样的选取方式:

    Python
    1
    2
    3
    4
    5
    6
    7
    8
    9
    codelist = QA.QA_fetch_stock_list_adv().code.tolist()
    len(codelist)

    Out[14]: 3556

    blocklist = QA.QA_fetch_stock_block_adv().get_block('云计算').code
    len(blocklist)

    Out[16]: 111
  2. 数据获取后,to_qfq()即可获取前复权数据

    Python
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    data = QA.QA_fetch_stock_day_adv(
    ['000001', '000002', '000004', '600000'],
    '2017-09-01', '2018-05-20')

    data
    Out[21]: < QA_DataStruct_Stock_day with 4 securities >

    data.data.head()
    Out[20]:
    open high ... volume amount
    date code ...
    2017-09-01 000001 11.28 11.39 ... 959976.0 1.080996e+09
    000002 23.26 23.37 ... 407080.0 9.337101e+08
    000004 23.16 23.26 ... 10381.0 2.394311e+07
    600000 12.68 12.87 ... 392641.0 5.017964e+08
    2017-09-04 000001 11.18 11.72 ... 1352325.0 1.551875e+09
    [5 rows x 6 columns]

Step3: 计算一些指标

指标的计算可以在回测前,也可以在回测中进行;
回测前的计算这是批量计算,效率较高;
回测中的计算,效率略低,但代码量较小,易于理解。

Python
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
import numpy as np
import pandas as pd
import pylab
def MACD_JCSC(DF, SHORT = 12, LONG = 26, M = 9):
"""
1. DIF向上突破DEA是买入信号的参考;
2. DIF向下突破DEA是卖出信号的参考;
3. DIF:差离值,即12日EMA数值减去26日EMA数值;
4. MACD:指数平滑移动平均线,由快的指数移动平均线(EMA12)减去慢的指数移动平均线
(EMA26)得到快线DIF,再用2×(快线DIF-DIF的9日加权移动均线DEA)得到MACD柱。
"""
CLOSE = DF.close
DIFF = QA.EMA(CLOSE, SHORT) - QA.EMA(CLOSE, LONG)
DEA = QA.EMA(DIFF, M)
MACD = 2 * (DIFF - DEA)

CROSS_JC = QA.CROSS(DIFF, DEA)
CROSS_SC = QA.CROSS(DEA, DIFF)
ZERO = 0
return pd.DataFrame({'DIFF': DIFF, 'DEA':DEA, 'MACD': MACD,
'CROSS_JC': CROSS_JC, 'CROSS_SC': CROSS_SC,
'ZERO': ZERO})

ind = data.add_func(MACD_JCSC)

ind.xs('000001', level = 1)['2018-01'].plot()
pylab.savefig("ind.png")

Python
1
2
3
4
5
6
7
8
9
ind.xs('000001', level = 1)['2018-01'].head()
Out[29]:
DIFF DEA MACD CROSS_JC CROSS_SC ZERO
date
2018-01-02 0.110686 0.103225 0.014921 1 0 0
2018-01-03 0.100387 0.102657 -0.004541 0 1 0
2018-01-04 0.084801 0.099086 -0.028570 0 0 0
2018-01-05 0.075607 0.094390 -0.037566 0 0 0
2018-01-08 0.040453 0.083603 -0.086299 0 0 0
Python
1
2
3
4
5
6
7
8
9
10
11
12
13
14
ind.loc['2018-01', slice(None)]
Out[33]:
DIFF DEA MACD CROSS_JC CROSS_SC ZERO
date code
2018-01-02 000001 0.110686 0.103225 0.014921 1 0 0
000002 0.404554 0.189272 0.430564 0 0 0
000004 -0.717479 -0.746157 0.057357 0 0 0
600000 -0.062653 -0.054428 -0.016450 0 0 0
... ... ... ... ... ...
2018-01-31 000001 0.180544 0.250474 -0.139859 0 0 0
000002 1.676000 2.103160 -0.854320 0 0 0
000004 -0.537710 -0.485270 -0.104879 0 1 0
600000 0.140515 0.126357 0.028316 0 0 0
[88 rows x 6 columns]

Step4: 选取回测开始和结束的日期,构建回测

下面的策略是:对每天每只股票进行循环,如果出现金叉就买入1000股。如果出现死叉就全部卖出(似乎是这样的)。

Python
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
data_forbacktest = data.select_time('2018-01-01', '2018-09-01')
for items in data_forbacktest.panel_gen:
for item in items.security_gen:
daily_ind = ind.loc[item.index]
if daily_ind.CROSS_JC.iloc[0] > 0:
order = Account.send_order(
code = item.code[0],
time = item.date[0],
amount = 1000,
towards = QA.ORDER_DIRECTION.BUY,
price = 0,
order_model = QA.ORDER_MODEL.CLOSE,
amount_model = QA.AMOUNT_MODEL.BY_AMOUNT
)
print(item.to_json()[0])
Broker.receive_order(QA.QA_Event(order = order, market_data = item))

trade_mes = Broker.query_orders(Account.account_cookie, 'filled')
res = trade_mes.loc[order.account_cookie, order.realorder_id]
order.trade(res.trade_id, res.trade_price, res.trade_amount, res.trade_time)
elif daily_ind.CROSS_SC.iloc[0] > 0:
if Account.sell_available.get(item.code[0], 0) > 0:
order = Account.send_order(
code = item.code[0],
time = item.date[0],
amount = Account.sell_available.get(item.code[0], 0),
towards = QA.ORDER_DIRECTION.SELL,
price = 0,
order_model = QA.ORDER_MODEL.MARKET,
amount_model = QA.AMOUNT_MODEL.BY_AMOUNT
)
Broker.receive_order(QA.QA_Event(order = order, market_data = item))
trade_mes = Broker.query_orders(Account_cookie, 'filled')
res = trade_mes.loc[order.account_cookie, order.realorder_id]
order.trade(res.trade_id, res.trade_price, res.trade_amount, res.trade_time)

Account.settle()

分析账户

交易历史

Python
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
Account.history
Out[66]:
[['2018-01-02 00:00:00',
'000001',
13.7,
1000,
186295.0,
'Order_tqP6SrVY',
'Order_tqP6SrVY',
'Trade_zqNZnfXK',
'JCSC_EXAMPLE',
5,
0],

······

['2018-04-11 00:00:00',
'600000',
11.91,
1000,
1117.505000000012,
'Order_olw18BgK',
'Order_olw18BgK',
'Trade_lw9ByeRA',
'JCSC_EXAMPLE',
5,
0]]
Python
1
2
3
4
5
6
7
8
9
Account.history_table.head()
Out[68]:
datetime code price ... account_cookie commission tax
0 2018-01-02 00:00:00 000001 13.70 ... JCSC_EXAMPLE 5.00 0
1 2018-01-04 00:00:00 600000 12.66 ... JCSC_EXAMPLE 5.00 0
2 2018-01-12 00:00:00 000001 13.55 ... JCSC_EXAMPLE 5.00 0
3 2018-01-24 00:00:00 000004 22.08 ... JCSC_EXAMPLE 5.52 0
4 2018-02-05 00:00:00 600000 13.49 ... JCSC_EXAMPLE 5.00 0
[5 rows x 11 columns]

每日持仓

Python
1
2
3
4
5
6
7
8
9
10
11
12
13
14
Account.daily_hold.head()
Out[73]:
code 000001 000002 000004 600000
date account_cookie
2018-01-02 JCSC_EXAMPLE 1000.0 0.0 0.0 0.0
2018-01-04 JCSC_EXAMPLE 1000.0 0.0 0.0 1000.0
2018-01-12 JCSC_EXAMPLE 2000.0 0.0 0.0 1000.0
2018-01-24 JCSC_EXAMPLE 2000.0 0.0 1000.0 1000.0
2018-02-05 JCSC_EXAMPLE 2000.0 0.0 1000.0 2000.0

pd.concat([Account.daily_hold.reset_index().set_index('date'),
pd.Series(data=None,index=pd.to_datetime(Account.trade_range).set_names('date'),
name='predrop')],axis=1).ffill().drop(['predrop'],axis=1).reset_index().plot()
pylab.savefig("daily_hold.png")

每日现金持有

Python
1
2
Account.daily_cash.cash.plot()
pylab.savefig("daily_cash.png")

风险分析

Python
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
Risk = QA.QA_Risk(Account)
Risk.message
Out[99]:
{'account_cookie': 'JCSC_EXAMPLE',
'portfolio_cookie': None,
'user_cookie': None,
'annualize_return': -0.11,
'profit': -0.03,
'max_dropback': 0.06,
'time_gap': 65,
'volatility': 0.26,
'benchmark_code': '000300',
'bm_annualizereturn': -0.14,
'bn_profit': -0.03,
'beta': 1.0,
'alpha': 0.03,
'sharpe': -0.62,
'init_cash': '200000.00',
'last_assets': '194127.51',
'total_tax': 0,
'total_commission': -62.5,
'profit_money': -5872.49}
Python
1
2
3
4
5
6
7
8
Risk.market_value.diff().iloc[-1]
Out[101]:
code
000001 1640.0
000002 860.0
000004 0.0
600000 12190.0
Name: (2018-04-11 00:00:00, JCSC_EXAMPLE), dtype: float64
Python
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
Risk.account.cash_table
Out[103]:
cash ... account_cookie
datetime account_cookie ...
2018-01-02 00:00:00 JCSC_EXAMPLE 186295 ... JCSC_EXAMPLE
2018-01-04 00:00:00 JCSC_EXAMPLE 173630 ... JCSC_EXAMPLE
2018-01-12 00:00:00 JCSC_EXAMPLE 160075 ... JCSC_EXAMPLE
2018-01-24 00:00:00 JCSC_EXAMPLE 137989 ... JCSC_EXAMPLE
2018-02-05 00:00:00 JCSC_EXAMPLE 124494 ... JCSC_EXAMPLE
2018-02-13 00:00:00 JCSC_EXAMPLE 104389 ... JCSC_EXAMPLE
2018-03-08 00:00:00 JCSC_EXAMPLE 92274.5 ... JCSC_EXAMPLE
JCSC_EXAMPLE 58626 ... JCSC_EXAMPLE
2018-03-29 00:00:00 JCSC_EXAMPLE 24457.5 ... JCSC_EXAMPLE
2018-04-10 00:00:00 JCSC_EXAMPLE 13032.5 ... JCSC_EXAMPLE
2018-04-11 00:00:00 JCSC_EXAMPLE 1117.51 ... JCSC_EXAMPLE
[11 rows x 4 columns]

现金变化表的处理

Python
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
z = Account.cash_table.drop_duplicates(subset='date', keep = 'last').set_index(['date', 'account_cookie'], drop = False)
z.head()
Out[94]:
cash ... account_cookie
date account_cookie ...
2018-02-26 JCSC_EXAMPLE 165811 ... JCSC_EXAMPLE
2018-03-06 JCSC_EXAMPLE 132133 ... JCSC_EXAMPLE
2018-03-07 JCSC_EXAMPLE 120078 ... JCSC_EXAMPLE
2018-03-13 JCSC_EXAMPLE 107583 ... JCSC_EXAMPLE
2018-03-29 JCSC_EXAMPLE 73414.5 ... JCSC_EXAMPLE
[5 rows x 4 columns]
res = Account.cash_table.drop_duplicates(subset = 'date', keep = 'last')
zx = pd.concat([res.set_index('date'),
pd.Series(data = None,
index = pd.to_datetime(Account.trade_range),
name = 'predrop')],
axis = 1).ffill().drop(['predrop'], axis = 1)
zxx = pd.to_datetime(Account.trade_range)
zxx.set_names('date')
Out[98]:
DatetimeIndex(['2018-02-26', '2018-02-27', '2018-02-28', '2018-03-01',
'2018-03-02', '2018-03-05', '2018-03-06', '2018-03-07',
'2018-03-08', '2018-03-09', '2018-03-12', '2018-03-13',
'2018-03-14', '2018-03-15', '2018-03-16', '2018-03-19',
'2018-03-20', '2018-03-21', '2018-03-22', '2018-03-23',
'2018-03-26', '2018-03-27', '2018-03-28', '2018-03-29',
'2018-03-30', '2018-04-02', '2018-04-03', '2018-04-04',
'2018-04-09', '2018-04-10'],
dtype='datetime64[ns]', name='date', freq=None)
Account.daily_hold.index.levels[0]
Out[99]:
Index(['2018-02-26', '2018-03-06', '2018-03-07', '2018-03-13', '2018-03-29',
'2018-04-10'],
dtype='object', name='date')
pd.Series(data = None, index = Account.trade_range, name = 'date')
Out[100]:
2018-02-26 NaN
2018-02-27 NaN
······
2018-04-03 NaN
2018-04-04 NaN
2018-04-09 NaN
2018-04-10 NaN
Name: date, dtype: float64

市值

1
2
3
4
5
6
7
8
9
10
Risk.market_value.sum(axis = 1)
Out[101]:
date account_cookie
2018-02-26 JCSC_EXAMPLE 34180.0
2018-03-06 JCSC_EXAMPLE 67340.0
2018-03-07 JCSC_EXAMPLE 77850.0
2018-03-13 JCSC_EXAMPLE 89470.0
2018-03-29 JCSC_EXAMPLE 125150.0
2018-04-10 JCSC_EXAMPLE 142920.0
dtype: float64

现金表

Python
1
2
3
4
5
6
7
8
9
10
11
12
Account.cash_table
Out[103]:
cash ... account_cookie
datetime account_cookie ...
2018-02-26 00:00:00 JCSC_EXAMPLE 165811 ... JCSC_EXAMPLE
2018-03-06 00:00:00 JCSC_EXAMPLE 132133 ... JCSC_EXAMPLE
2018-03-07 00:00:00 JCSC_EXAMPLE 120078 ... JCSC_EXAMPLE
2018-03-13 00:00:00 JCSC_EXAMPLE 107583 ... JCSC_EXAMPLE
2018-03-29 00:00:00 JCSC_EXAMPLE 73414.5 ... JCSC_EXAMPLE
2018-04-10 00:00:00 JCSC_EXAMPLE 61989.5 ... JCSC_EXAMPLE
JCSC_EXAMPLE 50214.5 ... JCSC_EXAMPLE
[7 rows x 4 columns]

总资产

Python
1
2
3
4
5
6
7
8
9
10
Risk.assets
Out[105]:
date
2018-02-26 199991.4550
2018-03-06 199473.0375
2018-03-07 197928.0375
2018-03-13 197053.0375
2018-03-29 198564.4975
2018-04-10 193134.4975
Name: 0, dtype: float64
Python
1
2
3
4
5
6
7
8
9
10
11
12
Risk.benchmark_assets
Out[107]:
date code
2018-02-26 000300 201222.455660
2018-02-27 000300 198318.268432
2018-02-28 000300 196591.586456
······
2018-04-03 000300 188717.447599
2018-04-04 000300 188345.140959
2018-04-09 000300 188250.842820
2018-04-10 000300 191878.145307
Name: close, dtype: float64

策略评价

Python
1
2
Risk.plot_assets_curve()
pylab.savefig("Riskplot.png")


Python
1
2
Risk.plot_dailyhold()
pylab.savefig("Riskdailyhold.png")


Python
1
2
Risk.plot_signal()
pylab.savefig("signal.png")

盈利分析

Python
1
2
3
4
5
6
7
Risk.profit_construct

Out[3]:
{'total_buyandsell': -6820.0,
'total_tax': 0,
'total_commission': -45.5,
'total_profit': -6865.5}

策略表现

Python
1
2
3
Performance = QA.QA_Performance(Account)
Performance.pnl_fifo
Performance.plot_pnlmoney(Performance.pnl_filo)

存储结果

Python
1
2
3
4
5
6
Account.save()
Risk.save()
account = QA.QA_Account().from_message(account_info[0])
account

Out[11]: < QA_Account JCSC_EXAMPLE>
# Python

评论

程振兴

程振兴 @czxa.top
截止今天,我已经在本博客上写了659.4k个字了!

Your browser is out-of-date!

Update your browser to view this website correctly. Update my browser now

×