上一篇文章裏,我們提及瞭如何設計向量化回測與調參。通過scikit-learn這套框架,你已經能快速的驗證你的任何想法。同時通過不斷地努力,也發現了一個頗有成效的策略模式,那麼接下來,讓我們把回測模型一點點轉變得更接近現實世界吧。

所以這一章,我們將分別聊一遍 事件驅動回測框架實盤交易系統 的架構與實現細節。這篇開始就逐漸有工程味道了,小夥伴們準備好哦。

參考

  • Architecture: quantinsti.com/blog/tra
  • vnpy: github.com/vnpy/vnpy/
  • backtrader: backtrader.com/
  • OMS:quantinsti.com/blog/aut

一、事件驅動回測

承接著向量化回測,這一篇我們先說事件驅動回測模塊。開始之前,先明確列出我們對事件驅動系統的需求:

1. 回測時,將儘可能模擬真實環境。

  • 市場數據將以發生時間為順序,一個個喂到系統中,我們的策略將一一對其響應。
  • 訂單撮合方式多樣。如訂單要等到市場價格與訂單價格發生交叉時,按給定掛單量進行撮合。

2. 策略擴展性強

  • 資產類型上能交易股票、期貨、電子貨幣
  • 合約類型上能交易現貨、期貨、期權、掉期
  • 每個策略可以交易多個合約

3. 策略有實時風控、倉位管理

4. 工程上,要能保證策略開發完後能無縫對接到實盤之中


我們一個一個需求來解決。

首先先明確事件驅動概念。事件驅動有別於向量化回測,更加接近真實,信息不再是一次性以一大塊矩陣的形式送到系統之中,而是以事件的形式一個一個被喂到系統裏,觸發設定好的回調來進行邏輯上的處理。

事件驅動回測中的各個模塊如下,我們將逐一對其進行介紹。


我們先來理解一下各個模塊的概念和作用,之後再正式進入細節上的探究。

Strategy,顧名思義,是我們的核心策略模塊,包含了4個重要組件,分別是

  • Event Processor,事件處理器,是策略核心的處理邏輯。其內部包含了多個回調函數,當接收到訂閱的事件時,會自動被觸發執行,也可稱之為事件驅動。通常會進行信號處理、交易決斷等,可由交易員根據策略自行定義操作。(需求1
  • Position Manager,倉位管理器,負責實時的倉位、盈虧計算,是核心模塊,因為你的策略可能不僅僅只交易現貨合約,可能還有期貨、期權、掉期等。品種也不一樣,可以是股票、可以是電子貨幣、也可以是外匯。良好的擴展性是非常重要的,將極大地提高你在複雜策略上的開發效率,如期現套利等。(需求2
  • Risk Manager,風控管理,負責檢查訂單風險和倉位風險。訂單風險主要是為了提防錯誤數額的交易量,避免類似「烏龍指」事件的發生。倉位風險則主要關注在實時止損上,有時,也涉及動態止盈。(需求3
  • Commander,交互控制,負責執行外界發來的指令控制,如策略的啟動、停止,手動訂單的發送、取消,以及相關策略參數的修改等。

StrategyEngine,策略引擎,負責管理多個策略。策略引擎在這裡是一個抽象,具體實現上有兩種,第一種就是回測引擎,另一種則是後面將提及的實時交易引擎。有了策略引擎這一層抽象,就能保證我們的策略在回測完之後,能無縫地接入到實盤進行交易。(需求4

這裡我們先介紹BackTestEngine,即回測引擎。回測引擎在程序中主要負責將數據(如bar、depth、trade、order等)轉化成事件、按時間發生的順序,一個個地餵給策略。其中比較重要的是兩個組件。

  • Matcher,撮合器,負責訂單成交的模擬。最簡單的就是訂單提交後立刻按照原價格成交。複雜點的,則會將訂單掛在Matcher內部的訂單簿上,等到市場價格發生了交叉後,在進行交易,同時會附上部分滑點和手續費。
  • Analyzer,分析器,負責記錄策略在回測期間的凈值變動情況,並計算出給定的回測指標,做可視化分析(採用pyfolio)。


概念講的隨意,但深入各個組件的設計上就有很多活幹了。慶幸的是,如第一篇文章曾提及的那樣,我們並不需要完全從零開始設計,借鑒開源項目vnpybacktrader,將幫我們的設計省下不少時間。

首先,我們先關注組件之間的信息流交互。下面是一張順序圖,描述了當一個bar數據被送到系統裏時發生的操作。

  1. BackTest Engine發出bar數據,先餵給Matcher,更新信息,檢查當時是否有未成交的訂單可以被成交(繞口!),方便起見,先假設沒有
  2. Event Processor收到轉發來bar信息,進行給定的邏輯處理和信號計算,如果信號被觸發,則生成訂單
  3. Risk Manager檢查訂單風險
  4. Position Manager檢查是否資金足夠,若是,則凍結資金,並發出訂單。
  5. Matcher收到訂單,模擬成交
  6. Event Processor,處理成交推送信息。一般來說,配對交易會對這塊的邏輯涉及的比較多。
  7. Position Manager,解凍資金、更新倉位及盈虧

現在我們重點關注一下如下幾個組件

BackTest EngineEvent Processor

BackTest Engine的設計理念就是廣為人知的觀察者模式。我們將把回測引擎設計成觀察者模式中Subject,是發布者。而策略則是一個Subscriber,即訂閱者,當有相關主題的數據發出來時,策略中的Event Processor就會對事件進行處理。

一般來說,事件和對應的處理函數主要分為這幾類, 其中的邏輯細節由交易員或研究員自行補充完整:

這塊重點提一下訂閱者與發布者這兩個核心基類,實現如下:

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)

使用時主要分兩步:

  1. 訂閱者訂閱主題:Subscriber調用subscribe函數,讓Subject知道自己訂閱了它的某個主題。
  2. 發布者發布事件:當Subject收到某個信息,他會將其轉化為一個帶有特定主題的事件對象,並調用notify函數,告訴訂閱了該主題的訂閱者們訂閱事件發生。此時訂閱者將立刻執行對應的回調函數,而這正是事件驅動的由來與核心所在。

別小看這簡簡單單的十來行代碼,在後期的工程開發,這個抽象模板將幫你省下不少功夫。模塊之間的耦合性大大降低,組件間的信息流動更加明確,開發者也可以將更多的精力放在業務邏輯處理之中。

參考:vnpy/EventEngine。

Position Manager

Position Manager是比較關鍵的一塊,此處,筆者並沒有直接使用其他開源項目進行改造,而是自己動手重新設計,加入兩組概念。

第一組新的概念是PositionPortfolioPosition是記錄了一個合約上交易的盈虧,Portfolio則管理了多個Position對象。如前文所述,這裡的Position要能支持多種資產(股票、外匯、電子貨幣)以及多種合約(現貨、期貨、掉期)。

第二組概念 是base_assetquote_asset,譬如對應外匯交易中,USD/HKD這樣的合約交易中,USD就是base_assetHKDquote_asset。如果只進行股票、期貨交易,可以略過這個設計,因為他們會使系統的複雜性升高,但相應的,你的系統也可以支持一些複雜的場景,例如外匯的三角套利。這裡我們先略過其細節,等後續有機會講實盤策略時再詳述其設計。

回到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,分別負責計算委託成交時、以及實時市場價格變動時,組合的盈虧變動情況。

這樣的抽象設計,就能使我們的程序不僅能處理現貨交易(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)


二、實盤交易

至此,我們已經能用事件驅動的方式回測我們的策略,回測的結果也將更加符合真實情況。更重要的是,我們能復用這套框架,直接對接到我們的實盤交易系統中,保證策略「所測即所得」

相同的,我們也先明確我們對實盤交易系統的需求所在。

  1. 多應用同時運行,且易於擴展,包括但不僅限於 多策略管理、可視化監控界面
  2. 有多種訂單執行方式供策略選擇,如Limit、Market、VWAP、SOR等
  3. 對接資料庫,持久化訂單委託、成交明細等數據,方便每日復盤
  4. 多交易所、多賬戶同時連接,連接需要穩定可靠
  5. 能支持歷史測試交易、實時模擬交易以及實盤交易

第一個需求比較容易實現。我們可以直接將BackTestEngine替換成TradingEngine,負責管理多個策略。同時,我們也把這個TradingEngine抽象成一個Application,相同的層級還包括Scrawler實時數據爬取器,Monitor UI市場監控器等。

在實現上,他們有一個相同點,那就是訂閱並監聽從上一層發過來的信息,原理即上文所介紹的訂閱者模式。訂閱各自關注的主題後,各個應用便能獨立運行,也就是說,各個應用之前相互獨立,同時與其依賴的底層關聯度也極低,僅是信息訂閱這一項而已,二者之間幾乎沒有耦合性,這能極大地方便我們開發的進程。(需求1

於是,我們可以把關注點轉移到這個應用們所依賴的這個」底層」,即交易系統的「交易服務中臺」。

交易服務中臺

中臺主要有兩個核心組件, CEP(複雜事件處理引擎)+ OMS(訂單管理系統)。

CEP 複雜事件處理引擎

CEP,即Complex Event Processing,複雜事件處理系統,是真正意義上驅動整個系統運行的引擎。主要由如下三個組件構成

1. 事件分發器

事件分發器,本質上也是一個Subject,負責將各個主題的分發出去,要稍微留意的是,單個事件的主題數量可多於1個。例如對於訂單信息而言,我們一般賦予它兩個主題:

  • EnumOrderStatus,這是最通用的主題,由單個枚舉變數構成,一般來說,UI監控器、總倉位管理器、持久化引擎會訂閱這個主題
  • (EnumOrderStatus, strategy_id),一個元組類型的主題,其中strategy_id是發送該訂單的策略ID,該ID通常是唯一的。通過構造這樣的主題,就能保證訂單信息只會被發送到它的「主人策略」去,而不錯發到其他地方去。

2. 信號計算器

信號計算器,負責將通過的信號計算出來,避免每個策略單獨計算,浪費資源。通常的任務包括 - K線集成: 1分鐘K --> N分鐘K --> 小時K --> 日K - 成交量K線生成 - 期權greek計算

3. 持久化引擎

持久化引擎,負責將存儲重要數據,並持久化到資料庫中,同時具備實時查詢功能。一般我們會將實時市場行情、訂單委託、成交、命令請求等都存入到該引擎中。

技術層面上,推薦使用 ORM,這樣能將技術細節的實現推到最後來完成,解耦系統與資料庫的依賴關係。未來無論是連接MongoDB還是MySQL,只需切換數據連接器,便能順滑過渡。

同時要注意的是,因為我們需要把數據持久化到資料庫裏,這就會涉及到I/O操作。I/O操作一般都要比內存操作慢N個數量級。為了保證系統運行的效率,我們一般會對此採取非同步操作。同樣的處理也出現在後面要提及的向交易所發送交易請求。(需求3


OMS 訂單管理系統

訂單管理系統也由3個組件構成:

  1. 總風控
  2. 總倉位管理
  3. 訂單演算法引擎

總風控和總倉位管理,將直接復用單策略裏的風控、倉位管理對象。只是職責上,總風控將是更關注總體的倉位、訂單風險,總倉位管理則實時匯總所有策略的倉位、盈虧情況。

這裡我們更關注訂單演算法引擎。實盤中有這樣一個公式,利潤 = 策略 + 交易,好的策略很重要,但好的交易方式也不可缺少,如何減少訂單的衝擊成本,如何獲得市場的最佳價格,同樣是一門學問。

訂單演算法引擎負責處理策略發來的訂單請求,並按照設計好的演算法,將訂單發送到交易所中。(需求2

最常見的訂單演算法,包括:

  • STOP,停止單,觸及某個價格時生成訂單
  • VWAP,將訂單分割成多個小訂單執行
  • Best Limit Price,永遠只下最優限價單,價格變動時即刻撤單重發
  • Smart Order Router,智能路由訂單,將單個訂單到不同交易所上,選擇提供最優價格的交易所進行成交

前3個演算法相對容易實現,提一下智能路由訂單演算法。

這個演算法在電子貨幣交易領域中使用得很多,因為這個市場上的交易所實在是太多了,像ETH/BTC這樣的合約,市場上有幾十個,交易價格參差不齊。我們的目標是選擇其中幾個穩定的交易所,交易時,選擇各個交易所中價格最優的進行掛單交易。

實現這個功能,需要我們要能快速地實時合併不同交易所訂單簿,使得該合併訂單簿能夠:

  1. 查詢O(1):直接查詢某個價格對應的總市場掛單量,並知道各個交易量在該價格上的掛單量
  2. 刪除、插入O(1):收到變動的市場深度行情,更新、創建或刪除對應合併訂單簿的價格檔位
  3. 排序O(N):一個大單的交易量可能超過單個檔位上的掛單量,在拆單時,這就要求我們需要按照最優價格的順序來喫掉多個檔位

抽象成演算法題,就是請你設計一個數據結構,使得查詢O(1),插入O(1),刪除O(1),排序O(N)的數據結構。顯然,這不是單個數據結構就能解決的,需要多個數據結構之間互相配合才能實現。權且留作這篇文章的思考題吧,對未來的演算法面試會有幫助的。

回到訂單結構的實現上,為了方便統一介面類型,我們可以將演算法訂單整合到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

底層數據流端

前臺、中臺都打通了,現在就差後臺了,也就是底層真正和交易所進行連接的模塊。

如圖所示,這裡主要有兩個部分構成

1. 多交易所連接

Gateway,交易所對接網關,負責將各個交易所五花八門的介面統一起來,接入系統。每個Gateway至少需要包含以下職能:

  1. 請求發送

信息請求將包括 (1).合約下載 (2).交易請求 (3).信息查詢

這裡著重講一下合約信息。合約信息是與交易所對接的關鍵,因為在發送訂單等信息到交易所時,必須要嚴格符合該交易所的格式標準。例如訂單發送時不能填寫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介面。

在工程上的,交易所連接部分有2個注意事項:

  • 斷線重連的實現。網路是不穩定的,可能由於各種因素連接斷掉了,這時要能立刻重連回交易所,避免漏掉重要行情。
  • 訪問次數的限制。普通的網站都會有一定的反爬蟲機制來限制訪問次數,交易所繫統尤甚,所有要注意自己的發單頻率是否超過了限制,否則,我們的賬號可能在其後幾個小時內都被封禁而無法交易了。

2.訂單路由器

Router,訂單路由器,負責將訂單發送到指定的交易所進行成交。(需求4

實現上要做到的功能包括

- 多交易所:同時連接多個交易所,這是進行跨交易所套利的基礎。

- 多賬戶:同時包含多個賬戶,這是對外提供交易服務的基礎

- 多類型:可以設置不同的連接類型

  • Real trading 實時數據流、真實訂單成交
  • Paper trading 實時數據流、模擬訂單成交
  • Simu trading 歷史數據流、模擬訂單成交

如此一來,在策略回測完畢之後,可以先使用Simu trading模式,比對兩者結果,確認策略無bug、系統無風險。之後,就可以上Paper trading,不使用真實賬戶資金,觀察實盤效果。如果效果良好,那麼,就切換到真正的實盤模式,加入自營資金,跑起策略來了。

實現上也很簡單,我們將定義多個Router的子類。(需求5

  • 如果是歷史回測(SimuTradingRouter),則從資料庫裏讀取歷史行情數據,轉化成事件發送出去,訂單成交交給Matcher負責。
  • 如果是實時模擬交易(PaperTradingRouter),則讀取實時數據,訂單成交發送給Matcher負責。
  • 如果是實盤交易(RealTradingRouter),則讀取實時數據,訂單成交由交易所負責

三、小結

現在,我們的實盤交易系統差不多就建好了。當然,部署和運維也是一大塊,後期也還需要不斷維護和迭代。

一如既往,我們還是需要分析一下系統,瞭解其中的不足之處。

首先是關於系統的性能方面,系統的瓶頸將會出現在3個地方:

  • 硬體瓶頸:網路
    • 一般的伺服器,網卡差、離交易所遠,系統延遲不可避免
  • 環境瓶頸:語言
    • GIL限制Python只能進行單核計算
    • 動態語言的特性進一步限制了Python的性能,有興趣的小夥伴可以搜搜這本書《Cython, A guide for Python programmers》,它會告訴你 1+1這個看似簡單的操作,Python的虛擬機裏要進行多少步操作才能完成。
  • 程序瓶頸:隊列
    • 對於事件驅動系統而言,不同模塊之間的信息交互,是使用隊列queue完成的。Python的queue能很好地應對multi-producer, multi-consumer的環境,但是其中對鎖的使用,會極大拖下程序的性能。或許我們該嘗試一下流行的無鎖化隊列以及CAS技術。

其次則是缺少一個統一的、標準化的、適合團隊協同工作的解決方案

  1. 策略開發流程、成果管理、上線標準
  2. 如何部署、如何監控
  3. 每日報告生成、PnL reconciliation

如果這些環節不完善的話,那麼整個交易系統也只是一堆花哨卻無意義的代碼。要與團隊相結合,運作流轉起來,才能最大程度地發揮系統的效能。

優秀的你對這兩個方向一定也有很多自己的想法,所以請繼續關注我的下篇文章,我們到時將一起深入探討更快的技術和更有效率的解決方案

四、題外話:工程思維

回頭看來,我們不難發現,這個交易系統已經不再只是簡單的程序包了,而是一個完整的工程項目。工程項目的管理也是一門藝術,有不少的工程規則和知識,這裡和大家分享一下這一路的心得

  • 架構原則
    • 敏捷開發
    • 分層架構
    • 設計模式
    • 面向對象
  • 流程
    • 需求分析
    • 驗收設計
    • 編碼實現
    • 持續集成
  • 工具
    • 開發IDE Pycharm
    • 版本控制 git + bitbucket
    • 項目管理 leangoo
    • 持續集成 TeamCity
    • 工具各有喜好,但要做到「唯手熟爾」

這些是都是軟體工程中的基本知識,但每一個展開都有極深的內容,點點滴滴,相信在搭建系統的過程中,我們會慢慢積累、慢慢成長。這也算是造這個交易系統輪子的意義之一。

聽到這,或許你會這樣反問道:「太CS了吧...」

確實如此。你大可用其他的、任何你覺得合適的現成平臺進行策略開發和交易。但原則上,你要能保證自己的發展不會被限制在某個在線平臺、或者某個公司裏。你的價值不會因為你的離職或者網站的關閉而減損。

對於一個Quant來說,當他受制於人的那一刻,就已經註定了是一場悲劇。

我的觀點是:要一直刻意地訓練自己,保持自己的競爭力。有獨立的思考、也有紮實的技術和執行力,有能力隨時驗證自己的想法並將其執行落地,不被外界事物限制成長。


推薦閱讀:
相關文章