一般而言,在学习或练习python的初级阶段,我们在Jupyter Notebook(spyder或pycharm)上进行逐条执行语句和代码,这样可以起到交互的良好效果。但是如果要进行大一点的项目实践,这种毫无规划的逐条执行语句与指令就显得不太适用了。为了使代码得到最大程度的重复使用,并且各模块之间逻辑更清晰,这时我们就有必要去学习模块化的抽象设计了。模块化的抽象设计基本思路是把主要框架和算法流程描述出来,再补充相应的细节。面向对象编程(Object-Oriented Programming)是模块化设计的重要方法之一,是Python进阶学习的必经之路。
本文主要介绍了面向对象编程的基础知识,并以股票数据管理为例,介绍面向对象编程的具体应用。后续推文将会以更复杂的形式呈现面向对象编程在实现股票策略量化回测系统中的应用,本文完整源码将分享在知识星球上(知识星球搜索 id:27799067,金融量化,CuteHand)。
01class类的定义
类是具有相同属性与方法的对象集合。
python定义类的语法有两种:
class 类名称:
class 类名称(继承类名称):
def 定义属性和方法
class后面是类名称,小括号里的继承类名称表示定义的类继承自哪一类。如果没有继承,则填object。比如下面定义一个股票stock类,属性和方法先不写,使用pass(跳过)表示。
#定义一个股票的类
class stock(object):
pass
#创建了stock这个类之后,便可以使用“stock()”来创建实例
s=stock()
s
#输出结果:<__main__.stock at 0x21f3f5aa160>
s是一个创建自stock类的实例。Python是一种动态语言,能够动态地绑定一个实例的属性与方法,如给s绑定一个code的代码属性:
s.code='000001.SZ'
s.code
#输出结果:'000001.SZ'
同理,也可以从stock类创建一个s1的实例,并绑定代表价格的属性:
s1=stock()
s1.price=15.8
s1.price
#输出结果: 15.8
通过动态绑定可以拥有不同的属性,但是这样不利于项目的开发与维护。若希望创建自同一个类的实例拥有一些共同的属性,则可以通过定义一个特殊的函数__init__方法(注意下划线是在英文语境下输入两次),来绑定在创建实例时非填不可的属性。
__init__()第一个参数根据惯例为self,用于代指被实例化出来的对象,参数code和price是用于给对象的属性赋值,比如一个股票的类假设有代码和价格两个属性,则定义stock的类可以写成:
class stock(object):
def __init__(self,code,price):
self.code=code
self.price=price
python通过遵循一定的属性和方法命名规则来达到访问控制的效果:
创建实例时,__init__方法中的参数,除了self不用输入外,其他参数非填不可,否则会报错。
在创建完类的名称后,可以加入一些文字说明,简要阐述类的主要内容,以方便使用者快速了解该类的功能,这些注释存储成字符串的形式。注意python注释的写法,单行注释前面加“#”,多行注释使用三引号。
class stock(object):
'''
stock类中包含属性code和price
'''
def __init__(self,code,price):
self.code=code
self.price=price
#查看类中的注释内容
print(stock.__doc__)
输出结果:stock类中包含属性code和price
假如将上述代码保存为StockClass.py本地文件(可以使用Anaconda的Spyder来写脚本程序),保存在jupyter当前运行路径中,则可以通过导入类名的方式来使用类。
from StockClass import stock
print(stock.__doc__)
02类的调用方法
类的调用方法主要有三种:(1)类的实例;(2)静态方法(@装饰器);(3)类的方法(clc)。
实例调用最常见,一般使用“类名.方法”。静态方法由类调用,无默认参数。将实例方法参数中的self去掉,然后在方法定义上方加上@staticmethod(前面加@是python函数的装饰器方法),就成为静态方法。类方法由类调用,采用@classmethod装饰,至少传入一个cls(代指类本身,类似self)参数。执行类方法时,自动将调用该方法的类赋值给cls,建议使用类名.类方法的调用方式。
实例调用法:
class StockCode:
def __init__(self,name,code):
self.name=name
self.code=code
def get_stock(self):
return (self.name,self.code)
s=StockCode('中国平安','601318.SH')
s.get_stock()
#输出结果:('中国平安', '601318.SH')
静态方法调用
class Codes(object):
@staticmethod
def get_code(s):
if len(s)==6:
return (s+'SH') if s.startswith('6') else (s+'SZ')
else:
print('股票代码必须为6位数字!')
Codes.get_code('00001')
Codes.get_code('000001')
#输出结果:股票代码必须为6位数字!
#'000001SZ'
类调用法:
有的时候传入的参数并不是('中国平安','601318.SH')这样的格式,而是('中国平安-601318.SH')这样的,那该怎么做?首先要把这个拆分,但是要使用实例方法实现起来很麻烦,这个时候就可以使用类方法。
class StockCode:
def __init__(self,stock,code):
self.stock=stock
self.code=code
@classmethod
# 装饰器,立马执行下面的函数
def split(cls,sc):
# cls是默认的这个类的init函数,sc是传入参数
stock,code=map(str,sc.split('-'))
# 这里转换成了格式化的结构
dd = cls(stock,code)
# 然后执行这个类第一个方法
return dd
s=StockCode.split(('中国平安-601318.SH'))
#查看属性
s.stock,s.code
#输出结果:('中国平安', '601318.SH')
03类的三大特征:封装、继承与多态
类有三个特征,分别是封装、继承和多态。封装简单理解是,将属性与方法放在某个对象内部,使外部无法访问。接着之前的例子,若要打印股票代码code和price,则可以先定义一个打印的函数,对之前的类修改如下:
class stock(object):
def __init__(self,code,price):
self.code=code
self.price=price
#定义打印属性的函数
def print_attr(self):
print(f'股票代码为:{self.code}')
print(f'股票价格为:{self.price}')
#使用“实例.方法”,即“实例名称.函数名()”的方式来调用
s1=stock('000001.SZ',15.8)
s1.print_attr()
#输出结果: 股票代码为:000001.SZ
#股票价格为:15.8
把方法写在对象内部,仍无法防止对象的属性被无关的函数意外改变或错误使用。为了对属性提供更加安全的保障,可以限制它们不被外界访问,可以在属性变量前面加上两个划线表示private属性,使其属性只能在类的内部进行访问,这时通过“实例.属性名称”从外部访问就会报错。通过priate属性访问限制,使对象内部状态得到保护,但若需要获取private属性,可以在类的内部编写一个获取属性的方法。
class stock(object):
def __init__(self,code,price):
self.__code=code
self.__price=price
def get_attr(self):
return(self.__code,self.__price)
s1=stock('000001.SZ',15.8)
s1.get_attr()
#输出结果:('000001.SZ', 15.8)
与从外部直接访问属性的设计相比,把方法封装在类中的好处是可以赋予方法一些行为规范。比如,若stock类需要修改code值的功能,则可以在类的内部增加一个set_code()的方法,且在定义该方法时,规定传入的值必须为字符串类型,否则报错。
class stock(object):
def __init__(self,code,price):
self.__code=code
self.__price=price
def get_attr(self):
return(self.__code,self.__price)
def set_code(self,codevalue):
if type(codevalue)!=str:
return("错误,输入参数必须为字符型")
self.__code=codevalue
s1=stock('000001.SZ',15.8)
print(s1.set_code(60000))
#重新定义是s1的代码
s1.set_code('600000.SH')
print(s1.get_attr())
#输出结果:
错误,输入参数必须为字符型
('600000.SH', 15.8)
继承是充分利用已有的类功能,在其基础上进行扩展和定义新的类。继承概念的实现方式主要有2类:实现继承、接口继承。实现继承是指使用基类的属性和方法而无需额外编码的能力。接口继承是指仅使用属性和方法的名称、但是子类必须提供实现的能力(子类重构父类方法)。在使用python编写量化回测系统时经常会用到class类的继承功能。
#实现继承
class Strategy(object):
def print_info(self):
print('策略模块的功能是生成“多”或“空”的交易信号')
class my_strategy(Strategy):
pass
ss=my_strategy()
ss.print_info()
#输出结果:
策略模块的功能是生成“多”或“空”的交易信号
继承有什么好处?最大的好处是子类获得了父类的全部功能。由于Strategy实现了print_info()方法,因此,my_strategy作为它的子类,就自动拥有了print_info()方法。
#接口继承
#这里需要先引入一个抽象模块abc
from abc import ABCMeta, abstractmethod
#python中使用@表示装饰器的意思,具体可以参照python装饰器的相关教程
class Strategy(object):
"""Strategy是一个抽象基类"""
# abc.ABCMeta是实现抽象类的一个基础类
__metaclass__ = ABCMeta
#子类必须有这个方法,否则报错
@abstractmethod # 定义抽象方法,无需实现功能
def generate_signals(self):
"""输入数据产生多空的交易信号"""
raise NotImplementedError("应包含方法:
generate_signals()!")
#定义子类
class my_strategy2(Strategy):
def generate_signals(self):
pass
ss2.generate_signals()
#输出结果:实现交易信号功能
ss1.generate_signals()
尽管上面的接口很简单,但是当为每种特定类型的策略继承此类时,它将变得更加复杂。策略类的目标是为投资组合模块中提供多/空/持有信号。继承可以把父类的所有功能都直接拿过来,这样就不必重零做起,子类只需要新增自己特有的方法,也可以把父类不适合的方法覆盖重写;有了继承,才能有多态。多态的意思是,当子类(strategy2)与父类(Strategy)具有相同的方法(print_info())时,在实例子类的时候会自动调用子类的该方法(而不是父类的方法)。
class Strategy(object):
def print_info(self):
print('策略模块的功能是生成“多”或“空”的交易信号')
#继承
class strategy1(Strategy):
pass
#多态
class strategy2(Strategy):
def print_info(self):
print('这是个人的交易策略')
#实例化
strategy1().print_info()
strategy2().print_info()
#输出结果:
策略模块的功能是生成“多”或“空”的交易信号
这是个人的交易策略
应用背景:使用tushare获取所有A股每日交易数据,保存到本地数据库,同时每日更新数据库;根据行情数据进行可视化和简单的策略分析与回测。由于篇幅有限,本文着重介绍股票数据管理(下载、数据更新)的面向对象编程应用实例。
#导入需要用到的模块
import numpy as np
import pandas as pd
from dateutil.parser import parse
from datetime import datetime,timedelta
#操作数据库的第三方包,使用前先安装pip install sqlalchemy
from sqlalchemy import create_engine
#tushare包设置
import tushare as ts
token='输入你在tushare上获得的token'
pro=ts.pro_api(token)
#使用python3自带的sqlite数据库
#本人创建的数据库地址为c:zjydb_stock
file='sqlite:///c:zjydb_stock'
#数据库名称
db_name='stock_data.db'
engine = create_engine(file+db_name)
class Data(object):
def __init__(self,
start='20050101',
end='20191115',
table_name='daily_data'):
self.start=start
self.end=end
self.table_name=table_name
self.codes=self.get_code()
self.cals=self.get_cals()
#获取股票代码列表
def get_code(self):
codes = pro.stock_basic(list_status='L').ts_code.values
return codes
#获取股票交易日历
def get_cals(self):
#获取交易日历
cals=pro.trade_cal(exchange='')
cals=cals[cals.is_open==1].cal_date.values
return cals
#每日行情数据
def daily_data(self,code):
try:
df0=pro.daily(ts_code=code,start_date=self.start,
end_date=self.end)
df1=pro.adj_factor(ts_code=code,trade_date='')
#复权因子
df=pd.merge(df0,df1) #合并数据
except Exception as e:
print(code)
print(e)
return df
#保存数据到数据库
def save_sql(self):
for code in self.codes:
data=self.daily_data(code)
data.to_sql(self.table_name,engine,
index=False,if_exists='append')
#获取最新交易日期
def get_trade_date(self):
#获取当天日期时间
pass
#更新数据库数据
def update_sql(self):
pass #代码省略
#查询数据库信息
def info_sql(self):
pass #代码省略
代码运行
#假设你将上述代码封装成class Data
#保存在'C:zjydb_stock'目录下的down_data.py中
import sys
#添加到当前工作路径
sys.path.append(r'C:zjydb_stock')
#导入py文件中的Data类
from download_data import Data
#实例类
data=Data()
#data.save_sql() #只需运行一次即可
data.update_sql()
data.info_sql()
输出结果:
数据已经是最新的!
统计查询的总数:7747184
数据期间:20050104——20191125
数据库包含股票个数:3724
#另外又根据画图需要,从数据库中提取数据画K线图
#写了一个stock_plot类保存在plot_stock.py文件中
from plot_stock import stock_plot
shfz=stock_plot('双汇发展')
shfz.kline_plot() #普通K线图
#画修正版K线图
shfz.kline_plot(ktype=1)
本文主要介绍了面向对象编程的基础知识和股票数据管理应用实例。
通常我们会以各种数据信息来刻画描述一个对象,而这个对象包含某些属性或特征。比如,将某只股票视为一个对象,那么该股票对象包含的信息可能有股票代码、公司名称、所属行业、收盘价等。这些用以描述对象特征的数据信息称为对象的属性(Attribute);而存取属性的函数则称为方法(Method),是该对象与外界沟通的接口。具有相同属性与方法的对象构成了一个类别(Class)。换句话说,类是一种将对象抽象化而形成的概念,而对象则是类具体实现的例子(实例)。以对象为基础的编程思想具有封装、继承和多态三大特征,在构建量化交易系统中发挥了非常重要的作用。后续推文将介绍使用面向对象编程的量化策略回测。
参考资料:廖雪峰的官方网站Python3教程——面向对象编程
关于Python金融量化
专注于分享Python在金融量化领域的应用。加入金融量化知识星球(id:27799067),可以免费获取30多g的量化投资视频资料、量化金融相关PDF资料、公众号文章Python完整源码、量化投资前沿分析框架,与博主直接交流、结识圈内朋友等。