上一篇文章裏,我們提及瞭如何設計向量化回測與調參。通過scikit-learn這套框架,你已經能快速的驗證你的任何想法。同時通過不斷地努力,也發現了一個頗有成效的策略模式,那麼接下來,讓我們把回測模型一點點轉變得更接近現實世界吧。
scikit-learn
所以這一章,我們將分別聊一遍 事件驅動回測框架 和 實盤交易系統 的架構與實現細節。這篇開始就逐漸有工程味道了,小夥伴們準備好哦。
參考
承接著向量化回測,這一篇我們先說事件驅動回測模塊。開始之前,先明確列出我們對事件驅動系統的需求:
1. 回測時,將儘可能模擬真實環境。
2. 策略擴展性強
3. 策略有實時風控、倉位管理
4. 工程上,要能保證策略開發完後能無縫對接到實盤之中
我們一個一個需求來解決。
首先先明確事件驅動概念。事件驅動有別於向量化回測,更加接近真實,信息不再是一次性以一大塊矩陣的形式送到系統之中,而是以事件的形式一個一個被喂到系統裏,觸發設定好的回調來進行邏輯上的處理。
事件驅動回測中的各個模塊如下,我們將逐一對其進行介紹。
我們先來理解一下各個模塊的概念和作用,之後再正式進入細節上的探究。
Strategy,顧名思義,是我們的核心策略模塊,包含了4個重要組件,分別是
Strategy
Event Processor
Position Manager
Risk Manager
Commander
StrategyEngine,策略引擎,負責管理多個策略。策略引擎在這裡是一個抽象,具體實現上有兩種,第一種就是回測引擎,另一種則是後面將提及的實時交易引擎。有了策略引擎這一層抽象,就能保證我們的策略在回測完之後,能無縫地接入到實盤進行交易。(需求4)
StrategyEngine
這裡我們先介紹BackTestEngine,即回測引擎。回測引擎在程序中主要負責將數據(如bar、depth、trade、order等)轉化成事件、按時間發生的順序,一個個地餵給策略。其中比較重要的是兩個組件。
BackTestEngine
Matcher
Analyzer
pyfolio
概念講的隨意,但深入各個組件的設計上就有很多活幹了。慶幸的是,如第一篇文章曾提及的那樣,我們並不需要完全從零開始設計,借鑒開源項目vnpy和backtrader,將幫我們的設計省下不少時間。
vnpy
backtrader
首先,我們先關注組件之間的信息流交互。下面是一張順序圖,描述了當一個bar數據被送到系統裏時發生的操作。
現在我們重點關注一下如下幾個組件
BackTest Engine
BackTest Engine的設計理念就是廣為人知的觀察者模式。我們將把回測引擎設計成觀察者模式中Subject,是發布者。而策略則是一個Subscriber,即訂閱者,當有相關主題的數據發出來時,策略中的Event Processor就會對事件進行處理。
Subject
Subscriber
一般來說,事件和對應的處理函數主要分為這幾類, 其中的邏輯細節由交易員或研究員自行補充完整:
這塊重點提一下訂閱者與發布者這兩個核心基類,實現如下:
class Subject(object): def __init__(self): super(Subject, self).__init__() self._handler_dict = defaultdict(list)
def register(self, topic: typing.Hashable, handler: typing.Callable): if not callable(handler): raise ValueError(Handler should be a callable object) if handler not in self._handler_dict[topic]: self._handler_dict[topic].append(handler)
def unregister(self, topic: typing.Hashable, handler: typing.Callable): if handler in self._handler_dict[topic]: self._handler_dict[topic].remove(handler) if not self._handler_dict[topic]: del self._handler_dict[topic]
def notify(self, topic: typing.Hashable, msg): if topic in self._handler_dict: for handler in self._handler_dict[topic]: handler(msg)
class Subscriber(object):
def __init__(self): super(Subscriber, self).__init__() self._topic_handler_list = [] self._subject: Subject = None
def subscribe(self, topic: typing.Hashable, handler: typing.Callable): self._topic_handler_list.append((topic, handler)) self._subject.register(topic, handler)
使用時主要分兩步:
subscribe
notify
別小看這簡簡單單的十來行代碼,在後期的工程開發,這個抽象模板將幫你省下不少功夫。模塊之間的耦合性大大降低,組件間的信息流動更加明確,開發者也可以將更多的精力放在業務邏輯處理之中。
參考:vnpy/EventEngine。
Position Manager是比較關鍵的一塊,此處,筆者並沒有直接使用其他開源項目進行改造,而是自己動手重新設計,加入兩組概念。
第一組新的概念是Position和Portfolio。Position是記錄了一個合約上交易的盈虧,Portfolio則管理了多個Position對象。如前文所述,這裡的Position要能支持多種資產(股票、外匯、電子貨幣)以及多種合約(現貨、期貨、掉期)。
Position
Portfolio
第二組概念 是base_asset、quote_asset,譬如對應外匯交易中,USD/HKD這樣的合約交易中,USD就是base_asset、HKD是quote_asset。如果只進行股票、期貨交易,可以略過這個設計,因為他們會使系統的複雜性升高,但相應的,你的系統也可以支持一些複雜的場景,例如外匯的三角套利。這裡我們先略過其細節,等後續有機會講實盤策略時再詳述其設計。
base_asset
quote_asset
USD/HKD
USD
HKD
回到Position,其介面設計如下
from dataclasses import dataclass
@dataclass class PositionData: symbol: str = EMPTY_STRING amount: float = EMPTY_FLOAT total_value: float = EMPTY_FLOAT last_price: float = EMPTY_FLOAT cost: float = EMPTY_FLOAT total_pnl: float = EMPTY_FLOAT realized_pnl: float = EMPTY_FLOAT unrealized_pnl: float = EMPTY_FLOAT
def on_trade(self, trade: TradeData) pass
def update_market(self, price, dt): self.last_price = price self.datetime = dt self.calculate_unrealized_pnl()
def calculate_unrealized_pnl(self): pass
這裡關鍵在於抽象介面on_trade,以及calculate_unrealized_pnl,分別負責計算委託成交時、以及實時市場價格變動時,組合的盈虧變動情況。
on_trade
calculate_unrealized_pnl
這樣的抽象設計,就能使我們的程序不僅能處理現貨交易(SpotPosition),即便是期權(OptionPosition)、掉期(SwapPosition)等複雜合約,也能順利對接到系統之中,上層模塊也便無需關心其中的實現細節,達到封裝的效果。
SpotPosition
OptionPosition
SwapPosition
於是,一個最簡單的均線交叉策略大概會長這個樣子
class ShortableMACrossStrategy(CTAStrategyTemplate): def __init__(self, n_sma=13, n_lma=20, frequency=1h, stop_loss=-0.02): super(ShortableMACrossStrategy, self).__init__() self.n_sma = n_sma self.n_lma = n_lma self.frequency = frequency self.stop_loss = stop_loss
self._loss_control = MaxLossControl(self.stop_loss) self._loss_control.portfolio = self.portfolio self._close_deque = deque(max_len=n_lma + 1)
def on_bar(self, bar): super(ShortableMACrossStrategy, self).on_bar(bar)
# stop loss if self._loss_control.on_bar(bar): self.close_position(self.symbol, 2)
if bar.frequency == self.frequency and bar.symbol == self.symbol: array = self._close_deque array.append(bar.close) if len(array) != array.max_len: logger.debug("%ss data is not enough", self) return array = np.array(array) sma_line = talib.MA(array, self.n_sma) lma_line = talib.MA(array, self.l_sma)
# up cross if lma_line[-1] < sma_line[-1] and lma_line[-2] >= sma_line[-2]: self._full_trade(EnumOrderDirection.BUY)
# down cross if lma_line[-1] > sma_line[-1] and lma_line[-2] <= sma_line[-2]: self._full_trade(EnumOrderDirection.SELL)
至此,我們已經能用事件驅動的方式回測我們的策略,回測的結果也將更加符合真實情況。更重要的是,我們能復用這套框架,直接對接到我們的實盤交易系統中,保證策略「所測即所得」。
相同的,我們也先明確我們對實盤交易系統的需求所在。
第一個需求比較容易實現。我們可以直接將BackTestEngine替換成TradingEngine,負責管理多個策略。同時,我們也把這個TradingEngine抽象成一個Application,相同的層級還包括Scrawler實時數據爬取器,Monitor UI市場監控器等。
TradingEngine
Application
Scrawler
Monitor UI
在實現上,他們有一個相同點,那就是訂閱並監聽從上一層發過來的信息,原理即上文所介紹的訂閱者模式。訂閱各自關注的主題後,各個應用便能獨立運行,也就是說,各個應用之前相互獨立,同時與其依賴的底層關聯度也極低,僅是信息訂閱這一項而已,二者之間幾乎沒有耦合性,這能極大地方便我們開發的進程。(需求1)
於是,我們可以把關注點轉移到這個應用們所依賴的這個」底層」,即交易系統的「交易服務中臺」。
中臺主要有兩個核心組件, CEP(複雜事件處理引擎)+ OMS(訂單管理系統)。
CEP,即Complex Event Processing,複雜事件處理系統,是真正意義上驅動整個系統運行的引擎。主要由如下三個組件構成
事件分發器,本質上也是一個Subject,負責將各個主題的分發出去,要稍微留意的是,單個事件的主題數量可多於1個。例如對於訂單信息而言,我們一般賦予它兩個主題:
EnumOrderStatus
(EnumOrderStatus, strategy_id)
strategy_id
信號計算器,負責將通過的信號計算出來,避免每個策略單獨計算,浪費資源。通常的任務包括 - K線集成: 1分鐘K --> N分鐘K --> 小時K --> 日K - 成交量K線生成 - 期權greek計算
持久化引擎,負責將存儲重要數據,並持久化到資料庫中,同時具備實時查詢功能。一般我們會將實時市場行情、訂單委託、成交、命令請求等都存入到該引擎中。
技術層面上,推薦使用 ORM,這樣能將技術細節的實現推到最後來完成,解耦系統與資料庫的依賴關係。未來無論是連接MongoDB還是MySQL,只需切換數據連接器,便能順滑過渡。
同時要注意的是,因為我們需要把數據持久化到資料庫裏,這就會涉及到I/O操作。I/O操作一般都要比內存操作慢N個數量級。為了保證系統運行的效率,我們一般會對此採取非同步操作。同樣的處理也出現在後面要提及的向交易所發送交易請求。(需求3)
訂單管理系統也由3個組件構成:
總風控和總倉位管理,將直接復用單策略裏的風控、倉位管理對象。只是職責上,總風控將是更關注總體的倉位、訂單風險,總倉位管理則實時匯總所有策略的倉位、盈虧情況。
這裡我們更關注訂單演算法引擎。實盤中有這樣一個公式,利潤 = 策略 + 交易,好的策略很重要,但好的交易方式也不可缺少,如何減少訂單的衝擊成本,如何獲得市場的最佳價格,同樣是一門學問。
利潤 = 策略 + 交易
訂單演算法引擎負責處理策略發來的訂單請求,並按照設計好的演算法,將訂單發送到交易所中。(需求2)
最常見的訂單演算法,包括:
前3個演算法相對容易實現,提一下智能路由訂單演算法。
這個演算法在電子貨幣交易領域中使用得很多,因為這個市場上的交易所實在是太多了,像ETH/BTC這樣的合約,市場上有幾十個,交易價格參差不齊。我們的目標是選擇其中幾個穩定的交易所,交易時,選擇各個交易所中價格最優的進行掛單交易。
ETH/BTC
實現這個功能,需要我們要能快速地實時合併不同交易所訂單簿,使得該合併訂單簿能夠:
抽象成演算法題,就是請你設計一個數據結構,使得查詢O(1),插入O(1),刪除O(1),排序O(N)的數據結構。顯然,這不是單個數據結構就能解決的,需要多個數據結構之間互相配合才能實現。權且留作這篇文章的思考題吧,對未來的演算法面試會有幫助的。
回到訂單結構的實現上,為了方便統一介面類型,我們可以將演算法訂單整合到order_type中
order_type
class EnumOrderType(IntEnum): NONE = auto() LIMIT = auto() MARKET = auto() BLP = auto() VWAP = auto() TWAP = auto() STOP = auto() SOR = auto()
@dataclass() class OrderData(TradingBaseData): price: float = EMPTY_FLOAT volume: float = EMPTY_FLOAT
direction: EnumOrderDirection = EnumOrderDirection.NONE order_type: EnumOrderType = EnumOrderType.NONE status: EnumOrderStatus = EnumOrderStatus.NONE action: EnumOrderAction = EnumOrderAction.NONE
client_order_id: str = EMPTY_STRING order_id: str = EMPTY_STRING executed_volume: float = EMPTY_FLOAT
parameter: dict = field(default_factory=dict)
參考:vn.py/algoTrading
前臺、中臺都打通了,現在就差後臺了,也就是底層真正和交易所進行連接的模塊。
如圖所示,這裡主要有兩個部分構成
Gateway,交易所對接網關,負責將各個交易所五花八門的介面統一起來,接入系統。每個Gateway至少需要包含以下職能:
Gateway
信息請求將包括 (1).合約下載 (2).交易請求 (3).信息查詢
這裡著重講一下合約信息。合約信息是與交易所對接的關鍵,因為在發送訂單等信息到交易所時,必須要嚴格符合該交易所的格式標準。例如訂單發送時不能填寫symbol(自身系統內的標的代表符號),而是symbol_exchange,對應到交易所繫統的標的代表符號。其他的還有lot_size代表 每筆交易中交易量變動的最小量,tick_size代表每筆交易中價格變動的最小量。在發送訂單之前,需要對這些量進行取整處理。
symbol
symbol_exchange
lot_size
tick_size
@dataclass class ContractData(MarketBaseData):
contract_type: str = EMPTY_STRING # 合約類型: 現貨、期貨、掉期、期權 symbol: str = EMPTY_STRING # 自身系統內的標的代表符號 symbol_exchange: str = EMPTY_STRING # 交易所繫統的標的代表符號 exchange: str = EMPTY_STRING # 交易所
lot_size: float = EMPTY_FLOAT # 每筆交易中 交易量 變動的最小量 tick_size: float = EMPTY_FLOAT # 每筆交易中 價格 變動的最小量
max_price: float = MAX_FLOAT min_price: float = EMPTY_FLOAT
max_quantity: float = MAX_FLOAT min_quantity: float = EMPTY_FLOAT
max_notional: float = MAX_FLOAT min_notional: float = EMPTY_FLOAT
從這個合約介面就能看出,洗介面是一件相當繁瑣的活,需要不斷將各個值都對應上,而且往往還一個交易所一套介面類型。「行百里者半九十」,我們馬上就能打通實盤交易了,不要懼怕這最後的臟活。
相似的,我們還需要把訂單發送和查詢的介面補充完整,請加油。
2. 信息訂閱
上文的請求發送,一般是交易系統主動發出的,而信息訂閱則是交易所主動推過來的。一般包括成交明細訂閱、訂單狀態訂閱、K線訂閱、深度行情訂閱。實現上,一般都是起一個線程單獨監聽對應的埠。例如電子貨幣交易中最常用的websockets介面。
websockets
在工程上的,交易所連接部分有2個注意事項:
Router,訂單路由器,負責將訂單發送到指定的交易所進行成交。(需求4)
實現上要做到的功能包括
- 多交易所:同時連接多個交易所,這是進行跨交易所套利的基礎。
- 多賬戶:同時包含多個賬戶,這是對外提供交易服務的基礎
- 多類型:可以設置不同的連接類型
如此一來,在策略回測完畢之後,可以先使用Simu trading模式,比對兩者結果,確認策略無bug、系統無風險。之後,就可以上Paper trading,不使用真實賬戶資金,觀察實盤效果。如果效果良好,那麼,就切換到真正的實盤模式,加入自營資金,跑起策略來了。
實現上也很簡單,我們將定義多個Router的子類。(需求5)
現在,我們的實盤交易系統差不多就建好了。當然,部署和運維也是一大塊,後期也還需要不斷維護和迭代。
一如既往,我們還是需要分析一下系統,瞭解其中的不足之處。
首先是關於系統的性能方面,系統的瓶頸將會出現在3個地方:
1+1
queue
multi-producer, multi-consumer
其次則是缺少一個統一的、標準化的、適合團隊協同工作的解決方案。
如果這些環節不完善的話,那麼整個交易系統也只是一堆花哨卻無意義的代碼。要與團隊相結合,運作流轉起來,才能最大程度地發揮系統的效能。
優秀的你對這兩個方向一定也有很多自己的想法,所以請繼續關注我的下篇文章,我們到時將一起深入探討更快的技術和更有效率的解決方案。
回頭看來,我們不難發現,這個交易系統已經不再只是簡單的程序包了,而是一個完整的工程項目。工程項目的管理也是一門藝術,有不少的工程規則和知識,這裡和大家分享一下這一路的心得
這些是都是軟體工程中的基本知識,但每一個展開都有極深的內容,點點滴滴,相信在搭建系統的過程中,我們會慢慢積累、慢慢成長。這也算是造這個交易系統輪子的意義之一。
聽到這,或許你會這樣反問道:「太CS了吧...」
確實如此。你大可用其他的、任何你覺得合適的現成平臺進行策略開發和交易。但原則上,你要能保證自己的發展不會被限制在某個在線平臺、或者某個公司裏。你的價值不會因為你的離職或者網站的關閉而減損。
對於一個Quant來說,當他受制於人的那一刻,就已經註定了是一場悲劇。
我的觀點是:要一直刻意地訓練自己,保持自己的競爭力。有獨立的思考、也有紮實的技術和執行力,有能力隨時驗證自己的想法並將其執行落地,不被外界事物限制成長。