本文涉及到的个股市场数据来自开源量化框架QUANTAXIS,涉及到的质量(mass)数据来自开源数据引擎原版tushare和湘财证券版tushare。需安装好QUANTAXIS,tushare,xcsc_tushare等模块,如果你不需要湘财证券版tushare的数据,也可以只安装前面两个模块。
    源代码粘贴如下(回测效果图在源代码后面):

    1. # -*- coding: utf-8 -*-
    2. """
    3. @author: yinxiuqu
    4. @license: GNU General Public License v3.0
    5. @contact: yinxiuqu@qq.com
    6. @file: physics.py
    7. @time: 2020/7/7 下午9:48
    8. @Software: PyCharm
    9. 本文件定义简单物理模型进行金融建模
    10. 共有三种动能计算方法,分别为:
    11. 1、日内(intraday):今收/今开 - 1
    12. 2、隔夜(overnight):今开/昨收 - 1
    13. 3、正常(normal):今收/昨收 - 1
    14. """
    15. # 导入相应模块
    16. from datetime import datetime, timedelta
    17. import pandas as pd
    18. import numpy as np
    19. import copy
    20. import QUANTAXIS as QA
    21. import tushare as ts
    22. import xcsc_tushare as xcts
    23. # 预处理数据接口和常量
    24. ts.set_token('bf1ec1f2e80391feae35a46db063501ad03accf588990dxxxxxxxxxxxxxx')
    25. # 此处请用你自己的token
    26. xcts.set_token('96e5167f90449e742407e3dc09de7795fd13edfee80xxxxxxxxxxxxxxx')
    27. # 此处请用你自己的token
    28. pro = ts.pro_api()
    29. xcpro = xcts.pro_api(env='prd')
    30. now = datetime.now().strftime('%Y-%m-%d')
    31. # 定义从QUANTAXIS获取个股日线数据的函数
    32. def get_stock_day_QA(start, end=None, code=None):
    33. """
    34. :param code:str type,stock code
    35. :param start:str type,start date
    36. :param end:str type,end date
    37. :return:multi-index sources-frame
    38. """
    39. # 处理默认结束日期
    40. if end is None or end > now or end is 'now':
    41. end = now
    42. # 如果结束日为今天且今天是交易日且没收盘
    43. if end == now and QA.QA_util_if_trade(end) and '09:30' < datetime.now().strftime('%H:%M') < '15:00':
    44. # 盘中在线获取个股列表
    45. if code is None:
    46. code = QA.QAFetch.QATdx.QA_fetch_get_stock_list('stock').index.get_level_values(0).to_list()
    47. # 如果起止日期不是同一个交易日
    48. if start != end:
    49. df1 = QA.QA_fetch_stock_day_adv(code=code, start=start, end=QA.QA_util_get_last_day(end, 1)).data
    50. df2 = QA.QAFetch.QATdx.QA_fetch_get_stock_latest(code=code)
    51. if df2 is not None:
    52. df2 = df2.loc[now].drop_duplicates()
    53. df2.rename({'vol': 'volume'}, axis='columns', inplace=True)
    54. df2.set_index([df2.index, 'code'], drop=True, inplace=True)
    55. df2 = df2[list(df1.columns)]
    56. df = df1.append(df2)
    57. # 如果起止日期是同一个交易日,只获取盘中数据(因为还没收盘)
    58. else:
    59. df = QA.QAFetch.QATdx.QA_fetch_get_stock_latest(code=code)
    60. df = df.loc[now].drop_duplicates()
    61. df.rename({'vol': 'volume'}, axis='columns', inplace=True)
    62. df.set_index([df.index, 'code'], drop=True, inplace=True)
    63. df = df[['open', 'high', 'low', 'close', 'volume', 'amount']]
    64. # 在非交易日或者交易日的非交易时间段内
    65. else:
    66. # 从数据库获取个股列表
    67. if code is None:
    68. code = QA.QA_fetch_stock_list_adv().index.to_list()
    69. df = QA.QA_fetch_stock_day_adv(code=code, start=start, end=end).data
    70. return df
    71. # 将多维列表打平为一维列表的函数(因为python3已经不支持列表flatten了)
    72. def flatten_list(input_list):
    73. """
    74. 将多维列表打平为一维列表的函数,因为python3已经不支持列表flatten。
    75. :param input_list: 输入的多维列表
    76. :return: 返回打通后的一维列表
    77. """
    78. output_list = []
    79. while True:
    80. if input_list == []:
    81. break
    82. for index, value in enumerate(input_list):
    83. # index :索引序列 value:索引序列对应的值
    84. # enumerate() 函数用于将一个可遍历的数据对象(如列表、元组或字符串)组合为一个索引序列,
    85. # 同时列出数据和数据下标,一般用在 for 循环当中。
    86. if type(value) == list:
    87. input_list = value + input_list[index+1:]
    88. break # 这里跳出for循环后,从While循环进入的时候index是更新后的input_list新开始算的。
    89. else:
    90. output_list.append(value)
    91. input_list.pop(index)
    92. break
    93. return output_list
    94. # 定义股票代码风格变换函数,本函数仅改变单个代码,即字符串代码
    95. def change_codestyle(code, style):
    96. """
    97. :param code: str
    98. :param style: 'QA'/'ts'/'bs'/'jq'分别为QUANTAXIS、tushare、baostock和joinquant风格
    99. :return: 返回改变风格后的code
    100. """
    101. # 特殊符号做code保持不变,例如
    102. '',' '
    103. if len(code) <= 3:
    104. code = code
    105. else:
    106. if style == 'QA':
    107. if code.startswith('sh') or code.startswith('sz'):
    108. code = code[3:]
    109. elif code.endswith('SH') or code.endswith('SZ'):
    110. code = code[:-3]
    111. elif code[-1].isdigit() and code[0].isdigit():
    112. code = code
    113. elif code.endswith('XSHG') or code.endswith('XSHE'):
    114. code = code[:-5]
    115. elif style == 'ts':
    116. if code.startswith('sh') or code.startswith('sz'):
    117. code = code[3:] + '.' + code[:2].upper()
    118. elif code.endswith('SH') or code.endswith('SZ'):
    119. code = code
    120. elif code[-1].isdigit() and code[0].isdigit():
    121. if code[0] == '6':
    122. code = code + '.SH'
    123. else:
    124. code = code + '.SZ'
    125. elif code.endswith('XSHG') or code.endswith('XSHE'):
    126. if code.endswith('XSHG'):
    127. code = code[:-5] + '.SH'
    128. else:
    129. code = code[:-5] + '.SZ'
    130. elif style == 'bs':
    131. if code.startswith('sh') or code.startswith('sz'):
    132. code = code
    133. elif code.endswith('SH') or code.endswith('SZ'):
    134. code = code[-2:].lower() + '.' + code[:6]
    135. elif code[-1].isdigit() and code[0].isdigit():
    136. if code[0] == '6':
    137. code = 'sh.' + code
    138. else:
    139. code = 'sz.' + code
    140. elif code.endswith('XSHG') or code.endswith('XSHE'):
    141. if code.endswith('XSHG'):
    142. code = 'sh.' + code[:-5]
    143. else:
    144. code = 'sz.' + code[:-5]
    145. elif style == 'jq':
    146. if code.startswith('sh') or code.startswith('sz'):
    147. if code.startswith('sh'):
    148. code = code[3:] + '.XSHG'
    149. else:
    150. code = code[3:] + '.XSHE'
    151. elif code.endswith('SH') or code.endswith('SZ'):
    152. if code.endswith('SH'):
    153. code = code[:-3] + '.XSHG'
    154. else:
    155. code = code[:-3] + '.XSHE'
    156. elif code[-1].isdigit() and code[0].isdigit():
    157. if code[0] == '6':
    158. code = code + '.XSHG'
    159. else:
    160. code = code + '.XSHE'
    161. elif code.endswith('XSHG') or code.endswith('XSHE'):
    162. code = code
    163. return code
    164. # 定义code风格改变函数,code可以是字符串或列表。
    165. def change_codeformat(code, style):
    166. """
    167. :param code: str or list
    168. :param style: 'QA'/'ts'/'bs'/'jq'分别为QUANTAXIS/tushare/baostock/joinquant风格
    169. :return: 返回改变风格后的code
    170. """
    171. if type(code) is str:
    172. code = change_codestyle(code, style)
    173. elif type(code) is list:
    174. code = [change_codestyle(i, style) for i in code]
    175. elif type(code) is pd.Series:
    176. index = code.index
    177. code = code.values.tolist()
    178. code = [change_codestyle(i, style) for i in code]
    179. code = pd.Series(code, index=index)
    180. return code
    181. # 定义日期格式转换函数,'2018-01-01'、'20180101'、'2018/01/01'、20180101等格式互相转化
    182. def change_dateformat(date, sep='-'):
    183. """
    184. :param date: 输入的日期
    185. :param sep: 日期中的间隔符号
    186. """
    187. if type(date) is int:
    188. date = str(date)
    189. elif type(date) is datetime:
    190. date = datetime.strftime(date.date(), '%Y%m%d')
    191. elif date is None:
    192. date = date
    193. elif type(date) is str:
    194. if sep == '-':
    195. if '-' not in date:
    196. if '/' not in date:
    197. date = date[:4] + '-' + date[4:6] + '-' + date[6:]
    198. elif '/' in date:
    199. date = date.replace('/', '-')
    200. if sep == '/':
    201. if '/' not in date:
    202. if '-' not in date:
    203. date = date[:4] + '/' + date[4:6] + '/' + date[6:]
    204. elif '-' in date:
    205. date = date.replace('-', '/')
    206. if sep is None:
    207. if '/' in date:
    208. date = date.replace('/', '')
    209. elif '-' in date:
    210. date = date.replace('-', '')
    211. if sep == '':
    212. if '/' in date:
    213. date = date.replace('/', '')
    214. elif '-' in date:
    215. date = date.replace('-', '')
    216. return date
    217. # 定义获取回测期间选股、换股日期节点的函数。
    218. def date_node(start, end, freq='m', n=None):
    219. """
    220. :param start: 回测起始日期
    221. :param end: 回测终止日期
    222. :param freq: 选股、换股频率,'d'/'w'/'m'/'q'/'y':分别表示日、周、月、季、年
    223. 当freq='w'时,按照每5个交易日取样(避免某些周无交易日或只有一两个交易日)。
    224. 当freq = None时,时间节点按n个交易日确定,n和freq不能同时为None。
    225. :param n: 选股、换股频率,为n个交易日。n和freq不能同时为None,必有一个为None。
    226. :return: 返回选股、换股时间节点的日期,即每个频率周期段的最后一个交易日
    227. """
    228. # 将日期统一成QA风格形式
    229. start = change_dateformat(start, sep='-')
    230. end = change_dateformat(end, sep='-')
    231. date_range = QA.QA_util_get_trade_range(start, end)
    232. df_date_range = pd.DataFrame(data=date_range, index=date_range)
    233. df_date_range.index = pd.to_datetime(df_date_range.index)
    234. if n is None:
    235. if freq == 'w':
    236. df_date_range = df_date_range.reset_index()
    237. df_date_range = df_date_range[df_date_range.index % 5 == 0]
    238. df_date_range = df_date_range.set_index('index')
    239. elif freq == 'd':
    240. df_date_range = df_date_range
    241. else:
    242. df_date_range = df_date_range.resample(rule=freq).first() # 以周期初为时间节点
    243. else:
    244. if n == 1:
    245. df_date_range = df_date_range
    246. else:
    247. df_date_range = df_date_range.reset_index()
    248. df_date_range = df_date_range[df_date_range.index % n == 0]
    249. df_date_range = df_date_range.set_index('index')
    250. list_date_range = list(df_date_range.iloc[:, 0])
    251. df = pd.DataFrame(data=list_date_range, index=list_date_range)
    252. df.index = pd.to_datetime(df.index)
    253. df.columns = ['date']
    254. df = df.dropna() # 排除日线取样时的nan
    255. return df
    256. # 定期换股函数,按传入的选股序列,计算买股、卖股、换股的序列
    257. def exchange_stocks(df, start, end, buy_today=False, h_days=None):
    258. """
    259. :param df: 传入的股票序列,以日期为index,包含'code'列
    260. :param start: 开始日期
    261. :param end: 结束日期
    262. :param buy_today: 是否当天买:False为第二天买,True为当天买
    263. :param h_days: 为None时按照换股节点换股,为数字时按照数字代表的交易日数换股
    264. :return: 返回买股、卖股、换股的序列
    265. """
    266. # 处理缺省值
    267. if end is None:
    268. end = now
    269. # 将日期统一成QA风格形式
    270. start = change_dateformat(start, sep='-')
    271. end = change_dateformat(end, sep='-')
    272. # 如果按照节点换股
    273. if h_days is None:
    274. # 根据每个节点所选股票列表,确定卖股节点为下一个日期节点
    275. # 注意并非推迟一个交易日,而是推迟一个节点!
    276. df = df.assign(sell_code=df.code.shift(1))
    277. # 如果不是选股当天买入或换股,而是第二个交易日
    278. if buy_today is False:
    279. # 如果换股频率不是每天(如果连续四个节点相差不止一个交易日,可确保节点不是每天换股):
    280. if df.index[4] - df.index[3] != timedelta(days=1) or \
    281. df.index[3] - df.index[2] != timedelta(days=1) or \
    282. df.index[2] - df.index[1] != timedelta(days=1) or \
    283. df.index[1] - df.index[0] != timedelta(days=1):
    284. # 恢复成日采样形式,便于推算后一个交易日
    285. df = df.resample('d').last()
    286. # 去掉非交易日行
    287. trade_day_index = QA.QA_util_get_trade_range(start, end)
    288. trade_day_index = pd.to_datetime(trade_day_index)
    289. df = df.reindex(trade_day_index)
    290. # 此处买入为推后一个交易日,并非推迟一个节点
    291. df = df.assign(buy_code=df.code.shift(1))
    292. # 卖股也推迟一个交易日,与买股同一个交易日,以便于判断是否需要新卖出
    293. df['sell_code'] = df.sell_code.shift(1)
    294. # 如果是选股当天买股或换股,很简单
    295. else:
    296. df = df.assign(buy_code=df.code)
    297. # 恢复成日采样形式,便于回测统一日期进度
    298. df = df.resample('d').last()
    299. # 去掉非交易日行
    300. trade_day_index = QA.QA_util_get_trade_range(start, end)
    301. trade_day_index = pd.to_datetime(trade_day_index)
    302. df = df.reindex(trade_day_index)
    303. # 对buy_code、sell_code分别和code进行去除交集运算,求出须实际买卖的股票代码列表
    304. df['new_sell_code'] = [np.nan] * len(df)
    305. df['new_buy_code'] = [np.nan] * len(df)
    306. df['new_sell_code'] = df['new_sell_code'].astype(object)
    307. df['new_buy_code'
    308. ] = df['new_buy_code'].astype(object)
    309. for i in df.index:
    310. # 如果没有卖出股,只有买入股,则只买入股
    311. if (type(df.loc[i, 'sell_code']) is not list or df.loc[i, 'sell_code'] == []) and \
    312. (type(df.loc[i, 'buy_code']) is list and len(df.loc[i, 'buy_code']) > 0):
    313. df.at[i, 'new_buy_code'] = list(set(df.loc[i, 'buy_code']))
    314. # 如果买股卖股都有,则买卖都取差集
    315. elif (type(df.loc[i, 'sell_code']) is list and len(df.loc[i, 'sell_code']) > 0) \
    316. and (type(df.loc[i, 'buy_code']) is list and len(df.loc[i, 'buy_code']) > 0):
    317. df.at[i, 'new_sell_code'] = list(set(df.loc[i, 'sell_code']) - set(df.loc[i, 'buy_code']))
    318. df.at[i, 'new_buy_code'] = list(set(df.loc[i, 'buy_code']) - set(df.loc[i, 'sell_code']))
    319. # 如果没有买入股,只有卖出股,则只卖出股
    320. elif (type(df.loc[i, 'sell_code']) is list and len(df.loc[i, 'sell_code']) > 0) and \
    321. (type(df.loc[i, 'buy_code']) is not list or df.loc[i, 'buy_code'] == []):
    322. df.at[i, 'new_sell_code'] = list(set(df.loc[i, 'sell_code']))
    323. # 如果date列包含在df中,则抛弃掉
    324. if 'date' in df.columns:
    325. df = df.drop(columns='date')
    326. # 按固定交易日数持股,即持股h_days卖掉。
    327. else:
    328. date_range = pd.date_range(start, end)
    329. df_date_range = pd.DataFrame(data=date_range, index=date_range)
    330. df_date_range.index = pd.to_datetime(df_date_range.index)
    331. # 为原始code单独建立一个序列,深度复制
    332. df_code = copy.deepcopy(df)
    333. # 将code序列转化成日期为交易日的序列,此处原index为非交易日的转化后可能产生重叠的index,如周六、周日都转化为后一个周一等。
    334. df_code.index = [QA.QA_util_get_real_date(i, towards=1) for i in df_code.index]
    335. # 合并index重复项
    336. df_code = df_code.groupby(level=0, group_keys=False).apply(lambda x: x.values.tolist())
    337. df_code = pd.Series([flatten_list(i) for i in df_code], index=df_code.index)
    338. df_code = pd.DataFrame(df_code)
    339. df_code.columns = ['code']
    340. df_code.index = pd.to_datetime(df_code.index)
    341. #
    342. 将原选股序列恢复成每天的形式
    343. df = df.reindex(df_date_range.index)
    344. df['buy_code'] = np.nan
    345. df['sell_code'] = np.nan
    346. # 将这两列转化为object类型,以便能够赋列表形式的值
    347. df[['buy_code', 'sell_code']] = df[['buy_code', 'sell_code']].astype(object)
    348. # 把df对应df_code日期位置的buy_code设置为df_code.buy_code的值
    349. if buy_today is True:
    350. # 为df添加(h_days)个交易日,便于最后一个交易日买的股能在h_days个交易日后卖
    351. added_trad_start = QA.QA_util_get_real_date(df.index[-1]) # 此处是real_date:如果是交易日就当天买
    352. added_trade_end = QA.QA_util_get_next_trade_date(added_trad_start, h_days)
    353. added_trade_days = QA.QA_util_get_trade_range(added_trad_start, added_trade_end)
    354. added_trade_days = pd.to_datetime(added_trade_days)
    355. df_added = pd.DataFrame([np.nan]*len(added_trade_days), index=added_trade_days)
    356. df = df.append(df_added)
    357. # 当天购买
    358. df.loc[df_code.index, 'buy_code'] = df_code.code.to_list()
    359. else:
    360. # 为df添加(1+h_days)个交易日,便于最后一天选出的个股能在下一个交易日购买,在(1+h_days)个交易日后卖
    361. added_trad_start = QA.QA_util_get_next_trade_date(df.index[-1]) # 此处是next_trade,下一个交易日买
    362. added_trade_end = QA.QA_util_get_next_trade_date(added_trad_start, h_days)
    363. added_trade_days = QA.QA_util_get_trade_range(added_trad_start, added_trade_end)
    364. added_trade_days = pd.to_datetime(added_trade_days)
    365. df_added = pd.DataFrame([np.nan]*len(added_trade_days), index=added_trade_days)
    366. df_added.columns = ['code']
    367. df = df.append(df_added, sort=True)
    368. # 推迟一个交易日购买
    369. next_trade_date = [QA.QA_util_get_next_trade_date(i) for i in df_code.index]
    370. next_trade_date = pd.to_datetime(next_trade_date)
    371. df.loc[next_trade_date, 'buy_code'] = df_code.code.to_list()
    372. # 将选股序列按h_days递延,作为卖出列表
    373. # 买入后持有h_days个交易日卖掉
    374. h_days_trade_date = [QA.QA_util_get_next_trade_date(i, h_days) for i in df_code.index]
    375. h_days_trade_date = pd.to_datetime(h_days_trade_date)
    376. df.loc[h_days_trade_date, 'sell_code'] = df_code.code.to_list()
    377. return df
    378. # 定义计算一段日期内个股日内或隔夜收益率函数(每天)
    379. def cal_stock_ret(start, end=None, code=None, method='normal'):
    380. """
    381. :param start: 开始日期
    382. :param end: 结束日期
    383. :param code: 个股代码,等于None时为市场内所有个股代码列表
    384. :param method: 计算收益率的方式,intraday为日内,overnight为隔夜,normal为正常, both为日内+隔夜的相反数
    385. :return: 返回个股收益率序列
    386. """
    387. # 日期和代码预处理
    388. start = QA.QA_util_get_real_date(start)
    389. if end is None:
    390. end = now
    391. if type(code) is str:
    392. code = [code]
    393. # 计算普通收益率
    394. if method == 'normal':
    395. start = QA.QA_util_get_last_day(start)
    396. # 获取个股行情数据
    397. df = get_stock_day_QA(code=code, start=start, end=end)
    398. # 将行情数据转化为QA的DataStruct数据结构,以便于复权
    399. data = QA.QA_DataStruct_Stock_day(df)
    400. # 前复权(仅计算今天盘中数据时,不能复权,其他情况都做复权处理)
    401. if end == now and QA.QA_util_if_trade(end) and \
    402. '09:30' < datetime.now().strftime('%H:%M') < '15:00' and start != end:
    403. data = data.to_qfq()
    404. ret = data.close / data.close.groupby(level=1).shift(1) - 1
    405. # 计算日内收益率
    406. elif method == 'intraday':
    407. # 获取个股行情数据
    408. df = get_stock_day_QA(code=code, start=start, end=end)
    409. # 将行情数据转化为QA的DataStruct数据结构,以便于复权
    410. data = QA.QA_DataStruct_Stock_day(df)
    411. # 前复权
    412. data = data.to_qfq()
    413. ret = data.close / data.open - 1
    414. # 计算隔夜收益率
    415. elif method == 'overnight':
    416. start = QA.QA_util_get_last_day(start)
    417. # 获取个股行情数据
    418. df = get_stock_day_QA(code=code, start=start, end=end)
    419. # 将行情数据转化为QA的DataStruct数据结构,以便于复权
    420. data = QA.QA_DataStruct_Stock_day(df)
    421. # 前复权
    422. data = data.to_qfq()
    423. ret = data.open / data.close.groupby(level=1).shift(1) - 1
    424. # 计算日内-隔夜:即日内+隔夜的相反数
    425. elif method == 'both':
    426. start = QA.QA_util_get_last_day(start)
    427. # 获取个股行情数据
    428. df = get_stock_day_QA(code=code, start=start, end=end)
    429. # 将行情数据转化为QA的DataStruct数据结构,以便于复权
    430. data = QA.QA_DataStruct_Stock_day(df)
    431. # 前复权
    432. data = data.to_qfq()
    433. # 日内收益率与隔夜收益率的差
    434. ret = data.close / data.open - data.open / data.close.groupby(level=1).shift(1)
    435. # 错误方式反馈
    436. else:
    437. ret = None
    438. print('Only can calculate intraday or overnight return! Please retry!')
    439. ret = ret.dropna()
    440. return ret
    441. # 计算个股前n个交易日涨跌率的函数
    442. def cal_n_ret(code=None, n=14, m=0, date=None, assets='stock', method='normal'):
    443. """
    444. :param code: 用于计算动量的指数或个股代码列表
    445. :param n: 计算前n天的动量
    446. :param m: 排除最近m天的动量
    447. :param date: 基准日期
    448. :param assets: 计算动量的证券种类,'index' or 'stock' or 'future'
    449. :param method: 计算动量的方法,'overnight' 、'intraday' or 'normal' or 'both'
    450. :return: 返回各标的的动量序列
    451. """
    452. if date is None:
    453. date = now
    454. date = QA.QA_util_get_real_date(date)
    455. # 过去n个交易日的日期(前一个交易日就是它本身)
    456. last_n_day = QA.QA_util_get_last_day(date=date, n=n - 1)
    457. if type(code) is str: # 如果code为字符串,即为单独一个代码而非列表,则转化为列表形式
    458. code = [code]
    459. # 如果标的为指数
    460. if assets == 'index':
    461. pass
    462. # 如果标的为个股
    463. elif assets == 'stock':
    464. ret = cal_stock_ret(start=last_n_day, end=date, code=code, method=method)
    465. else: # 为期货等预留
    466. pass
    467. # 排除前m天的涨跌率
    468. if m != 0:
    469. index2 = ret.index.get_level_values(level=0).drop_duplicates()[:-m]
    470. ret = ret.reindex(index2, level=0)
    471. # 对过去n-m个交易日收益率进行加总
    472. n_ret = ret.groupby(level=1).sum()
    473. n_ret.name = 'sum_ret'
    474. return n_ret.sort_values() # 返回排序后的动量序列
    475. # 计算一段日期内,每天前n个交易日的涨跌率的函数(上个函数仅计算一个基准日,本函数计算一段日期内每个交易日做基准日)
    476. def cal_period_ret(start, end=None, code=None, n=14, m=0, assets='index', method='normal'):
    477. """
    478. :param start: 起始日期
    479. :param end: 结束日期
    480. :param code: 用于计算动量的指数或个股代码或代码列表
    481. :param n: 计算前n天的动量
    482. :param m: 排除最近m天的动量
    483. :param assets: 计算动量的证券种类,'index' or 'stock'
    484. :param method: 计算动量的方法,'overnight'、'intraday' or 'normal' or 'both'
    485. :return: 返回各标的的动量序列(multiindex)
    486. """
    487. if end is None:
    488. end = now
    489. end = QA.QA_util_get_real_date(end) #
    490. 结束日期如果为非交易日,则向前找最近交易日做结束日
    491. start = QA.QA_util_get_real_date(start, towards=1) # 开始日期如果为非交易日,则向后找最近交易日做开始日
    492. # start之前的n个交易日的日期(前一个交易日就是它本身)
    493. last_n_day = QA.QA_util_get_last_day(date=start, n=n-1)
    494. if type(code) is str: # 如果code为字符串,即为单独一个代码而非列表,则转化为列表形式
    495. code = [code]
    496. # 如果标的为指数
    497. if assets == 'index':
    498. pass
    499. # 如果标的为个股
    500. elif assets == 'stock':
    501. ret = cal_stock_ret(start=last_n_day, end=end, code=code, method=method)
    502. else: # 为期货预留
    503. pass
    504. period_ret = ret.groupby(level=1).rolling(n - m).sum().dropna() # 按窗口移动加总收益率
    505. period_ret = period_ret.droplevel(level=2) # 上一步计算中,momentum会形成第三层index,所以抛弃第三层index
    506. period_ret.name = 'sum_ret' # 设置序列名称
    507. period_ret = period_ret.swaplevel().sort_index(level=0) # 按日期排序
    508. # 下面的排序中增加了一层重复index,去掉。返回根据n日涨跌率排名后的multi_index序列(以日期和证券代码为index)。
    509. period_ret = period_ret.groupby(level=0).apply(lambda x: x.sort_values()).droplevel(0)
    510. if m != 0:
    511. period_ret = period_ret.groupby(level=1).shift(m).dropna().\
    512. groupby(level=0).apply(lambda x: x.sort_values()).droplevel(0)
    513. return period_ret
    514. # 定义计算一段日期个股涨跌幅的涨跌幅,即加速度acceleration
    515. def cal_acceleration_period(start, end=None, code=None, n=14, m=0, assets='index', method='normal'):
    516. """
    517. :param start: 起始日期
    518. :param end: 结束日期
    519. :param code: 用于计算动量的指数或个股代码或代码列表
    520. :param n: 计算前n天每天的收益率
    521. :param m: 排除前m天的收益率
    522. :param assets: 计算动量的证券种类,'index' or 'stock' or 'future'
    523. :param method: 计算动量的方法,'overnight'、'intraday' or 'normal' or 'both'
    524. :return: 返回各标的的动量序列(multiindex)
    525. """
    526. # 先处理起止时间
    527. if end is None:
    528. end = now
    529. # 结束日期如果为非交易日,则向前找最近交易日做结束日
    530. end = QA.QA_util_get_real_date(end)
    531. # 开始日期如果为非交易日,则向后找最近交易日做开始日
    532. start = QA.QA_util_get_real_date(start, towards=1)
    533. # start之前的n个交易日的日期(前一个交易日就是它本身)
    534. last_n_day = QA.QA_util_get_last_day(date=start, n=n-1)
    535. # 如果code为字符串,即为单独一个代码而非列表,则转化为列表形式
    536. if type(code) is str:
    537. code = [code]
    538. # 先求前n天内,投资品的每天的收益率(当作速度)
    539. velocity = cal_period_ret(start=last_n_day, end=end, code=code, n=n, m=m,
    540. assets=assets, method=method)
    541. # 求每天的加速度
    542. acceleration = velocity / velocity.groupby(level=1).shift() - 1
    543. # 求过去n天加速度的和
    544. acceleration = acceleration.rolling(n).sum()
    545. # 修改名称
    546. acceleration.name = 'acceleration'
    547. return acceleration.dropna().sort_index().groupby(level=0, group_keys=False).apply(lambda x: x.sort_values())
    548. # 计算市场或板块或给定范围的个股,每天涨跌幅均值与其标准差的比值,命名为尹氏指数
    549. def cal_yin_period(start, end=None, code=None, assets='stock', method='normal'):
    550. """
    551. :param start: 开始日期
    552. :param end: 结束日期
    553. :param code: 计算范围, 个股或指数代码列表
    554. :param assets: 投资种类,'index'/'stock'
    555. :param method: 涨跌幅计算方式
    556. :return: 返回每天涨跌幅均值与其标准差的比值的序列
    557. """
    558. # 先处理起止时间
    559. if end is None:
    560. end = now
    561. # 结束日期如果为非交易日,则向前找最近交易日做结束日
    562. end = QA.QA_util_get_real_date(end)
    563. # 开始日期如果为非交易日,则向后找最近交易日做开始日
    564. start = QA.QA_util_get_real_date(start, towards=1)
    565. # 如果code为字符串,即为单独一个代码而非列表,则转化为列表形式
    566. if type(code) is str:
    567. code = [code]
    568. # 求每天的收益率
    569. ret = cal_period_ret(start=start, end=end, code=code, n=1, m=0,
    570. assets=assets, method=method)
    571. # 求每天收益率的均值
    572. mean = ret.groupby(level=0).mean()
    573. std = ret.groupby(level=0).std()
    574. # 求尹氏指数
    575. yin = mean / std
    576. return mean, std, yin
    577. # 定义一段日期内,求每天的净资产(或者公司其他的总量性质的统计量),当作个股的质量因子,用于构建动量、动能模型
    578. def cal_period_mass(start, end=None, code=None, kind='net_assets'):
    579. """
    580. :param start: 起始日期
    581. :param end: 结束日期
    582. :param code: 用于计算动量的指数或个股代码或代码列表
    583. :param kind: 质量类型,如净资产/营业收入/经营活动产生的现金流量净额等等总量性质的统计量,具体可参看tushare每日指标数据
    584. :return: 返回每天各个股的质量(multiindex)
    585. """
    586. # 先处理起止时间
    587. if end is None:
    588. end = now
    589. # 结束日期如果为非交易日,则向前找最近交易日做结束日
    590. end = QA.QA_util_get_real_date(end)
    591. # 开始日期如果为非交易日,则向后找最近交易日做开始日
    592. start = QA.QA_util_get_real_date(start, towards=1)
    593. # 将起止日期内的交易日期做成序列
    594. df_date = QA.QA_util_get_trade_range(start=start, end=end)
    595. df_date = pd.to_datetime(df_date)
    596. df_date = pd.DataFrame(df_date, index=df_date, columns=['date'])
    597. # 如果code为字符串,即为单独一个代码而非列表,则转化为列表形式
    598. if type(code) is str:
    599. code = change_codeformat(code, 'QA')
    600. code = [code]
    601. elif type(code) is list:
    602. code = change_codeformat(code, 'QA')
    603. # 逐日获取全市场质量因子
    604. df = pd.DataFrame()
    605. for i in df_date.date:
    606. try:
    607. # 从原版tushare获取指标
    608. df = df.append(pro.daily_basic(trade_date=i.strftime('%Y%m%d'), fields="ts_code,trade_date," + kind))
    609. # 从湘财tushare获取指标
    610. except Exception:
    611. df = df.append(xcpro.daily_basic(trade_date=i.strftime('%Y%m%d'), fields="ts_code,trade_date," + kind))
    612. # 整理数据
    613. df.trade_date = pd.to_datetime(df.trade_date)
    614. df.ts_code = change_codeformat(df.ts_code.values.tolist(), 'QA')
    615. df.columns = ['code', 'date', kind]
    616. df.set_index(['date', 'code'], inplace=True)
    617. df.sort_index(inplace=True)
    618. if code is not None:
    619. df = df.reindex(code, level=1)
    620. return df[kind]
    621. # 定义一段日期内,计算每天前n个交易日个股的物理动量的函数
    622. def cal_physics_momentum(start, end=None, code=None, n=14, m=0, method='normal', kind='net_assets'):
    623. """
    624. :param start: 起始日期
    625. :param end: 结束日期
    626. :param code: 用于计算动量的指数或个股代码或代码列表
    627. :param n: 计算前n天的动量
    628. :param m: 排除最近m天的动量
    629. :param method: 计算动量的方法,'overnight'、'intraday' or 'normal' or 'both'
    630. :param kind: 质量类型,如净资产/营业收入/经营活动产生的现金流量净额等等,具体可参看tushare每日指标数据
    631. :return: 返回各标的的动量序列(multiindex)
    632. """
    633. # 先求物理速度,即前n天的涨跌幅
    634. velocity = cal_period_ret(start=start, end=end, code=code, n=n, m=m,
    635. assets='stock', method=method)
    636. # 再求物理质量,即个股每天的净资产
    637. mass = cal_period_mass(start=start, end=end, code=code, kind=kind)
    638. # 最后求动量,p=mv
    639. momentum = velocity * mass
    640. return momentum.dropna()
    641. # 定义一段日期内,计算每天前n个交易日个股的物理动能的函数
    642. def cal_physics_energy(start, end=None, code=None, n=14, m=0, method='normal', kind='net_assets'):
    643. """
    644. :param start: 起始日期
    645. :param end: 结束日期
    646. :param code: 用于计算动量的指数或个股代码或代码列表
    647. :param n: 计算前n天的动量
    648. :param m: 排除最近m天的动量
    649. :param method: 计算动量的方法,'overnight'、'intraday' or 'normal' or 'both'
    650. :param kind: 质量类型,如净资产/营业收入/经营活动产生的现金流量净额等等,具体可参看tushare每日指标数据
    651. :return: 返回各标的的动量序列(multiindex)
    652. """
    653. # 先求物理速度,即前n天的涨跌幅
    654. velocity = cal_period_ret(start=start, end=end, code=code, n=n, m=m,
    655. assets='stock', method=method)
    656. # 再求物理质量,即个股每天的净资产
    657. mass = cal_period_mass(start=start, end=end, code=code, kind=kind)
    658. # 最后求动量,e=0.5mv**2
    659. energy = 0.5 * mass * velocity**2
    660. return energy.dropna()
    661. # 定义将序列数据分割成i段并选取第j段数据的函数(按pandas.qcut分割)
    662. def qcut_select(s, n, m):
    663. """
    664. :param s: 传入用于分割的序列数据,类型为pandas.Series/pandas.Dataframe
    665. :param n: 将序列数据切割成n份
    666. :param m: 选取第m份数据,0=<m<n
    667. :return: 返回分割i份后选取的第j份数据
    668. """
    669. # 排序
    670. s = s.sort_values()
    671. # 如果序列长度小于等于m,则返回原序列
    672. if len(s) <= m:
    673. if len(s) == 1:
    674. pass
    675. else:
    676. s = s[0:1]
    677. # 如果序列长度小于n,大于m,则直接切取第m个元素
    678. elif m < len(s) < n:
    679. s = s[m:m+1]
    680. # 如果序列长度大于等于n,进行分割选取
    681. elif len(s) >= n:
    682. # 为分割设置从0到n-1的标签
    683. labels = [str(i) for i in range(n)]
    684. r = pd.qcut(s, n, labels=labels)
    685. # 取第m层数据
    686. r = r[r == str(m)]
    687. s = s[r.index]
    688. return s
    689. # 定义将序列数据分割成i段并选取第j段数据的函数(按pandas.cut分割)
    690. def cut_select(s, n, m):
    691. """
    692. :param s: 传入用于分割的序列数据,类型为pandas.Series/pandas.Dataframe
    693. :param n: 将序列数据切割成n份
    694. :param m: 选取第m份数据,0=<m<n
    695. :return: 返回分割i份后选取的第j份数据
    696. """
    697. # 排序
    698. s = s.sort_values()
    699. # 如果序列长度小于等于m,则返回第一个元素
    700. if len(s) <= m:
    701. if len(s) == 1:
    702. pass
    703. else:
    704. s = s[0:1]
    705. # 如果序列长度小于n,大于m,则直接切取第m个元素
    706. elif m < len(s) < n:
    707. s = s[m:m+1]
    708. # 为分割设置从0到n-1的标签
    709. elif len(s) >= n:
    710. labels = [str(i) for i in range(n)]
    711. r = pd.cut(s, n, labels=labels)
    712. # 取第m层数据
    713. r = r[r == str(m)]
    714. s = s[r.index]
    715. return s
    716. # 根据个股加速度acceleration分层选取
    717. def acceleration_select(start, k, s, end=None, code=None, freq='m', freq_n=None,
    718. n=14,
    719. split_method='qcut', assets='stock'):
    720. """
    721. :param start: 选股开始日期
    722. :param end: 选股结束日期
    723. :param code: 选股范围,个股代码列表,None时为全市场范围
    724. :param freq: 换股频率,即持股天数,可以计算各换股节点,str type or None
    725. :param freq_n: 换股间隔天数,和freq不能同时为None
    726. :param n: 计算前n天的加速度
    727. :param k: 将动量按从大到小分成k组
    728. :param s: 从k组成选择第s组数据
    729. :param split_method: 分组方法,'qcut/cut',参见pandas.qcut和pandas.cut方法
    730. :param assets: 证券种类,'stok/index'
    731. :return: 返回各选股时间节点上选出的个股列表序列
    732. """
    733. # 先处理起止时间
    734. if end is None:
    735. end = now
    736. # 结束日期如果为非交易日,则向前找最近交易日做结束日
    737. end = QA.QA_util_get_real_date(end)
    738. # 开始日期如果为非交易日,则向后找最近交易日做开始日
    739. start = QA.QA_util_get_real_date(start, towards=1)
    740. # 计算加速度
    741. acceleration = cal_acceleration_period(start=start, end=end, code=code, n=n, assets=assets)
    742. # 获取时间节点序列
    743. df_date = date_node(start=start, end=end, freq=freq, n=freq_n)
    744. # 用时间节点过滤
    745. acceleration = acceleration.reindex(df_date.index, level=0)
    746. # 用分割函数分割k组后取第s组
    747. if split_method == 'qcut':
    748. acceleration = acceleration.groupby(level=0).rank(na_option='top', method='first').groupby(level=0).\
    749. apply(qcut_select, k, s).droplevel(0)
    750. elif split_method == 'cut':
    751. acceleration = acceleration.groupby(level=0).apply(cut_select, k, s).droplevel(0)
    752. # 仅返回每个时间点选出的板块的代码
    753. return acceleration.reset_index(level=1).groupby(level=0).\
    754. apply(lambda x: list(x.code.values.flatten()))
    755. # 根据个股物理模型分层选股函数
    756. def physics_select(start, k, s, end=None, code=None, freq='m', freq_n=None, n=14,
    757. m=0, model='acceleration', kind=None, method='normal'):
    758. """
    759. :param start: 选股开始日期
    760. :param end: 选股结束日期
    761. :param code: 选股范围,个股代码列表,None时为全市场范围
    762. :param freq: 换股频率,即持股天数,可以计算各换股节点,str type or None
    763. :param freq_n: 换股间隔天数,和freq不能同时为None
    764. :param n: 计算前n天的速度或加速度
    765. :param m: 计算速度时排除最近m天数据
    766. :param k: 将动量按从大到小分成k组
    767. :param s: 从k组成选择第s组数据
    768. :param kind: str type, 计算物理质量类型,如'net_assets'等
    769. :param model: 物理模型名称
    770. :param method: 计算速度或加速度的方式,如'normal'/'overnight'/'intraday'等
    771. :return: 返回各选股时间节点上选出的个股列表序列
    772. """
    773. # 先处理起止时间
    774. if end is None:
    775. end = now
    776. # 结束日期如果为非交易日,则向前找最近交易日做结束日
    777. end = QA.QA_util_get_real_date(end)
    778. # 开始日期如果为非交易日,则向后找最近交易日做开始日
    779. start = QA.QA_util_get_real_date(start, towards=1)
    780. # 计算物理模型值
    781. if model == 'acceleration':
    782. physics = cal_acceleration_period(start=start, end=end, code=code, n=n, m=m, assets='stock', method=method)
    783. elif model == 'momentum':
    784. physics = cal_physics_momentum(start=start, end=end, code=code, n=n, m=m, kind=kind, method=method)
    785. elif model == 'energy':
    786. physics = cal_physics_energy(start=start, end=end, code=code, n=n, m=m, kind=kind, method=method)
    787. # 为其他物理模型预留
    788. else:
    789. physics = None
    790. # 获取时间节点序列
    791. df_date = date_node(start=start, end=end, freq=freq, n=freq_n)
    792. # 用时间节点过滤
    793. physics = physics.reindex(df_date.index, level=0)
    794. # 用分割函数分割k组后取第s组
    795. physics = physics.groupby(level=0).rank(na_option='top', method='first').groupby(level=0).\
    796. apply(qcut_select, k, s).droplevel(0)
    797. # 仅返回每个时间点选出的板块的代码
    798. return physics.reset_index(level=1).groupby(level=0).\
    799. apply(lambda x: list(x.code.values.flatten()))
    800. # 定义直接用物理模型选股的回测函数
    801. def stock_physics_backtest(start, end=None, k=300, s=299, code=None, freq='m', freq_n=None,
    802. n=7, m=3, model='momentum', kind='net_assets', method='intraday', cash=1000000):
    803. """
    804. :param start: 回测开始日期
    805. :param end: 回测结束日期
    806. :param k: 将个股动量分成k份
    807. :param s: 选取动量排名第s份的个股
    808. :param code: 指定个股范围,不指定时为所有A股
    809. :param freq: 换股频率,即持股天数,可以计算各换股节点,str type or None
    810. :param freq_n: 换股间隔天数,和freq不能同时为None
    811. :param n: 计算过去n天的数据
    812. :param m: 排除过去m天的数据
    813. :param model: 物理模型名称
    814. :param kind: 计算物理质量的类型,如'net_assets'等
    815. :param method: 计算速度/加速度的方法
    816. :param cash: 回测初始现金
    817. :return: 返回回测结果
    818. """
    819. # 用模型选股
    820. ss = physics_select(start=start, k=k, s=s, end=end, code=code, freq=freq, freq_n=freq_n, n=n,
    821. m=m, model=model, kind=kind, method=method)
    822. ss.name =
    823. 'code'
    824. df = pd.DataFrame(ss)
    825. # 根据每个节点的选股列表,确定各节点的买入卖出股票
    826. df = exchange_stocks(df=df, start=start, end=end, buy_today=False, h_days=None)
    827. stock_code = list() # 用于采集所有涉及到的股票交易数据
    828. for i in df.index:
    829. if type(df.loc[i, 'code']) is list:
    830. for j in df.loc[i, 'code']:
    831. stock_code.append(j)
    832. stock_code = list(set(stock_code))
    833. data = QA.QA_fetch_stock_day_adv(stock_code, df.index[0], df.index[-1])
    834. data = data.to_qfq()
    835. # 设置回测架构
    836. Account = QA.QA_Account(strategy_name='physics_model', user_cookie='yinxiuqu',
    837. portfolio_cookie=str(kind), start=start, end=end)
    838. Broker = QA.QA_BacktestBroker()
    839. Account.reset_assets(cash)
    840. Account.account_cookie = 'k:' + str(k) + 's:' + str(s) + 'n:' + str(n) + 'm:' +\
    841. 'freq:' + str(freq) + 'freq_n:' + str(freq_n)
    842. # 找出选股代码列表里,最长的列表的长度
    843. long = df.code.dropna().groupby(level=0).apply(lambda x: len(x.values[0])).groupby(level=0).max()[0]
    844. cash_weight = 0.6 / long # 平均每只股票分配的资金比例
    845. for items in data.panel_gen:
    846. # 下行用于查找买卖日期节点
    847. if type(df.loc[items.date, 'sell_code'].values[0]) is list:
    848. # 到节点全部清仓
    849. for i in Account.sell_available.index.to_list():
    850. # 可卖的股票未停牌时
    851. if i in items.code:
    852. order = Account.send_order(
    853. code=i,
    854. time=items.date,
    855. amount=Account.sell_available.get(i, 0),
    856. towards=QA.ORDER_DIRECTION.SELL,
    857. price=0,
    858. order_model=QA.ORDER_MODEL.MARKET,
    859. amount_model=QA.AMOUNT_MODEL.BY_AMOUNT
    860. )
    861. # 能够成交时
    862. if order:
    863. Broker.receive_order(QA.QA_Event(order=order, market_data=items.select_code(i)))
    864. trade_mes = Broker.query_orders(Account.account_cookie, 'filled')
    865. res = trade_mes.loc[order.account_cookie, order.realorder_id]
    866. order.trade(res.trade_id, res.trade_price, res.trade_amount, res.trade_time)
    867. if type(df.loc[items.date, 'buy_code'].values[0]) is list:
    868. for j in df.loc[items.date, 'buy_code'].values[0]:
    869. if j in items.code: # 欲买的股票未停牌时
    870. order = Account.send_order(
    871. code=j,
    872. time=items.date,
    873. money=cash_weight * Account.cash_available,
    874. # 添加ams指数权重
    875. # money=cash_weight * df.loc[items.date, 'weight'].values[0] * Account.cash_available,
    876. towards=QA.ORDER_DIRECTION.BUY,
    877. price=items.select_code(j).open[0],
    878. order_model=QA.ORDER_MODEL.MARKET,
    879. amount_model=QA.AMOUNT_MODEL.BY_MONEY
    880. )
    881. if order: # 能够成交时
    882. Broker.receive_order(QA.QA_Event(order=order, market_data=items.select_code(j)))
    883. trade_mes = Broker.query_orders(Account.account_cookie, 'filled')
    884. res = trade_mes.loc[order.account_cookie, order.realorder_id]
    885. order.trade(res.trade_id, res.trade_price, res.trade_amount, res.trade_time)
    886. Account.settle()
    887. Risk = QA.QA_Risk(Account)
    888. return Account, Risk

    选择2015年作为模型回测时间段。质量mass我选取的是营业收入(TTM)因子,速度计算方法,前两个模型用的是普通计算模式,第三个模型用的是隔夜计算模式。

    1、先用加速度模型回测,每7个交易日计算前7个交易日的加速度,选取靠近最大值的个股。
    account, risk = stock_physics_backtest(start=’2015-01-01’, end=’2016-01-01’, k=300, s=289, code=None, freq=None, freq_n=7,
    n=7, m=0, model=’acceleration’, kind=’oper_rev_ttm’, method=’normal’, cash=1000000)
    运行结束后,运行risk.plot_assets_curve(),效果图如下:

    024ff359-7e80-4108-a460-047c44229a0e.png

    蓝色线是沪深300基准线,当年上涨3%,策略组合为红色线,当年上涨41%

    2、用动能模型回测,每7个交易日计算前7个交易日的动能,选取靠近最大值的个股。
    account, risk = stock_physics_backtest(start=’2015-01-01’, end=’2016-01-01’, k=300, s=289, code=None, freq=None, freq_n=7,
    n=7, m=0, model=’energy’, kind=’oper_rev_ttm’, method=’normal’, cash=1000000)
    再运行risk.plot_assets_curve(),效果图如下:
    ea30150c-cbc9-477b-a7d4-5cfb31d53492.png
    年收益率27%。
    3、用动量模型回测,每7个交易日计算前7个交易日的动量,选取靠近最大值的个股。注意这里计算速度用的是隔夜模式。
    account, risk = stock_physics_backtest(start=’2015-01-01’, end=’2016-01-01’, k=300, s=289, code=None, freq=None, freq_n=7,
    n=7, m=0, model=’momentum’, kind=’oper_rev_ttm’, method=’overnight’, cash=1000000)

    risk.plot_assets_curve()
    效果图如下:
    54cf275c-c53d-4eb0-9590-46a5a34c36e7.png
    年收益为17%

    仅以三个简单模型为例,说明用物理模型进行金融建模是可行的,参数也并未做优化,也有很多错误,请批指正。