Python量化交易系统实战-课程笔记

1,290 阅读21分钟

来源

程序员理财课 Python量化交易系统实战

pandas学习

教程

API

文档

获取股票数据方式

  • 爬虫 缺点:费时费力
  • 免费的数据接口 例如:天勤量化,BigQuant,JoinQuant等等
  • 付费的数据接口 例如:Wind,彭博

jQData学习

文档

API

获取行情数据

  • get_price

获取单季度/年度财务数据

  • get_fundamentals

第一次使用JQData

(1)步骤1.申请试用

申请试用

姓名,邮箱,手机号,验证码填写正确,其他填xxx

账号: 用户名 密码: 密码

(2)步骤2.打开编辑器PyCharm

  • 2.1 创建新项目DeltaFQuant

  • 2.2 新建文件夹

    在项目根路径,新建Python包,data`

  • 2.4 新建文件data/stock.py

from jqdatasdk import *
auth('用户名','密码') #账号是申请时所填写的手机号;密码为聚宽官网登录密码,新申请用户默认为手机号后6位
# XSHE深圳证券交易所,XSHG上海证券交易所
df = get_price('000001.XSHE', end_date='2021-09-17',count=4, frequency='daily', fields=['open','close','high','low','volume','money'])

print(len(df))
print(df)
  • 2.4 安装包

(1)方式1-在编辑器中

(2)方式2-在终端中

pip install jqdatasdk
  • 2.5 运行程序

(3)对照数据

打开同花顺股票软件,查看一天,特定一天,对照

编辑器相关

报错

解决办法,暂时在终端使用pip install jqdatasdk 安装包

网站相关

(1)同花顺

(2)其他

获取股票数据

# 每天数据
df = get_price('000001.XSHE', end_date='2021-02-22',count=100, frequency='daily')

# 每分钟数据
df = get_price('000001.XSHE', end_date='2021-02-22',count=10, frequency='1m')

# 所有股票代码
# 得到:结果4000多只股票的代码,['000001.XSHE', '000002.XSHE', ...]
stocks = list(get_all_securities(['stock']).index)

# 获取多个股票数据
# panel=False表格数据
df = get_price(['600519.XSHG','000001.XSHE'], end_date='2021-02-22',count=10, frequency='daily',panel=False)

# 获取所有股票数据
for stock_code in stocks:
	df = get_price(stock_code, end_date='2021-02-22', count=10, frequency='daily', panel=False)

转换时间序列&汇总统计

from jqdatasdk import *
import pandas as pd
auth('用户名','密码')

"""
日K转周K
"""
# (1)获取日K
df = get_price(security='000001.XSHG', end_date='2021-08-02',count=20, frequency='1d', panel=False)
# 获取这天是星期几
df['weekday'] = df.index.weekday
print(df)
# (2)获取周K(当周的):开盘价(当周第一天)、收盘价(当周最后一天)、最高价(当周)、最低价(当周)
df_week = pd.DataFrame()
df_week['open'] = df['open'].resample('W').first()
df_week['close'] = df['close'].resample('W').last()
df_week['high'] = df['high'].resample('W').max()
df_week['low'] = df['low'].resample('W').min()
"""
汇总统计:统计月成交量、成交额(sum)
"""
df_week['volume(sum)'] = df['volume'].resample('W').sum()
df_week['money(sum)'] = df['money'].resample('W').sum()

print(len(df))
print(df)

结果

获取财务指标数据

from jqdatasdk import *
import pandas as pd
import time

# 设置行列不忽略
pd.set_option('display.max_rows', 100000)
pd.set_option('display.max_columns', 100)

# 账号是申请时所填写的手机号;密码为聚宽官网登录密码,新申请用户默认为手机号后6位
auth('用户名', '密码')

# XSHG-上海证券交易所;XSHE-深圳证券交易所

df = get_fundamentals(query(indicator), statDate="2020")
# print(df)
# df.to_csv('/Users/liurenpeng/PycharmProjects/DeltaFQuant/data/finance/finance2020.csv')

# 基于盈利指标选股:eps,operating_profit,roe,inc_net_profit_year_on_year
df = df[(df['eps'] > 0)
        & (df['operating_profit'] > 123456)
        & (df['roe'] > 100)
        & (df['inc_net_profit_year_on_year'] > 100)]
print(df)

获取股票估值数据

from jqdatasdk import *
import pandas as pd
import datetime
import time

# 设置行列不忽略
pd.set_option('display.max_rows', 100000)
pd.set_option('display.max_columns', 100)

# 账号是申请时所填写的手机号;密码为聚宽官网登录密码,新申请用户默认为手机号后6位
auth('用户名', '密码')

# XSHG-上海证券交易所;XSHE-深圳证券交易所

df = get_fundamentals(query(valuation), statDate = datetime.datetime.today())
print(df)

结果

封装获取股票数据模块

本身

data/stock.py

from jqdatasdk import *
import time
import pandas as pd
import datetime

auth('用户名', '密码')  # 账号是申请时所填写的手机号
# 设置行列不忽略
pd.set_option('display.max_rows', 100000)
pd.set_option('display.max_columns', 1000)

# 全局变量
data_root = 'E:/workspace/DeltaFQuant/data/'


def get_stock_list():
    """
    获取所有A股股票列表
    上海证券交易所.XSHG
    深圳证券交易所.XSHE
    :return: stock_list
    """
    stock_list = list(get_all_securities(['stock']).index)
    return stock_list


def get_single_price(code, time_freq, start_date, end_date):
    """
    获取单个股票行情数据
    :param code:
    :param time_freq:
    :param start_date:
    :param end_date:
    :return:
    """
    # 获取行情数据
    data = get_price(code, start_date=start_date, end_date=end_date,
                     frequency=time_freq, panel=False)  # 获取获得在2015年
    return data


def export_data(data, filename, type):
    """
    导出股票相关数据
    :param data:
    :param filename:
    :param data: 股票数据类型,可以是:price、finance
    :return:
    """
    file_root = data_root + type + '/' + filename + '.csv'
    data.index.names = ['date']
    data.to_csv(file_root)  # 判断一下file是否存在 > 存在:追加 / 不存在:保持
    print('已成功存储至:', file_root)


def transfer_price_freq(data, time_freq):
    """
    将数据转换为制定周期:开盘价(周期第1天)、收盘价(周期最后1天)、最高价(周期内)、最低价(周期内)
    :param data:
    :param time_freq: w-周
    :return:
    """
    df_trans = pd.DataFrame()
    df_trans['open'] = data['open'].resample(time_freq).first()
    df_trans['close'] = data['close'].resample(time_freq).last()
    df_trans['high'] = data['high'].resample(time_freq).max()
    df_trans['low'] = data['low'].resample(time_freq).min()

    return df_trans


def get_single_finance(code, date, statDate):
    """
    获取单个股票财务指标
    :param code:
    :param date:
    :param statDate:
    :return:
    """
    data = get_fundamentals(query(indicator).filter(indicator.code == code), date=date, statDate=statDate)  # 获取财务指标数据
    return data


def get_single_valuation(code, date, statDate):
    """
    获取单个股票估值指标
    :param code:
    :param date:
    :param statDate:
    :return:
    """
    data = get_fundamentals(query(valuation).filter(valuation.code == code), date=date, statDate=statDate)  # 获取财务指标数据
    return data

测试

新建example/stock.py

import data.stock as st

# 获取平安银行的行情数据(日K)
data = st.get_single_price('000001.XSHE', 'daily', '2020-01-01', '2020-02-01')
print(data)
# 获取周K
data = st.transfer_price_freq(data, 'w')
print(data)

结果

(1)print(data)

(2)print(data)

计算交易指标

计算涨跌幅

本身

在获取数据模块data/stock.py添加方法

def calculate_change_pct(data):
    """
    涨跌幅 = (当期收盘价-前期收盘价) / 前期收盘价
    :param data: dataframe,带有收盘价
    :return: dataframe,带有涨跌幅
    """
    data['close_pct'] = (data['close'] - data['close'].shift(1)) \
                        / data['close'].shift(1)
    return data

测试

example/stock.py添加代码

import data.stock as st

# 获取平安银行的行情数据(日K)
data = st.get_single_price('000001.XSHE', 'daily', '2020-01-01', '2020-02-01')
# print(data)

# 计算涨跌幅,验证准确性
data = st.calculate_change_pct(data)
print(data)  # 多了一列close_pct

结果

模拟股票交易

买入,卖出信号

strategy/strategy.py

import data.stock as st
import datetime
import numpy as np

def week_period_strategy(code, time_freq, start_date, end_date):
    """
    周期选股(周四买,周一卖)
    :param code:
    :param time_freq:
    :param start_date:
    :param end_date:
    :return:
    """
    data = st.get_single_price(code, time_freq, start_date, end_date)
    # 新建周期字段
    data['weekday'] = data.index.weekday
    # 周四买入
    data['buy_signal'] = np.where((data['weekday'] == 3), 1, 0)
    # 周一卖出
    data['sell_signal'] = np.where((data['weekday'] == 0), -1, 0)
    data['buy_signal'] = np.where((data['buy_signal'] == 1)
                                  & (data['buy_signal'].shift(1) == 1), 0, data['buy_signal'])
    data['sell_signal'] = np.where((data['sell_signal'] == -1)
                                   & (data['sell_signal'].shift(1) == -1), 0, data['sell_signal'])
    data['signal'] = data['buy_signal'] + data['sell_signal']
    return data

if __name__ == '__main__':
    df = week_period_strategy('000001.XSHE', 'daily', None, datetime.date.today())
    print(df)

结果

image-20210919181458339

计算持仓收益

本身

(1)完善data/stock.py中的get_single_price方法

def get_single_price(code, time_freq, start_date, end_date):
    """
    获取单个股票行情数据
    :param code: 
    :param time_freq: 
    :param start_date: 
    :param end_date: 
    :return: 
    """
+    # 如果start_date=None,默认为从上市日期开始
+    if start_date is None:
+        start_date = get_security_info(code).start_date
+    if end_date is None:
+        end_date = datetime.datetime.today()
    # 获取行情数据
    data = get_price(code, start_date=start_date, end_date=end_date,
                     frequency=time_freq, panel=False)  # 获取获得在2015年
    return data

(2)在strategy/strategy.py

import data.stock as st
import numpy as np
import matplotlib.pyplot as plt
import datetime

def compose_signal(data):
    """
    整合信号
    :param data:
    :return:
    """
    # 本行buy_signal为1,当上一行也为1,本行改为0,否则不变
    data['buy_signal'] = np.where((data['buy_signal'] == 1)
                                  & (data['buy_signal'].shift(1) == 1), 0, data['buy_signal'])
    # 本行sell_signal为-1,当上一行也为-1,本行改为0,否则不变
    data['sell_signal'] = np.where((data['sell_signal'] == -1)
                                   & (data['sell_signal'].shift(1) == -1), 0, data['sell_signal'])
    # 有信号的行,要么买,要么卖
    data['signal'] = data['buy_signal'] + data['sell_signal']
    return data

def calculate_prof_pct(data):
    """
    计算单次收益率:开仓、平仓(开仓的全部股数)
    :param data:
    :return:
    """
    # 筛选,有信号的行
    data = data[data['signal'] != 0] 
    # 单次收益率 = (本次收盘价 - 上次收盘价) / 上次收盘价
    data['profit_pct'] = (data['close'] - data['close'].shift(1)) / data['close'].shift(1)
    # 筛选卖出的行
    data = data[data['signal'] == -1]
    return data

测试

strategy/strategy.py中,完善week_period_strategy方法

def week_period_strategy(code, time_freq, start_date, end_date):
    """
    周期选股(周四买,周一卖)
    :param code:
    :param time_freq:
    :param start_date:
    :param end_date:
    :return:
    """
    data = st.get_single_price(code, time_freq, start_date, end_date)
    # 新建周期字段
    data['weekday'] = data.index.weekday
    # 周四买入
    data['buy_signal'] = np.where((data['weekday'] == 3), 1, 0)
    # 周一卖出
    data['sell_signal'] = np.where((data['weekday'] == 0), -1, 0)
-	data['buy_signal'] = np.where((data['buy_signal'] == 1)
-                                  & (data['buy_signal'].shift(1) == 1), 0, data['buy_signal'])
-    data['sell_signal'] = np.where((data['sell_signal'] == -1)
-                                   & (data['sell_signal'].shift(1) == -1), 0, data['sell_signal'])
-    data['signal'] = data['buy_signal'] + data['sell_signal']
+    data = compose_signal(data)  # 整合信号
+    data = calculate_prof_pct(data)  # 计算收益
    return data


if __name__ == '__main__':
    df = week_period_strategy('000001.XSHE', 'daily', None, datetime.date.today())
-    print(df)
+    print(df[['close', 'signal', 'profit_pct']])
+    print(df.describe())
+    df['profit_pct'].plot()
+    plt.show()

结果

(1)print(df[['close', 'signal', 'profit_pct']])

(2)`print(df.describe())

(3)plt.show()

计算累计收益率

思路

(1)理财产品

时间收益率
第1天3%
第2天2%
第3天5%
第4天6%

(2)累计收益

时间累计收益率
第1天1*(1+3%)=¥103
第2天以上*(1+2%)=¥103+2.06
第3天以上*(1+5%)= 以上 * 收益
第4天以上*(1+6%)= 以上 * 收益
第...天(1+当天收益率)的累计乘积-1

本身

strategy/strategy.py中添加calculate_cum_prof方法

import pandas as pd

def calculate_cum_prof(data):
    """
    计算累计收益率
    :param data: dataframe
    :return:
    """
    data['cum_profit'] = pd.DataFrame(1 + data['profit_pct']).cumprod() - 1
    return data

测试

strategy/strategy.py

def week_period_strategy(code, time_freq, start_date, end_date):
    """
    周期选股(周四买,周一卖)
    :param code:
    :param time_freq:
    :param start_date:
    :param end_date:
    :return:
    """
    data = st.get_single_price(code, time_freq, start_date, end_date)
    # 新建周期字段
    data['weekday'] = data.index.weekday
    # 周四买入
    data['buy_signal'] = np.where((data['weekday'] == 3), 1, 0)
    # 周一卖出
    data['sell_signal'] = np.where((data['weekday'] == 0), -1, 0)

    data = compose_signal(data)  # 整合信号
    data = calculate_prof_pct(data)  # 计算收益
+    data = calculate_cum_prof(data)  # 计算累计收益率
    return data

if __name__ == '__main__':
    df = week_period_strategy('000001.XSHE', 'daily', None, datetime.date.today())
-    print(df[['close', 'signal', 'profit_pct']])
+    print(df[['close', 'signal', 'profit_pct','cum_profit']])
    print(df.describe())
    df['profit_pct'].plot()
    plt.show()

结果

计算风险收益指标

最大回测

strategy/strategy.py

def caculate_max_drawdown(data):
    """
    计算最大回撤比
    :param data:
    :return:
    """
    # 选取时间周期(时间窗口)
    window = 252
    # 选取时间周期中的最大净值
    data['roll_max'] = data['close'].rolling(window=window, min_periods=1).max()
    # 计算当天的回撤比 = (谷值 — 峰值)/峰值 = 谷值/峰值 - 1
    data['daily_dd'] = data['close'] / data['roll_max'] - 1
    # 选取时间周期内最大的回撤比,即最大回撤
    data['max_dd'] = data['daily_dd'].rolling(window, min_periods=1).min()

    return data

def week_period_strategy(code, time_freq, start_date, end_date):
    """
    周期选股(周四买,周一卖)
    :param code:
    :param time_freq:
    :param start_date:
    :param end_date:
    :return:
    """
    data = st.get_single_price(code, time_freq, start_date, end_date)
    # 新建周期字段
    data['weekday'] = data.index.weekday
    # 周四买入
    data['buy_signal'] = np.where((data['weekday'] == 3), 1, 0)
    # 周一卖出
    data['sell_signal'] = np.where((data['weekday'] == 0), -1, 0)

    data = compose_signal(data)  # 整合信号
    data = calculate_prof_pct(data)  # 计算收益
    data = calculate_cum_prof(data)  # 计算累计收益率
+    data = caculate_max_drawdown(data)  # 最大回撤
    return data

夏普比率

strategy/strategy.py

def calculate_sharpe(data):
    """
    计算夏普比率,返回的是年化的夏普比率
    :param data: dataframe, stock
    :return: float
    """
    # 公式:sharpe = (回报率的均值 - 无风险利率) / 回报率的标准差
    daily_return = data['close'].pct_change()
    avg_return = daily_return.mean()
    sd_reutrn = daily_return.std()
    # 计算夏普:每日收益率 * 252 = 每年收益率
    sharpe = avg_return / sd_reutrn
    sharpe_year = sharpe * np.sqrt(252)
    return sharpe, sharpe_year

if __name__ == '__main__':
    # 计算夏普比率
    df = st.get_single_price('000001.XSHE', 'daily', '2006-01-01', '2021-01-01')
    sharpe = calculate_sharpe(df)
    print(sharpe)    

结果

image-20210920052557725

实战

比较3只股票的夏普指数

(1)将strategy/strategy.py修改为strategy/base.py

(2)创建example/comp_sharpe_ratio.py

import data.stock as st
import strategy.base as stb
import pandas as pd
import matplotlib.pyplot as plt

# 获取3只股票的数据:比亚迪、宁德时代、隆基
codes = ['002594.XSHE', '300750.XSHE', '601012.XSHG']

# 容器:存放夏普
sharpes = []
for code in codes:
    data = st.get_single_price(code, 'daily', '2018-10-01', '2021-01-01')
    print(data.head())

    # 计算每只股票的夏普比率
    daily_sharpe, annual_sharpe = stb.calculate_sharpe(data)
    sharpes.append([code, annual_sharpe])  # 存放 [[c1,s1],[c2,s2]..]
    print(sharpes)

# 可视化3只股票并比较
sharpes = pd.DataFrame(sharpes, columns=['code', 'sharpe']).set_index('code')
print(sharpes)

# 绘制bar图
sharpes.plot.bar(title='Compare Annual Sharpe Ratio')
plt.xticks(rotation=30)
plt.show()

结果

(1)print(data.head())

(2)print(sharpes)

(3)print(sharpes)

(4)plt.show()

设计交易策略

本地化股票数据

思路

  • 1.获取股票列表
  • 2.依次存储股票数据
  • 3.每天以增量形式更新数据
    • 如何追加数据:df.to_csv(mode='a')
    • 重复获取了怎么办:df.drop_duplicates
    • 缺少数据怎么办:startdate=最后一个日期

步骤

本身

(1)在data/stock.py中完善export_data方法

- def export_data(data, filename, type):
+ def export_data(data, filename, type, mode=None):
    # 新增mode参数
    """
    导出股票相关数据
    :param data:
    :param filename:
    :param type: 股票数据类型,可以是:price、finance
+    :param mode: a代表追加,none代表默认w写入
    :return:
    """
    file_root = data_root + type + '/' + filename + '.csv'
    data.index.names = ['date']
-    data.to_csv(file_root)
+    if mode == 'a':
+        data.to_csv(file_root, mode=mode, header=False)
+        # 删除重复值
+        data = pd.read_csv(file_root)  # 读取数据
+        data = data.drop_duplicates(subset=['date'])  # 以日期列为准
+        data.to_csv(file_root, index=False)  # 重新写入
+    else:
+        data.to_csv(file_root)  # 判断一下file是否存在 > 存在:追加 / 不存在:保持

    print('已成功存储至:', file_root)

(2)在data/stock.py中新增update_daily_price方法

import os

def update_daily_price(stock_code, type='price'):
    # 3.1是否存在文件:不存在-重新获取,存在->3.2
    file_root = data_root + type + '/' + stock_code + '.csv'
    if os.path.exists(file_root):  # 如果存在对应文件
        # 3.2获取增量数据(code,startsdate=对应股票csv中最新日期,enddate=今天)
        startdate = pd.read_csv(file_root, usecols=['date'])['date'].iloc[-1]
        df = get_single_price(stock_code, 'daily', startdate, datetime.datetime.today())
        # 3.3追加到已有文件中
        export_data(df, stock_code, 'price', 'a')
    else:
        # 重新获取该股票行情数据
        df = get_single_price(stock_code, 'daily')
        export_data(df, stock_code, 'price')

    print("股票数据已经更新成功:", stock_code)

(4)在data/stock.py中新增init_db方法

def init_db():
    '''
    初始化股票数据库
    :return:
    '''
    # 1.获取所有股票代码
    stocks = get_stock_list()
    # 2.存储到csv文件中
    for code in stocks:
        df = get_single_price(code, 'daily')
        export_data(df, code, 'price')
        print(code)
        print(df.head())

(5)在data/stock.py中完善方法

- def get_single_price(code, time_freq, start_date, end_date):
+ def get_single_price(code, time_freq, start_date=None, end_date=None):
    # 新增start_date=None, end_date=None,支持默认值
    """
    获取单个股票行情数据
    :param code: 
    :param time_freq: 
    :param start_date: 
    :param end_date: 
    :return: 
    """
    # 如果start_date=None,默认为从上市日期开始
    if start_date is None:
        start_date = get_security_info(code).start_date
    if end_date is None:
        end_date = datetime.datetime.today()
    # 获取行情数据
    data = get_price(code, start_date=start_date, end_date=end_date,
                     frequency=time_freq, panel=False)  # 获取获得在2015年
    return data

测试

(1)新建data/price目录

(2)新建example/stock_upd_database.py

import data.stock as st
# 初始化变量
code = '000002.XSHE'
# 测试存储一只股票的数据
data = st.get_single_price(code=code,
						  time_freq='daily',
                            start_date='2021-01-01',
                            end_date='2021-02-01')
# 存入csv
st.export_data(data=data, filename=code, type='price')

(3)在example/stock_upd_database.py

# 1.获取所有股票代码
stocks = st.get_stock_list()
# 2.存储到csv文件中
for code in stocks:
     df = st.get_single_price(code, 'daily')
     st.export_data(df)
# 3.每日更新数据
# for code in stocks:
# st.update_daily_price(code, 'price')

从本地读取数据

本身

(1)在data/stock.py中新增get_csv_price方法

def get_csv_price(code, start_date, end_date):
    """
    获取本地数据,且顺便完成数据更新工作
    :param code: str,股票代码
    :param start_date: str,起始日期
    :param end_date: str,起始日期
    :return: dataframe
    """
    # 使用update直接更新
    update_daily_price(code)
    # 读取数据
    file_root = data_root + 'price/' + code + '.csv'
    # index_col:读取的时候指定索引值
    data = pd.read_csv(file_root, index_col='date')
    # print(data)
    # 根据日期筛选股票数据
    return data[(data.index >= start_date) & (data.index <= end_date)]

测试

example/stock.py中,新增代码

import data.stock as st

# 本地读取数据
data = st.get_csv_price('000002.XSHE', '2020-01-01', '2020-02-01')
print(data)
exit()  # 终端程序

均线策略

什么均线

代表N日股价平均走势,例如5日均线 = (第1天价格+...+第5天价格)/5天

策略实现思路

  • 1.获取标的行情
  • 2.计算技术指标(移动平均线:5日,10日)
  • 3.生成交易信号(金叉买入,死叉卖出)
  • 4.计算收益率(单次收益率,累计收益率)
  • 5.寻找最优参数(均线周期,投资标的)
  • 6.与市场基准比较(沪深300,上证,中证500)
  • 7.策略评估(收益,夏普,波动率,回撤,胜率)

生成交易信号

本身

新建strategy/ma_strategy.py

import data.stock as st
import strategy.base as strat
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt


def ma_strategy(data, short_window=5, long_window=20):
    """
    双均线策略
    :param data: dataframe, 投资标的行情数据(必须包含收盘价)
    :param short_window: 短期n日移动平均线,默认5
    :param long_window: 长期n日移动平均线,默认20
    :return: 
    """
    print("==========当前周期参数对:", short_window, long_window)

    data = pd.DataFrame(data)
    # 计算技术指标:ma短期、ma长期
    data['short_ma'] = data['close'].rolling(window=short_window).mean()
    data['long_ma'] = data['close'].rolling(window=long_window).mean()

    # 生成信号:金叉买入、死叉卖出
    data['buy_signal'] = np.where(data['short_ma'] > data['long_ma'], 1, 0)
    data['sell_signal'] = np.where(data['short_ma'] < data['long_ma'], -1, 0)
    # 预览
    # print(data[['close', 'short_ma', 'long_ma', 'buy_signal', 'sell_signal']])

    # 过滤信号:st.compose_signal
    data = strat.compose_signal(data)
    # 预览
    # print(data[['close', 'short_ma', 'long_ma', 'signal']])
    
    # 删除多余的columns
    data.drop(labels=['buy_signal', 'sell_signal'], axis=1)
    return data

测试

strategy/ma_strategy.py

if __name__ == '__main__':
    # 股票列表
    df = st.get_single_price('000001.XSHE', 'daily', '2016-01-01', '2021-01-01')
    df = ma_strategy(df)  # 调用双均线策略
    # 筛选有信号点
    df = df[df['signal'] != 0]
    # 预览数据
    print("开仓次数", int(len(df)/2))
    print(df[['close', 'short_ma', 'long_ma', 'signal']])

结果:

计算信号收益率

本身

(1)在strategy/base.py中,完善代码

def calculate_prof_pct(data):
    """
    计算单次收益率:开仓、平仓(开仓的全部股数)
    :param data:
    :return:
    """
    - data = data[data['signal'] != 0]
    - data['profit_pct'] = (data['close'] - data['close'].shift(1)) / data['close'].shift(1)    
    # 筛选信号不为0的,并且计算涨跌幅
    # 单次收益率 = (本次收盘价 - 上次收盘价) / 上次收盘价
    + data.loc[data['signal'] != 0, 'profit_pct'] = data['close'].pct_change()
    data = data[data['signal'] == -1]  # 筛选平仓后的数据:单次收益
    return data

(2)在strategy/ma_strategy.py完善代码

import data.stock as st
import strategy.base as strat
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt


def ma_strategy(data, short_window=5, long_window=20):
    """
    双均线策略
    :param data: dataframe, 投资标的行情数据(必须包含收盘价)
    :param short_window: 短期n日移动平均线,默认5
    :param long_window: 长期n日移动平均线,默认20
    :return: 
    """
    print("==========当前周期参数对:", short_window, long_window)

    data = pd.DataFrame(data)
    # 计算技术指标:ma短期、ma长期
    data['short_ma'] = data['close'].rolling(window=short_window).mean()
    data['long_ma'] = data['close'].rolling(window=long_window).mean()

    # 生成信号:金叉买入、死叉卖出
    data['buy_signal'] = np.where(data['short_ma'] > data['long_ma'], 1, 0)
    data['sell_signal'] = np.where(data['short_ma'] < data['long_ma'], -1, 0)
    # print(data[['close', 'short_ma', 'long_ma', 'buy_signal', 'sell_signal']])

    # 过滤信号:st.compose_signal
    data = strat.compose_signal(data)
    # print(data[['close', 'short_ma', 'long_ma', 'signal']])

    # 计算单次收益
    + data = strat.calculate_prof_pct(data)
    # print(data.describe())

    # 计算累计收益
    + data = strat.calculate_cum_prof(data)

    # 删除多余的columns
    data.drop(labels=['buy_signal', 'sell_signal'], axis=1)

    # 数据预览
    - print(data[['close', 'short_ma', 'long_ma', 'signal']])
    + print(data[['close', 'short_ma', 'long_ma', 'signal', 'cum_profit']])

    return data

测试

if __name__ == '__main__':
    # 股票列表
    stocks = ['000001.XSHE', '000858.XSHE', '002594.XSHE']

    # 存放累计收益率
    cum_profits = pd.DataFrame()
    # 循环获取数据
    for code in stocks:
        df = st.get_single_price(code, 'daily', '2016-01-01', '2021-01-01')
        df = ma_strategy(df)  # 调用双均线策略
        # .reset_index(drop=True):不要索引
        cum_profits[code] = df['cum_profit'].reset_index(drop=True)  # 存储累计收益率
        # 折线图
        df['cum_profit'].plot(label=code)
        # 筛选有信号点
        # df = df[df['signal'] != 0]
        # 预览数据
        print("开仓次数:", int(len(df)))
        # print(df[['close', 'signal', 'pro   、fit_pct', 'cum_profit']])

    # 预览
    print(cum_profits)
    # 可视化
    cum_profits.plot()
    plt.legend() # 图加标识
    plt.title('Comparison of Ma Strategy Profits')
    plt.show()

结果

(1)print("开仓次数:", int(len(df)))

(2)print(cum_profits)

image-20210924222736544

(3)cum_profits.plot()

(4)plt.show()

验证策略可靠性

思路

  • 评估指标: 胜率,年化收益,最大回撤,夏普
  • 假设检验: 收益率>0是否为大概率时间

假设检验

先提出一个假设,通过多次实验,计算不同结果出现的频数和频率,来决定是否接受或者拒绝这个假设,从而验证这个现象是否随机(偶然)

思路

周期策略: 周四买入,周一卖出,计算分布图,统计最小收益率(min),最大收益率(max),总共数据点(count),均值(mean),样本标准差(std) 假设检验: t-test H0: 样本均值 = 理论均值 -> 每次收益率的均值 = 0 H1: 样本均值 > 理论均值 -> 每次收益率的均值 > 0 t统计量 -> 计算公式如下 ​ t值越大,相识度越低 ​ t值越小,相识度越高

查找p值:

步骤

本身

新建strategy/statistical_test.py

import data.stock as st
import strategy.ma_strategy as ma
import matplotlib.pyplot as plt
from scipy import stats


def ttest(data_return):
    """
    对策略收益进行t检验
    :param strat_return: dataframe,单次收益率
    :return: float,t值和p值
    """
    # 调用假设检验ttest函数:scipy
    t, p = stats.ttest_1samp(data_return, 0, nan_policy='omit')

    # 判断是否与理论均值有显著性差异:α=0.05
    p_value = p / 2  # 获取单边p值

    # 打印
    print("t-value:", t)
    print("p-value:", p_value)
    print("是否可以拒绝[H0]收益均值=0:", p_value < 0.05)

    return t, p_value
测试
if __name__ == '__main__':
    # 股票列表
    stocks = ['000001.XSHE', '000858.XSHE', '002594.XSHE']
    for code in stocks:
        print(code)
        df = st.get_single_price(code, 'daily', '2016-12-01', '2021-01-01')
        df = ma.ma_strategy(df)  # 调用双均线策略

        # 策略的单次收益率
        returns = df['profit_pct']
        print(returns)

        # 绘制一下分布图用于观察
        plt.hist(returns, bins=30)
        plt.show()

        # 对多个股票进行计算、测试
        ttest(returns)
结果

(1)print(returns)

(2) plt.show()

(3)

寻找最优参数

新建example/find_best_param.py

import strategy.ma_strategy as ma
import data.stock as st
import pandas as pd

# 参数1:股票池
# stocks = ['000001.XSHE']
data = st.get_csv_price('000001.XSHE', '2016-01-01', '2021-01-01')
# 参数2:周期参数
params = [5, 10, 20, 60, 120, 250]
# 存放参数与收益
res = []
# 匹配并计算不同的周期参数对:5-10,5-20 …… 120-250
for short in params:
    for long in params:
        if long > short:
            data_res = ma.ma_strategy(data=data, short_window=short,
                                      long_window=long)
            # 获取周期参数,及其对应累计收益率
            cum_profit = data_res['cum_profit'].iloc[-1]  # 获取累计收益率最终数据
            res.append([short, long, cum_profit])  # 将参数放入结果列表

# 将结果列表转换为df,并找到最优参数
res = pd.DataFrame(res, columns=['short_win', 'long_win', 'cum_profit'])
# 排序
res = res.sort_values(by='cum_profit', ascending=False)  # 按收益倒序排列
print(res)

结果

image-20210925113836151

动量策略

概述

  • 什么是动量策略 预先对股票收益交易量设定过滤准则,当股票收益或股票收益和交易量同时满足过滤准则就买入(做多)或卖出(做空)股票的投资策略

  • 动量策略的设计思路 正向策略:涨的还会涨,跌的还会跌,买入涨最多的,卖出跌最多的,利用市场对信息的反应不足

    反向策略:涨太多会跌,跌太多了会涨,

  • 动量策略的实现步骤

    • (1)确定交易对象:股票池,考虑流动性(沪深300,创业板)
    • (2)选定业绩评价周期:过去1-12个月
    • (3)计算形成期收益率:过去N个月的收益率
    • (4)对收益率进行排序:最佳赢家组合,最差输家组合
    • (5)确定持仓/换仓周期:1个月,可自定义测算
    • (6)连续或间隔一段时期,不断重复2-5行为
    • (7)计算动量/反向策略各持有期的回报率
    • (8)计算t/p统计值,判断是否存在动态效益

筛选股票池

概述

  • 如何确定股票池

    流动性: 成交活跃,买入卖出顺畅

    基本面: 行业,营收,盈利增速,现金流,负债

    标的价格: 1手起买起卖

    股票池初始值: 沪深300持有个股

本身

(1)在data/stock.py中新增方法

from jqdatasdk import *
def get_index_list(index_symbol='000300.XSHG'):
    """
    获取指数成分股,指数代码查询:https://www.joinquant.com/indexData
    :param index_symbol: 指数的代码,默认沪深300
    :return: list,成分股代码
    """
    stocks = get_index_stocks(index_symbol)
    return stocks

(2)新建strategy/momentum_strategy.py

import data.stock as st
import pandas as pd
import numpy as np
import strategy.base as base
import matplotlib.pyplot as plt

def get_data(start_date, end_date, index_symbol='000300.XSHG'):
    """
    获取股票收盘价数据,并拼接为一个df
    :param start_date: str
    :param end_date: str
    :param use_cols: list
    :param index_symbol: str
    :return data_concat: df,拼接后的数据表
    """
    # 获取股票列表代码:沪深300持有个股、创业板、上证
    stocks = st.get_index_list(index_symbol)
    # 拼接收盘价数据
    data_concat = pd.DataFrame()
    # 获取股票数据
    for code in stocks[0:9]:
        data = st.get_csv_price(code, start_date, end_date)
        # 预览股票数据
        print("===================", code)
        print(data.tail())

def momentum():
    return 0

测试

strategy/momentum_strategy.py

if __name__ == '__main__':
    # 测试:获取沪深300个股数据
    data = get_data('2016-01-01', '2021-04-04')

结果

image-20210925114147945

计算动量因子

本身

(1)在data/stock.py中完善get_csv_price方法

- def get_csv_price(code, start_date, end_date):
+ def get_csv_price(code, start_date, end_date, columns=None):
    """
    获取本地数据,且顺便完成数据更新工作
    :param code: str,股票代码
    :param start_date: str,起始日期
    :param end_date: str,起始日期
    :param columns: list,选取的字段
    :return: dataframe
    """
    # 使用update直接更新
    update_daily_price(code)
    # 读取数据
    file_root = data_root + 'price/' + code + '.csv'
-    data = pd.read_csv(file_root, index_col='date')
+    if columns is None:
+        # index_col:读取的时候指定索引值
+        data = pd.read_csv(file_root, index_col='date')
+    else:
+        data = pd.read_csv(file_root, usecols=columns, index_col='date')
    # print(data)
    # 根据日期筛选股票数据
    return data[(data.index >= start_date) & (data.index <= end_date)]

(2)在strategy/momentum_strategy.py

import data.stock as st
import pandas as pd
import numpy as np
import strategy.base as base
import matplotlib.pyplot as plt

- def get_data(start_date, end_date, index_symbol='000300.XSHG'):
+ def get_data(start_date, end_date, use_cols, index_symbol='000300.XSHG'):
    """
    获取股票收盘价数据,并拼接为一个df
    :param start_date: str
    :param end_date: str
    :param use_cols: list
    :param index_symbol: str
    :return data_concat: df,拼接后的数据表
    """
    # 获取股票列表代码:沪深300持有个股、创业板、上证
    stocks = st.get_index_list(index_symbol)
    # 拼接收盘价数据
    data_concat = pd.DataFrame()
    # 获取股票数据
-    for code in stocks[0:9]:
+    for code in stocks:
-        data = st.get_csv_price(code, start_date, end_date)        
+		data = st.get_csv_price(code, start_date, end_date, use_cols)
        # 拼接多个股票的收盘价:日期 股票A收盘价 股票B收盘价 ...
+        data.columns = [code]
+        data_concat = pd.concat([data_concat, data], axis=1)
    	# 预览股票数据
-		print("===================", code) 
-		print(data.tail())
+    	# print(data_concat.tail())
+    return data_concat

- def momentum():
+ def momentum(data_concat, shift_n=1, top_n=4):
-   return 0 
+    """
+    :param data_concat: df
+    :param shift_n: int,表示业绩统计周期(单位:月)
+    :return:
+    """
+    # 转换时间频率:日->月
+    data_concat.index = pd.to_datetime(data_concat.index)
+    data_month = data_concat.resample('M').last()
+    # 计算过去N个月的收益率 = 期末值/期初值 - 1 =(期末-期初)/ 期初
+    # optional:对数收益率 = log(期末值 / 期初值)
+    print(data_month.head())
+    shift_return = data_month / data_month.shift(shift_n) - 1
+    print(shift_return.head())
+    return shift_return

测试

if __name__ == '__main__':
    # 测试:获取沪深300个股数据
-    data = get_data('2016-01-01', '2021-04-04')
+    data = get_data('2016-01-01', '2021-04-04', ['date', 'close'])
+    # 测试:动量策略
+    returns = momentum(data)

结果

生成交易信号

本身

strategy/momentum_strategy.py

import data.stock as st
import pandas as pd
import numpy as np
import strategy.base as base
import matplotlib.pyplot as plt

def get_data(start_date, end_date, use_cols, index_symbol='000300.XSHG'):
    """
    获取股票收盘价数据,并拼接为一个df
    :param start_date: str
    :param end_date: str
    :param use_cols: list
    :param index_symbol: str
    :return data_concat: df,拼接后的数据表
    """
    # 获取股票列表代码:沪深300持有个股、创业板、上证
    stocks = st.get_index_list(index_symbol)
    # 拼接收盘价数据
    data_concat = pd.DataFrame()
    # 获取股票数据
-    for code in stocks:
+    for code in stocks[0:9]:
        data = st.get_csv_price(code, start_date, end_date, use_cols)
        # 拼接多个股票的收盘价:日期 股票A收盘价 股票B收盘价 ...
        data.columns = [code]
        data_concat = pd.concat([data_concat, data], axis=1)
    # 预览股票数据
    # print(data_concat.tail())
    return data_concat

def momentum(data_concat, shift_n=1, top_n=4):
    """
    :param data_concat: df
    :param shift_n: int,表示业绩统计周期(单位:月)
    :return:
    """
    # 转换时间频率:日->月
    data_concat.index = pd.to_datetime(data_concat.index)
    data_month = data_concat.resample('M').last()
    # 计算过去N个月的收益率 = 期末值/期初值 - 1 =(期末-期初)/ 期初
    # optional:对数收益率 = log(期末值 / 期初值)
-    print(data_month.head())
    shift_return = data_month / data_month.shift(shift_n) - 1
-    print(shift_return.head())    
+    print(shift_return)
-    return shift_return

+    # 生成交易信号:收益率排前n的>赢家组合>买入1,排最后n个>输家>卖出-1
+    buy_signal = get_top_stocks(shift_return, top_n)
+    sell_signal = get_top_stocks(-1 * shift_return, top_n)
+    signal = buy_signal - sell_signal
+    print(signal)

def get_top_stocks(data, top_n):
    """
    找到前n位的极值,并转换为信号返回
    :param data: df
    :param top_n: int, 表示要产生信号的个数
    :return signals: df, 返回0-1信号数据表
    """
    # 初始化信号容器
    signals = pd.DataFrame(index=data.index, columns=data.columns)
    # 对data的每一行进行遍历,找里面的最大值,并利用bool函数标注0或1信号
    for index, row in data.iterrows():
        signals.loc[index] = row.isin(row.nlargest(top_n)).astype(np.int)
    return signals

测试

if __name__ == '__main__':
    # 测试:获取沪深300个股数据
    data = get_data('2016-01-01', '2021-04-04', ['date', 'close'])
    # 测试:动量策略
    returns = momentum(data)

结果

(1)print(shift_return)

(2)print(signal)

计算投资组合收益率

概述

  • 如何计算

= (金额1收益率1+...+金额n收益率n)/(金额1+...+n) = (收益率1+n)/n

本身

(1)在strategy\base.py中新增caculate_portfolio_return方法

def caculate_portfolio_return(data, signal, n):
    """
    计算组合收益率
    :param data: dataframe
    :param signal: dataframe
    :param n: int
    :return returns: dataframe
    """
    returns = data.copy()
    # 投组收益率(等权重)= 收益率之和 / 股票个数
    returns['profit_pct'] = (signal * returns.shift(-1)).T.sum() / n
    returns = calculate_cum_prof(returns)
    return returns.shift(1)  # 匹配对应的交易月份

(2)在strategy/momentum_strategy.py

def momentum(data_concat, shift_n=1, top_n=4):
    """
    :param data_concat: df
    :param shift_n: int,表示业绩统计周期(单位:月)
    :return:
    """
    # 转换时间频率:日->月
    data_concat.index = pd.to_datetime(data_concat.index)
    data_month = data_concat.resample('M').last()
    # 计算过去N个月的收益率 = 期末值/期初值 - 1 =(期末-期初)/ 期初
    # optional:对数收益率 = log(期末值 / 期初值)
    shift_return = data_month / data_month.shift(shift_n) - 1
-    print(shift_return)
+    print(shift_return.head())
+    # print(shift_return.shift(-1))

    # 生成交易信号:收益率排前n的>赢家组合>买入1,排最后n个>输家>卖出-1
    buy_signal = get_top_stocks(shift_return, top_n)
    sell_signal = get_top_stocks(-1 * shift_return, top_n)
    signal = buy_signal - sell_signal
-    print(signal)
+    print(signal.head())

+    # 计算投资组合收益率
+    returns = base.caculate_portfolio_return(shift_return, signal, top_n * 2)
+    print(returns.head())
    
+    return returns

测试

if __name__ == '__main__':
    # 测试:获取沪深300个股数据
    data = get_data('2016-01-01', '2021-04-04', ['date', 'close'])
    # 测试:动量策略
    returns = momentum(data)
+    # 可视化每个月的收益率
+    returns['cum_profit'].plot()
+    plt.show()

结果

(1)print(shift_return.head())

(2)print(signal.head())

image-20210925180319510

(3)print(returns.head())

(4)plt.show()

打印策略评估指标

本身

(1)在strategy\base.py中完善caculate_max_drawdown方法和新增evaluate_strategy方法

- def caculate_max_drawdown(data):
+ def caculate_max_drawdown(data, window=252):
    """
    计算最大回撤比
    :param data:
    :param window: int, 时间窗口设置,默认为252(日k)
    :return:
    """
-    # 选取时间周期(时间窗口)
-    window = 252
+    # 模拟持仓金额:投入的总金额 *(1+收益率)
+    data['close'] = 10000 * (1 + data['cum_profit'])
    # 选取时间周期中的最大净值
    data['roll_max'] = data['close'].rolling(window=window, min_periods=1).max()
    # 计算当天的回撤比 = (谷值 — 峰值)/峰值 = 谷值/峰值 - 1
    data['daily_dd'] = data['close'] / data['roll_max'] - 1
    # 选取时间周期内最大的回撤比,即最大回撤
    data['max_dd'] = data['daily_dd'].rolling(window, min_periods=1).min()

    return data

def evaluate_strategy(data):
    """
    评估策略收益表现
    :param data: dataframe, 包含单次收益率数据
    :return results: dict, 评估指标数据
    """
    # 评估策略效果:总收益率、年化收益率、最大回撤、夏普比
    data = calculate_cum_prof(data)

    # 获取总收益率
    total_return = data['cum_profit'].iloc[-1]
    # 计算年化收益率(每月开仓)
    annual_return = data['profit_pct'].mean() * 12

    # 计算近一年最大回撤
    data = caculate_max_drawdown(data, window=12)
    # print(data)
    # 获取近一年最大回撤
    max_drawdown = data['max_dd'].iloc[-1]

    # 计算夏普比率
    sharpe, annual_sharpe = calculate_sharpe(data)

    # 放到dict中
    results = {'总收益率': total_return, '年化收益率': annual_return,
               '最大回撤': max_drawdown, '夏普比率': annual_sharpe}

    # 打印评估指标
    for key, value in results.items():
        print(key, value)

    return data

(2)在strategy/momentum_strategy.py

def momentum(data_concat, shift_n=1, top_n=4):
    """
    :param data_concat: df
    :param shift_n: int,表示业绩统计周期(单位:月)
    :return:
    """
    # 转换时间频率:日->月
    data_concat.index = pd.to_datetime(data_concat.index)
    data_month = data_concat.resample('M').last()
    # 计算过去N个月的收益率 = 期末值/期初值 - 1 =(期末-期初)/ 期初
    # optional:对数收益率 = log(期末值 / 期初值)
    shift_return = data_month / data_month.shift(shift_n) - 1
    print(shift_return.head())
    # print(shift_return.shift(-1))

    # 生成交易信号:收益率排前n的>赢家组合>买入1,排最后n个>输家>卖出-1
    buy_signal = get_top_stocks(shift_return, top_n)
    sell_signal = get_top_stocks(-1 * shift_return, top_n)
    signal = buy_signal - sell_signal
    print(signal.head())

    # 计算投资组合收益率
    returns = base.caculate_portfolio_return(shift_return, signal, top_n * 2)
    print(returns.head())

+    # 评估策略效果:总收益率、年化收益率、最大回撤、夏普比
+    returns = base.evaluate_strategy(returns)

+    # 数据预览
+    # print(data_month.head())
    return returns

测试

新建results文件夹

if __name__ == '__main__':
    # 测试:获取沪深300个股数据
    data = get_data('2016-01-01', '2021-04-04', ['date', 'close'])
    # 测试:动量策略
    returns = momentum(data)
+    # 存储结果
+    returns.to_csv('E:/workspace/DeltaFQuant/strategy/results/momentum.csv')
    # 可视化每个月的收益率
-    returns['cum_profit'].plot()
+    # returns['cum_profit'].plot()
-    plt.show()
+    # plt.show()

结果

(1)print(shift_return.head())

(2)print(signal.head())

(3)print(returns.head())

(4)print(key, value)

拓展:调整投资组合权重

概念

  • 什么是

    投入金额比重

  • 如何调整

    按收益率算同比

数据回测与优化

概念

为什么回测与实盘有差异?

  • (1)未来函数

    解决办法:

    • 方式1:第二天买入

      存在问题:

      • 错过最佳入场时机

      • 成本变高了

      • 方式2:用今天的分时价格代替日收盘价

      存在问题:

      • 代码逻辑变复杂
  • (2)滑点

    一笔交易或挂单交易中所要求的价格和实际订单执行或成交价格之间的差异

    解决办法:

    • 方式1:挂市价单

      理论价格1,实际价格1.1

    • 方式2:挂限价单

      理论价格1,可能无法成交

常用的数据回测框架

  • ZipLine
  • PyAlgoTrade
  • BackTrader

使用PyAlgoTrade

步骤

(1)安装

pip install pyalgotrade