1 前言
一般而言,在學(xué)習(xí)或練習(xí)python的初級階段,我們在Jupyter Notebook(spyder或pycharm)上進(jìn)行逐條執(zhí)行語句和代碼,這樣可以起到交互的良好效果。但是如果要進(jìn)行大一點(diǎn)的項(xiàng)目實(shí)踐,這種毫無規(guī)劃的逐條執(zhí)行語句與指令就顯得不太適用了。為了使代碼得到最大程度的重復(fù)使用,并且各模塊之間邏輯更清晰,這時我們就有必要去學(xué)習(xí)模塊化的抽象設(shè)計(jì)了。模塊化的抽象設(shè)計(jì)基本思路是把主要框架和算法流程描述出來,再補(bǔ)充相應(yīng)的細(xì)節(jié)。面向?qū)ο缶幊蹋∣bject-Oriented Programming)是模塊化設(shè)計(jì)的重要方法之一,是Python進(jìn)階學(xué)習(xí)的必經(jīng)之路。
本文主要介紹了面向?qū)ο缶幊痰幕A(chǔ)知識,并以股票數(shù)據(jù)管理為例,介紹面向?qū)ο缶幊痰木唧w應(yīng)用。后續(xù)推文將會以更復(fù)雜的形式呈現(xiàn)面向?qū)ο缶幊淘趯?shí)現(xiàn)股票策略量化回測系統(tǒng)中的應(yīng)用,本文完整源碼將分享在知識星球上,可掃描下方二維碼加入獲取。
2 class類編程入門
01 class類的定義
類是具有相同屬性與方法的對象集合。
python定義類的語法有兩種:
class 類名稱:
class 類名稱(繼承類名稱):
def 定義屬性和方法
class后面是類名稱,小括號里的繼承類名稱表示定義的類繼承自哪一類。如果沒有繼承,則填object。比如下面定義一個股票stock類,屬性和方法先不寫,使用pass(跳過)表示。
#定義一個股票的類
classstock(object):
pass
#創(chuàng)建了stock這個類之后,便可以使用“stock()”來創(chuàng)建實(shí)例
s=stock()
s
#輸出結(jié)果:<__main__.stock?at?0x21f3f5aa160>
s是一個創(chuàng)建自stock類的實(shí)例。Python是一種動態(tài)語言,能夠動態(tài)地綁定一個實(shí)例的屬性與方法,如給s綁定一個code的代碼屬性:
s.code='000001.SZ'
s.code
#輸出結(jié)果:'000001.SZ'
同理,也可以從stock類創(chuàng)建一個s1的實(shí)例,并綁定代表價格的屬性:
s1=stock()
s1.price=15.8
s1.price
#輸出結(jié)果:?15.8
通過動態(tài)綁定可以擁有不同的屬性,但是這樣不利于項(xiàng)目的開發(fā)與維護(hù)。若希望創(chuàng)建自同一個類的實(shí)例擁有一些共同的屬性,則可以通過定義一個特殊的函數(shù)__init__方法(注意下劃線是在英文語境下輸入兩次),來綁定在創(chuàng)建實(shí)例時非填不可的屬性。
__init__()第一個參數(shù)根據(jù)慣例為self,用于代指被實(shí)例化出來的對象,參數(shù)code和price是用于給對象的屬性賦值,比如一個股票的類假設(shè)有代碼和價格兩個屬性,則定義stock的類可以寫成:
classstock(object):
def__init__(self,code,price):
self.code=code
self.price=price
python通過遵循一定的屬性和方法命名規(guī)則來達(dá)到訪問控制的效果:
以單下劃線開頭的名字代表protect(受保護(hù)的),如self._price
以雙下劃線開頭的名字代表private,如self.__init_change(self):
以雙下劃線開頭結(jié)尾的名字代表系統(tǒng)保留定義,如__init__(self)、__str__(self)、__len__(self)
創(chuàng)建實(shí)例時,__init__方法中的參數(shù),除了self不用輸入外,其他參數(shù)非填不可,否則會報(bào)錯。
在創(chuàng)建完類的名稱后,可以加入一些文字說明,簡要闡述類的主要內(nèi)容,以方便使用者快速了解該類的功能,這些注釋存儲成字符串的形式。注意python注釋的寫法,單行注釋前面加“#”,多行注釋使用三引號。
classstock(object):
'''
stock類中包含屬性code和price
'''
def__init__(self,code,price):
self.code=code
self.price=price
#查看類中的注釋內(nèi)容
print(stock.__doc__)
輸出結(jié)果:stock類中包含屬性code和price
假如將上述代碼保存為StockClass.py本地文件(可以使用Anaconda的Spyder來寫腳本程序),保存在jupyter當(dāng)前運(yùn)行路徑中,則可以通過導(dǎo)入類名的方式來使用類。
fromStockClassimportstock
print(stock.__doc__)
02 類的調(diào)用方法
類的調(diào)用方法主要有三種:(1)類的實(shí)例;(2)靜態(tài)方法(@裝飾器);(3)類的方法(clc)。
實(shí)例調(diào)用最常見,一般使用“類名.方法”。靜態(tài)方法由類調(diào)用,無默認(rèn)參數(shù)。將實(shí)例方法參數(shù)中的self去掉,然后在方法定義上方加上@staticmethod(前面加@是python函數(shù)的裝飾器方法),就成為靜態(tài)方法。類方法由類調(diào)用,采用@classmethod裝飾,至少傳入一個cls(代指類本身,類似self)參數(shù)。執(zhí)行類方法時,自動將調(diào)用該方法的類賦值給cls,建議使用類名.類方法的調(diào)用方式。
實(shí)例調(diào)用法:
classStockCode:
def__init__(self,name,code):
self.name=name
self.code=code
defget_stock(self):
return(self.name,self.code)
s=StockCode('中國平安','601318.SH')
s.get_stock()
#輸出結(jié)果:('中國平安',?'601318.SH')
靜態(tài)方法調(diào)用
classCodes(object):
@staticmethod
defget_code(s):
iflen(s)==6:
return(s+'SH')ifs.startswith('6')else(s+'SZ')
else:
print('股票代碼必須為6位數(shù)字!')
Codes.get_code('00001')
Codes.get_code('000001')
#輸出結(jié)果:股票代碼必須為6位數(shù)字!
#'000001SZ'
類調(diào)用法:
有的時候傳入的參數(shù)并不是('中國平安','601318.SH')這樣的格式,而是('中國平安-601318.SH')這樣的,那該怎么做?首先要把這個拆分,但是要使用實(shí)例方法實(shí)現(xiàn)起來很麻煩,這個時候就可以使用類方法。
classStockCode:
def__init__(self,stock,code):
self.stock=stock
self.code=code
@classmethod
#?裝飾器,立馬執(zhí)行下面的函數(shù)
defsplit(cls,sc):
#?cls是默認(rèn)的這個類的init函數(shù),sc是傳入?yún)?shù)
stock,code=map(str,sc.split('-'))
#?這里轉(zhuǎn)換成了格式化的結(jié)構(gòu)
dd?=?cls(stock,code)
#?然后執(zhí)行這個類第一個方法
returndd
s=StockCode.split(('中國平安-601318.SH'))
#查看屬性
s.stock,s.code
#輸出結(jié)果:('中國平安',?'601318.SH')
03 類的三大特征:封裝、繼承與多態(tài)
類有三個特征,分別是封裝、繼承和多態(tài)。封裝簡單理解是,將屬性與方法放在某個對象內(nèi)部,使外部無法訪問。接著之前的例子,若要打印股票代碼code和price,則可以先定義一個打印的函數(shù),對之前的類修改如下:
classstock(object):
def__init__(self,code,price):
self.code=code
self.price=price
#定義打印屬性的函數(shù)
defprint_attr(self):
print(f'股票代碼為:{self.code}')
print(f'股票價格為:{self.price}')
#使用“實(shí)例.方法”,即“實(shí)例名稱.函數(shù)名()”的方式來調(diào)用
s1=stock('000001.SZ',15.8)
s1.print_attr()
#輸出結(jié)果:?股票代碼為:000001.SZ
#股票價格為:15.8
把方法寫在對象內(nèi)部,仍無法防止對象的屬性被無關(guān)的函數(shù)意外改變或錯誤使用。為了對屬性提供更加安全的保障,可以限制它們不被外界訪問,可以在屬性變量前面加上兩個劃線表示private屬性,使其屬性只能在類的內(nèi)部進(jìn)行訪問,這時通過“實(shí)例.屬性名稱”從外部訪問就會報(bào)錯。通過priate屬性訪問限制,使對象內(nèi)部狀態(tài)得到保護(hù),但若需要獲取private屬性,可以在類的內(nèi)部編寫一個獲取屬性的方法。
classstock(object):
def__init__(self,code,price):
self.__code=code
self.__price=price
defget_attr(self):
return(self.__code,self.__price)
s1=stock('000001.SZ',15.8)
s1.get_attr()
#輸出結(jié)果:('000001.SZ',?15.8)
與從外部直接訪問屬性的設(shè)計(jì)相比,把方法封裝在類中的好處是可以賦予方法一些行為規(guī)范。比如,若stock類需要修改code值的功能,則可以在類的內(nèi)部增加一個set_code()的方法,且在定義該方法時,規(guī)定傳入的值必須為字符串類型,否則報(bào)錯。
classstock(object):
def__init__(self,code,price):
self.__code=code
self.__price=price
defget_attr(self):
return(self.__code,self.__price)
defset_code(self,codevalue):
iftype(codevalue)!=str:
return("錯誤,輸入?yún)?shù)必須為字符型")
self.__code=codevalue
s1=stock('000001.SZ',15.8)
print(s1.set_code(60000))
#重新定義是s1的代碼
s1.set_code('600000.SH')
print(s1.get_attr())
#輸出結(jié)果:
錯誤,輸入?yún)?shù)必須為字符型
('600000.SH',15.8)
繼承是充分利用已有的類功能,在其基礎(chǔ)上進(jìn)行擴(kuò)展和定義新的類。繼承概念的實(shí)現(xiàn)方式主要有2類:實(shí)現(xiàn)繼承、接口繼承。實(shí)現(xiàn)繼承是指使用基類的屬性和方法而無需額外編碼的能力。接口繼承是指僅使用屬性和方法的名稱、但是子類必須提供實(shí)現(xiàn)的能力(子類重構(gòu)父類方法)。在使用python編寫量化回測系統(tǒng)時經(jīng)常會用到class類的繼承功能。
#實(shí)現(xiàn)繼承
classStrategy(object):
defprint_info(self):
print('策略模塊的功能是生成“多”或“空”的交易信號')
classmy_strategy(Strategy):
pass
ss=my_strategy()
ss.print_info()
#輸出結(jié)果:
策略模塊的功能是生成“多”或“空”的交易信號
繼承有什么好處?最大的好處是子類獲得了父類的全部功能。由于Strategy實(shí)現(xiàn)了print_info()方法,因此,my_strategy作為它的子類,就自動擁有了print_info()方法。
#接口繼承
#這里需要先引入一個抽象模塊abc
fromabcimportABCMeta,?abstractmethod
#python中使用@表示裝飾器的意思,具體可以參照python裝飾器的相關(guān)教程
classStrategy(object):
"""Strategy是一個抽象基類"""
#?abc.ABCMeta是實(shí)現(xiàn)抽象類的一個基礎(chǔ)類
__metaclass__?=?ABCMeta
#子類必須有這個方法,否則報(bào)錯
@abstractmethod#?定義抽象方法,無需實(shí)現(xiàn)功能
defgenerate_signals(self):
"""輸入數(shù)據(jù)產(chǎn)生多空的交易信號"""
raiseNotImplementedError("應(yīng)包含方法:
generate_signals()!"
)
#定義子類
classmy_strategy2(Strategy):
defgenerate_signals(self):
pass
ss2.generate_signals()
#輸出結(jié)果:實(shí)現(xiàn)交易信號功能
ss1.generate_signals()
盡管上面的接口很簡單,但是當(dāng)為每種特定類型的策略繼承此類時,它將變得更加復(fù)雜。策略類的目標(biāo)是為投資組合模塊中提供多/空/持有信號。繼承可以把父類的所有功能都直接拿過來,這樣就不必重零做起,子類只需要新增自己特有的方法,也可以把父類不適合的方法覆蓋重寫;有了繼承,才能有多態(tài)。多態(tài)的意思是,當(dāng)子類(strategy2)與父類(Strategy)具有相同的方法(print_info())時,在實(shí)例子類的時候會自動調(diào)用子類的該方法(而不是父類的方法)。
classStrategy(object):
defprint_info(self):
print('策略模塊的功能是生成“多”或“空”的交易信號')
#繼承????????
classstrategy1(Strategy):
pass
#多態(tài)????????
classstrategy2(Strategy):
defprint_info(self):
print('這是個人的交易策略')
#實(shí)例化
strategy1().print_info()
strategy2().print_info()
#輸出結(jié)果:
策略模塊的功能是生成“多”或“空”的交易信號
這是個人的交易策略
3 class類編程在金融量化中的應(yīng)用
應(yīng)用背景:使用tushare獲取所有A股每日交易數(shù)據(jù),保存到本地?cái)?shù)據(jù)庫,同時每日更新數(shù)據(jù)庫;根據(jù)行情數(shù)據(jù)進(jìn)行可視化和簡單的策略分析與回測。由于篇幅有限,本文著重介紹股票數(shù)據(jù)管理(下載、數(shù)據(jù)更新)的面向?qū)ο缶幊虘?yīng)用實(shí)例。
#導(dǎo)入需要用到的模塊
importnumpyasnp
importpandasaspd
fromdateutil.parserimportparse
fromdatetimeimportdatetime,timedelta
#操作數(shù)據(jù)庫的第三方包,使用前先安裝pip?install?sqlalchemy
fromsqlalchemyimportcreate_engine
#tushare包設(shè)置
importtushareasts
token='輸入你在tushare上獲得的token'
pro=ts.pro_api(token)
#使用python3自帶的sqlite數(shù)據(jù)庫
#本人創(chuàng)建的數(shù)據(jù)庫地址為c:\zjy\db_stock\
file='sqlite:///c:\\zjy\\db_stock\\'
#數(shù)據(jù)庫名稱
db_name='stock_data.db'
engine?=?create_engine(file+db_name)
classData(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()
#獲取股票代碼列表????
defget_code(self):
codes?=?pro.stock_basic(list_status='L').ts_code.values
returncodes
#獲取股票交易日歷
defget_cals(self):
#獲取交易日歷
cals=pro.trade_cal(exchange='')
cals=cals[cals.is_open==1].cal_date.values
returncals
#每日行情數(shù)據(jù)
defdaily_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='')
#復(fù)權(quán)因子
df=pd.merge(df0,df1)#合并數(shù)據(jù)
exceptExceptionase:
print(code)
print(e)
returndf
#保存數(shù)據(jù)到數(shù)據(jù)庫
defsave_sql(self):
forcodeinself.codes:
data=self.daily_data(code)
data.to_sql(self.table_name,engine,
index=False,if_exists='append')
#獲取最新交易日期
defget_trade_date(self):
#獲取當(dāng)天日期時間
pass
#更新數(shù)據(jù)庫數(shù)據(jù)
defupdate_sql(self):
pass#代碼省略
#查詢數(shù)據(jù)庫信息????????????
definfo_sql(self):
pass#代碼省略
代碼運(yùn)行
#假設(shè)你將上述代碼封裝成class?Data
#保存在'C:\zjy\db_stock'目錄下的down_data.py中
importsys
#添加到當(dāng)前工作路徑
sys.path.append(r'C:\zjy\db_stock')
#導(dǎo)入py文件中的Data類
fromdownload_dataimportData
#實(shí)例類
data=Data()
#data.save_sql()?#只需運(yùn)行一次即可
data.update_sql()
data.info_sql()
輸出結(jié)果:
數(shù)據(jù)已經(jīng)是最新的!
統(tǒng)計(jì)查詢的總數(shù):7747184
數(shù)據(jù)期間:20050104——20191125
數(shù)據(jù)庫包含股票個數(shù):3724
#另外又根據(jù)畫圖需要,從數(shù)據(jù)庫中提取數(shù)據(jù)畫K線圖
#寫了一個stock_plot類保存在plot_stock.py文件中
fromplot_stockimportstock_plot
shfz=stock_plot('雙匯發(fā)展')
shfz.kline_plot()#普通K線圖

#畫修正版K線圖
shfz.kline_plot(ktype=1)

4 結(jié)語
本文主要介紹了面向?qū)ο缶幊痰幕A(chǔ)知識和股票數(shù)據(jù)管理應(yīng)用實(shí)例。
通常我們會以各種數(shù)據(jù)信息來刻畫描述一個對象,而這個對象包含某些屬性或特征。比如,將某只股票視為一個對象,那么該股票對象包含的信息可能有股票代碼、公司名稱、所屬行業(yè)、收盤價等。這些用以描述對象特征的數(shù)據(jù)信息稱為對象的屬性(Attribute);而存取屬性的函數(shù)則稱為方法(Method),是該對象與外界溝通的接口。具有相同屬性與方法的對象構(gòu)成了一個類別(Class)。換句話說,類是一種將對象抽象化而形成的概念,而對象則是類具體實(shí)現(xiàn)的例子(實(shí)例)。以對象為基礎(chǔ)的編程思想具有封裝、繼承和多態(tài)三大特征,在構(gòu)建量化交易系統(tǒng)中發(fā)揮了非常重要的作用。后續(xù)推文將介紹使用面向?qū)ο缶幊痰牧炕呗曰販y。
參考資料:廖雪峰的官方網(wǎng)站Python3教程——面向?qū)ο缶幊?/p>
關(guān)于Python金融量化
專注于分享Python在金融量化領(lǐng)域的應(yīng)用。加入知識星球,可以免費(fèi)獲取30多g的量化投資視頻資料、量化金融相關(guān)PDF資料、公眾號文章Python完整源碼、量化投資前沿分析框架,與博主直接交流、結(jié)識圈內(nèi)朋友等。