CTA:交易引擎CtaEngine
交易引擎CtaEngine
CTA策略既可以用于投研、回测,也可以用于实盘交易。执行实盘交易的引擎是CtaEngine
。为了保证策略在回测和实盘时接口保持统一,CtaEngine
与BacktestingEninge
在与策略相关的接口设计上有相似之处。
而不同点在于,CtaEngine
要对接实时行情接口、订单提交接口、UI界面接口,不需要绩效分析功能。
代码解读
CtaEngine
的代码在vnpy_ctastrategy -> engine.py
中。
初始设置
class CtaEngine(BaseEngine):
""""""
engine_type: EngineType = EngineType.LIVE # live trading engine
setting_filename: str = "cta_strategy_setting.json"
data_filename: str = "cta_strategy_data.json"
def __init__(self, main_engine: MainEngine, event_engine: EventEngine) -> None:
""""""
super().__init__(main_engine, event_engine, APP_NAME)
self.strategy_setting: dict = {} # strategy_name: dict
self.strategy_data: dict = {} # strategy_name: dict
self.classes: dict = {} # class_name: stategy_class
self.strategies: dict = {} # strategy_name: strategy
self.symbol_strategy_map: defaultdict = defaultdict(list) # vt_symbol: strategy list
self.orderid_strategy_map: dict = {} # vt_orderid: strategy
self.strategy_orderid_map: defaultdict = defaultdict(set) # strategy_name: orderid set
self.stop_order_count: int = 0 # for generating stop_orderid
self.stop_orders: Dict[str, StopOrder] = {} # stop_orderid: stop_order
self.init_executor: ThreadPoolExecutor = ThreadPoolExecutor(max_workers=1)
self.vt_tradeids: set = set() # for filtering duplicate trade
self.database: BaseDatabase = get_database()
self.datafeed: BaseDatafeed = get_datafeed()
symbol_strategy_map
:每一个标的可能有多个策略追踪,故建立一个标的与其对应的一系列策略的映射orderid_strategy_map
与strategy_orderid_map
:在订单编号与策略名称之间建立双向映射
处理实时行情
def call_strategy_func(
self, strategy: CtaTemplate, func: Callable, params: Any = None
) -> None:
"""
Call function of a strategy and catch any exception raised.
"""
try:
if params:
func(params)
else:
func()
except Exception:
strategy.trading = False
strategy.inited = False
msg: str = f"触发异常已停止\n{traceback.format_exc()}"
self.write_log(msg, strategy)
call_strategy_func
提供了调用策略回调函数的同一接口,只需将需要调用的回调函数传入func
参数,而params
是传入回调函数的对象。
def register_event(self) -> None:
""""""
self.event_engine.register(EVENT_TICK, self.process_tick_event)
self.event_engine.register(EVENT_ORDER, self.process_order_event)
self.event_engine.register(EVENT_TRADE, self.process_trade_event)
此处将交易过程产生的事件及其对应的处理函数注册到事件引擎中。
-
EVENT_TICK
:实时Tick行情- 每当实时行情接口受到一笔Tick数据,就会产生一个
EVENT_TICK
并加到事件引擎的事件队列中去 - 事件引擎调用对应函数处理:
def process_tick_event(self, event: Event) -> None: """""" tick: TickData = event.data strategies: list = self.symbol_strategy_map[tick.vt_symbol] if not strategies: return self.check_stop_order(tick) for strategy in strategies: if strategy.inited: self.call_strategy_func(strategy, strategy.on_tick, tick)
其中,
call_strategy_func
函数调用了策略处理Tick数据的回调函数,最终实现了将Tick数据传到策略。 - 每当实时行情接口受到一笔Tick数据,就会产生一个
-
EVENT_ORDER
:订单事件- 处理订单事件
def process_order_event(self, event: Event) -> None: """""" order: OrderData = event.data strategy: Optional[type] = self.orderid_strategy_map.get(order.vt_orderid, None) if not strategy: return # Remove vt_orderid if order is no longer active. vt_orderids: set = self.strategy_orderid_map[strategy.strategy_name] if order.vt_orderid in vt_orderids and not order.is_active(): vt_orderids.remove(order.vt_orderid) # For server stop order, call strategy on_stop_order function if order.type == OrderType.STOP: so: StopOrder = StopOrder( vt_symbol=order.vt_symbol, direction=order.direction, offset=order.offset, price=order.price, volume=order.volume, stop_orderid=order.vt_orderid, strategy_name=strategy.strategy_name, datetime=order.datetime, status=STOP_STATUS_MAP[order.status], vt_orderids=[order.vt_orderid], ) self.call_strategy_func(strategy, strategy.on_stop_order, so) # Call strategy on_order function self.call_strategy_func(strategy, strategy.on_order, order)
-
EVENT_TRADE
:成交事件- 处理成交事件
def process_trade_event(self, event: Event) -> None: """""" trade: TradeData = event.data # Filter duplicate trade push if trade.vt_tradeid in self.vt_tradeids: return self.vt_tradeids.add(trade.vt_tradeid) strategy: Optional[type] = self.orderid_strategy_map.get(trade.vt_orderid, None) if not strategy: return # Update strategy pos before calling on_trade method if trade.direction == Direction.LONG: strategy.pos += trade.volume else: strategy.pos -= trade.volume self.call_strategy_func(strategy, strategy.on_trade, trade) # Sync strategy variables to data file self.sync_strategy_data(strategy) # Update GUI self.put_strategy_event(strategy)
策略的加载与运行
def add_strategy(
self, class_name: str, strategy_name: str, vt_symbol: str, setting: dict
) -> None:
"""
Add a new strategy.
"""
......
strategy: CtaTemplate = strategy_class(self, strategy_name, vt_symbol, setting)
self.strategies[strategy_name] = strategy
# Add vt_symbol to strategy map.
strategies: list = self.symbol_strategy_map[vt_symbol]
strategies.append(strategy)
# Update to setting file.
......
add_strategy
将策略添加到引擎中。
- 这里主要需要理解
CtaEngine
的strategies
字典,它是一个策略名称到策略实例的映射,这样,通过策略名称就能找到策略 - 在
add_strategy
中,完成了将策略实例化为strategy
(注意策略实例的名称strategy_name
不能重复) - 在
symbol_strategy_map
中根据策略所定义的标的,将策略加入其中
CtaEngine
的逻辑下,一个策略实例只支持跟踪单个交易所的单个标的。
def init_strategy(self, strategy_name: str) -> Future:
"""
Init a strategy.
"""
return self.init_executor.submit(self._init_strategy, strategy_name)
def _init_strategy(self, strategy_name: str) -> None:
"""
Init strategies in queue.
"""
strategy: CtaTemplate = self.strategies[strategy_name]
if strategy.inited:
self.write_log(_("{}已经完成初始化,禁止重复操作").format(strategy_name))
return
self.write_log(_("{}开始执行初始化").format(strategy_name))
# Call on_init function of strategy
self.call_strategy_func(strategy, strategy.on_init)
# Restore strategy data(variables)
data: Optional[dict] = self.strategy_data.get(strategy_name, None)
if data:
for name in strategy.variables:
value = data.get(name, None)
if value is not None:
setattr(strategy, name, value)
# Subscribe market data
contract: Optional[ContractData] = self.main_engine.get_contract(strategy.vt_symbol)
if contract:
req: SubscribeRequest = SubscribeRequest(
symbol=contract.symbol, exchange=contract.exchange)
self.main_engine.subscribe(req, contract.gateway_name)
else:
self.write_log(_("行情订阅失败,找不到合约{}").format(strategy.vt_symbol), strategy)
# Put event to update init completed status.
strategy.inited = True
self.put_strategy_event(strategy)
self.write_log(_("{}初始化完成").format(strategy_name))
策略的初始化:做三件事情。
init_executor
是一个单独的初始化线程,用于执行初始化- 一是,调用策略的初始化函数
strategy.on_init
- 二是,设置策略的相关属性
strategy_data
通过load_strategy_data
函数,从本地json配置文件中获得
def load_strategy_data(self) -> None: """ Load strategy data from json file. """ self.strategy_data = load_json(self.data_filename)
- 三是,订阅合约数据
- 最后,推送策略初始化完成的事件
在调用策略的初始化函数strategy.on_init
时,回顾on_init
的内容:
def on_init(self):
"""
Callback when strategy is inited.
"""
self.write_log("策略初始化")
self.load_bar(10)
需要调用load_bar
函数,预先向ArrayManager
填充一部分数据用于计算相关指标。
在实盘时,它最终调用的是CtaEngine -> load_bar
(在回测时是BacktestingEngine -> load_bar
)。
def load_bar(
self,
vt_symbol: str,
days: int,
interval: Interval,
callback: Callable[[BarData], None],
use_database: bool
) -> List[BarData]:
""""""
symbol, exchange = extract_vt_symbol(vt_symbol)
end: datetime = datetime.now(DB_TZ)
start: datetime = end - timedelta(days)
bars: List[BarData] = []
# Pass gateway and datafeed if use_database set to True
if not use_database:
# Query bars from gateway if available
contract: Optional[ContractData] = self.main_engine.get_contract(vt_symbol)
if contract and contract.history_data:
req: HistoryRequest = HistoryRequest(
symbol=symbol,
exchange=exchange,
interval=interval,
start=start,
end=end
)
bars: List[BarData] = self.main_engine.query_history(req, contract.gateway_name)
# Try to query bars from datafeed, if not found, load from database.
else:
bars: List[BarData] = self.query_bar_from_datafeed(symbol, exchange, interval, start, end)
if not bars:
bars: List[BarData] = self.database.load_bar_data(
symbol=symbol,
exchange=exchange,
interval=interval,
start=start,
end=end,
)
return bars
load_bar
按照先后顺序尝试获得数据:
- 首先是网关
Gateway
- 然后是数据接口
datafeed
(前两步可以省略) - 最后是本地数据库
database
def load_tick(
self,
vt_symbol: str,
days: int,
callback: Callable[[TickData], None]
) -> List[TickData]:
""""""
symbol, exchange = extract_vt_symbol(vt_symbol)
end: datetime = datetime.now(DB_TZ)
start: datetime = end - timedelta(days)
ticks: List[TickData] = self.database.load_tick_data(
symbol=symbol,
exchange=exchange,
start=start,
end=end,
)
return ticks
策略初始化有时也需要加载Tick数据。
订单管理
还是回到订单发出的源头,回顾CtaTemplate -> send_order
函数:
def send_order(
self,
direction: Direction,
offset: Offset,
price: float,
volume: float,
stop: bool = False,
lock: bool = False,
net: bool = False
) -> list:
"""
Send a new order.
"""
if self.trading:
vt_orderids: list = self.cta_engine.send_order(
self, direction, offset, price, volume, stop, lock, net
)
return vt_orderids
else:
return []
在实盘的情况下,这里调用的是CtaEngine
的send_order
函数:
def send_order(
self,
strategy: CtaTemplate,
direction: Direction,
offset: Offset,
price: float,
volume: float,
stop: bool,
lock: bool,
net: bool
) -> list:
"""
"""
contract: Optional[ContractData] = self.main_engine.get_contract(strategy.vt_symbol)
if not contract:
self.write_log(_("委托失败,找不到合约:{}").format(strategy.vt_symbol), strategy)
return ""
# Round order price and volume to nearest incremental value
price: float = round_to(price, contract.pricetick)
volume: float = round_to(volume, contract.min_volume)
if stop:
if contract.stop_supported:
return self.send_server_stop_order(
strategy, contract, direction, offset, price, volume, lock, net
)
else:
return self.send_local_stop_order(
strategy, direction, offset, price, volume, lock, net
)
else:
return self.send_limit_order(
strategy, contract, direction, offset, price, volume, lock, net
)
订单类型分为两类:停止单和限价单
- 对于停止单,如果该合约支持报送停止单,则调用
send_server_stop_order
直接调用;否则将停止单存在本地send_local_stop_order
- 对于限价单,则直接调用
send_limit_order
报送
直接报送至网关的函数为send_server_order
,而send_server_stop_order
和send_limit_order
都使用了这个接口。
def send_server_order(
self,
strategy: CtaTemplate,
contract: ContractData,
direction: Direction,
offset: Offset,
price: float,
volume: float,
type: OrderType,
lock: bool,
net: bool
) -> list:
"""
Send a new order to server.
"""
# Create request and send order.
original_req: OrderRequest = OrderRequest(
symbol=contract.symbol,
exchange=contract.exchange,
direction=direction,
offset=offset,
type=type,
price=price,
volume=volume,
reference=f"{APP_NAME}_{strategy.strategy_name}"
)
# Convert with offset converter
req_list: List[OrderRequest] = self.main_engine.convert_order_request(
original_req,
contract.gateway_name,
lock,
net
)
# Send Orders
vt_orderids: list = []
for req in req_list:
vt_orderid: str = self.main_engine.send_order(req, contract.gateway_name)
# Check if sending order successful
if not vt_orderid:
continue
vt_orderids.append(vt_orderid)
self.main_engine.update_order_request(req, vt_orderid, contract.gateway_name)
# Save relationship between orderid and strategy.
self.orderid_strategy_map[vt_orderid] = strategy
self.strategy_orderid_map[strategy.strategy_name].add(vt_orderid)
return vt_orderids
此处将订单信息打包成请求,比回测中稍复杂些。OrderRequest
是发送至网关的请求信息,是与网关通信的方式。
def send_limit_order(
self,
strategy: CtaTemplate,
contract: ContractData,
direction: Direction,
offset: Offset,
price: float,
volume: float,
lock: bool,
net: bool
) -> list:
"""
Send a limit order to server.
"""
return self.send_server_order(
strategy,
contract,
direction,
offset,
price,
volume,
OrderType.LIMIT,
lock,
net
)
def send_server_stop_order(
self,
strategy: CtaTemplate,
contract: ContractData,
direction: Direction,
offset: Offset,
price: float,
volume: float,
lock: bool,
net: bool
) -> list:
"""
Send a stop order to server.
Should only be used if stop order supported
on the trading server.
"""
return self.send_server_order(
strategy,
contract,
direction,
offset,
price,
volume,
OrderType.STOP,
lock,
net
)
若合约不支持停止单,则将停止单存在本地,若停止单被触发,则转为限价单报送。
def send_local_stop_order(
self,
strategy: CtaTemplate,
direction: Direction,
offset: Offset,
price: float,
volume: float,
lock: bool,
net: bool
) -> list:
"""
Create a new local stop order.
"""
self.stop_order_count += 1
stop_orderid: str = f"{STOPORDER_PREFIX}.{self.stop_order_count}"
stop_order: StopOrder = StopOrder(
vt_symbol=strategy.vt_symbol,
direction=direction,
offset=offset,
price=price,
volume=volume,
stop_orderid=stop_orderid,
strategy_name=strategy.strategy_name,
datetime=datetime.now(DB_TZ),
lock=lock,
net=net
)
self.stop_orders[stop_orderid] = stop_order
vt_orderids: set = self.strategy_orderid_map[strategy.strategy_name]
vt_orderids.add(stop_orderid)
self.call_strategy_func(strategy, strategy.on_stop_order, stop_order)
self.put_stop_order_event(stop_order)
return [stop_orderid]
以下是检测停止单是否被触发的函数。回顾上面的process_tick_event
函数,当中调用了check_stop_order
,也就是说,每接收到一笔Tick数据,就会去检查停止单是否被触发。一旦被触发,则转为限价单报送。
def check_stop_order(self, tick: TickData) -> None:
""""""
for stop_order in list(self.stop_orders.values()):
if stop_order.vt_symbol != tick.vt_symbol:
continue
long_triggered = (
stop_order.direction == Direction.LONG and tick.last_price >= stop_order.price
)
short_triggered = (
stop_order.direction == Direction.SHORT and tick.last_price <= stop_order.price
)
if long_triggered or short_triggered:
strategy: CtaTemplate = self.strategies[stop_order.strategy_name]
# To get excuted immediately after stop order is
# triggered, use limit price if available, otherwise
# use ask_price_5 or bid_price_5
if stop_order.direction == Direction.LONG:
if tick.limit_up:
price = tick.limit_up
else:
price = tick.ask_price_5
else:
if tick.limit_down:
price = tick.limit_down
else:
price = tick.bid_price_5
contract: Optional[ContractData] = self.main_engine.get_contract(stop_order.vt_symbol)
vt_orderids: list = self.send_limit_order(
strategy,
contract,
stop_order.direction,
stop_order.offset,
price,
stop_order.volume,
stop_order.lock,
stop_order.net
)
# Update stop order status if placed successfully
if vt_orderids:
# Remove from relation map.
self.stop_orders.pop(stop_order.stop_orderid)
strategy_vt_orderids: set = self.strategy_orderid_map[strategy.strategy_name]
if stop_order.stop_orderid in strategy_vt_orderids:
strategy_vt_orderids.remove(stop_order.stop_orderid)
# Change stop order status to cancelled and update to strategy.
stop_order.status = StopOrderStatus.TRIGGERED
stop_order.vt_orderids = vt_orderids
self.call_strategy_func(
strategy, strategy.on_stop_order, stop_order
)
self.put_stop_order_event(stop_order)
相应地,有一系列撤销订单函数。