[Add] backtesting function for spread trading

This commit is contained in:
vn.py 2019-11-10 16:09:43 +08:00
parent ddbf62d47a
commit 687bdbc66d
5 changed files with 297 additions and 95 deletions

View File

@ -0,0 +1,120 @@
{
"cells": [
{
"cell_type": "code",
"execution_count": 1,
"metadata": {},
"outputs": [],
"source": [
"#%%\n",
"from vnpy.app.spread_trading.backtesting import BacktestingEngine\n",
"from vnpy.app.spread_trading.strategies.statistical_arbitrage_strategy import (\n",
" StatisticalArbitrageStrategy\n",
")\n",
"from vnpy.app.spread_trading.base import LegData, SpreadData\n",
"from datetime import datetime"
]
},
{
"cell_type": "code",
"execution_count": 2,
"metadata": {},
"outputs": [],
"source": [
"spread = SpreadData(\n",
" name=\"IF-Spread\",\n",
" legs=[LegData(\"IF1911.CFFEX\"), LegData(\"IF1912.CFFEX\")],\n",
" price_multipliers={\"IF1911.CFFEX\": 1, \"IF1912.CFFEX\": -1},\n",
" trading_multipliers={\"IF1911.CFFEX\": 1, \"IF1912.CFFEX\": -1},\n",
" active_symbol=\"IF1911.CFFEX\",\n",
" inverse_contracts={\"IF1911.CFFEX\": False, \"IF1912.CFFEX\": False},\n",
" min_volume=1\n",
")"
]
},
{
"cell_type": "code",
"execution_count": 3,
"metadata": {},
"outputs": [],
"source": [
"#%%\n",
"engine = BacktestingEngine()\n",
"engine.set_parameters(\n",
" spread=spread,\n",
" interval=\"1m\",\n",
" start=datetime(2019, 6, 10),\n",
" end=datetime(2019, 11, 10),\n",
" rate=0,\n",
" slippage=0,\n",
" size=300,\n",
" pricetick=0.2,\n",
" capital=1_000_000,\n",
")\n",
"engine.add_strategy(StatisticalArbitrageStrategy, {})"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {
"scrolled": false
},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"2019-11-10 16:09:03.822440\t开始加载历史数据\n"
]
}
],
"source": [
"#%%\n",
"engine.load_data()\n",
"engine.run_backtesting()\n",
"df = engine.calculate_result()\n",
"engine.calculate_statistics()\n",
"engine.show_chart()"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"for trade in engine.trades.values():\n",
" print(trade)"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": []
}
],
"metadata": {
"kernelspec": {
"display_name": "Python 3",
"language": "python",
"name": "python3"
},
"language_info": {
"codemirror_mode": {
"name": "ipython",
"version": 3
},
"file_extension": ".py",
"mimetype": "text/x-python",
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.7.1"
}
},
"nbformat": 4,
"nbformat_minor": 2
}

View File

@ -3,7 +3,9 @@ from pathlib import Path
from vnpy.trader.app import BaseApp from vnpy.trader.app import BaseApp
from vnpy.trader.object import ( from vnpy.trader.object import (
OrderData, OrderData,
TradeData TradeData,
TickData,
BarData
) )
from .engine import ( from .engine import (

View File

@ -1,6 +1,6 @@
from collections import defaultdict from collections import defaultdict
from datetime import date, datetime, timedelta from datetime import date, datetime, timedelta
from typing import Callable from typing import Callable, Type, Dict, List
from functools import lru_cache from functools import lru_cache
import numpy as np import numpy as np
@ -14,7 +14,7 @@ from vnpy.trader.database import database_manager
from vnpy.trader.object import TradeData, BarData, TickData from vnpy.trader.object import TradeData, BarData, TickData
from vnpy.trader.utility import round_to from vnpy.trader.utility import round_to
from .template import SpreadStrategyTemplate from .template import SpreadStrategyTemplate, SpreadAlgoTemplate
from .base import SpreadData, BacktestingMode from .base import SpreadData, BacktestingMode
sns.set_style("whitegrid") sns.set_style("whitegrid")
@ -38,8 +38,8 @@ class BacktestingEngine:
self.capital = 1_000_000 self.capital = 1_000_000
self.mode = BacktestingMode.BAR self.mode = BacktestingMode.BAR
self.strategy_class = None self.strategy_class: Type[SpreadStrategyTemplate] = None
self.strategy = None self.strategy: SpreadStrategyTemplate = None
self.tick: TickData = None self.tick: TickData = None
self.bar: BarData = None self.bar: BarData = None
self.datetime = None self.datetime = None
@ -138,7 +138,8 @@ class BacktestingEngine:
self.spread, self.spread,
self.interval, self.interval,
self.start, self.start,
self.end self.end,
self.pricetick
) )
else: else:
self.history_datas = load_tick_data( self.history_datas = load_tick_data(
@ -208,8 +209,7 @@ class BacktestingEngine:
start_pos, start_pos,
self.size, self.size,
self.rate, self.rate,
self.slippage, self.slippage
self.inverse
) )
pre_close = daily_result.close_price pre_close = daily_result.close_price
@ -427,10 +427,9 @@ class BacktestingEngine:
"""""" """"""
self.bar = bar self.bar = bar
self.datetime = bar.datetime self.datetime = bar.datetime
self.cross_algo()
self.cross_limit_order() self.strategy.on_spread_bar(bar)
self.cross_stop_order()
self.strategy.on_bar(bar)
self.update_daily_close(bar.close_price) self.update_daily_close(bar.close_price)
@ -438,44 +437,39 @@ class BacktestingEngine:
"""""" """"""
self.tick = tick self.tick = tick
self.datetime = tick.datetime self.datetime = tick.datetime
self.cross_algo()
self.cross_limit_order() self.spread.bid_price = tick.bid_price_1
self.cross_stop_order() self.spread.bid_volume = tick.bid_volume_1
self.strategy.on_tick(tick) self.spread.ask_price = tick.ask_price_1
self.spread.ask_volume = tick.ask_volume_1
self.strategy.on_spread_data()
self.update_daily_close(tick.last_price) self.update_daily_close(tick.last_price)
def cross_limit_order(self): def cross_algo(self):
""" """
Cross limit order with last bar/tick data. Cross limit order with last bar/tick data.
""" """
if self.mode == BacktestingMode.BAR: if self.mode == BacktestingMode.BAR:
long_cross_price = self.bar.low_price long_cross_price = self.bar.close_price
short_cross_price = self.bar.high_price short_cross_price = self.bar.close_price
long_best_price = self.bar.open_price
short_best_price = self.bar.open_price
else: else:
long_cross_price = self.tick.ask_price_1 long_cross_price = self.tick.ask_price_1
short_cross_price = self.tick.bid_price_1 short_cross_price = self.tick.bid_price_1
long_best_price = long_cross_price
short_best_price = short_cross_price
for order in list(self.active_limit_orders.values()):
# Push order update with status "not traded" (pending).
if order.status == Status.SUBMITTING:
order.status = Status.NOTTRADED
self.strategy.on_order(order)
for algo in list(self.active_algos.values()):
# Check whether limit orders can be filled. # Check whether limit orders can be filled.
long_cross = ( long_cross = (
order.direction == Direction.LONG algo.direction == Direction.LONG
and order.price >= long_cross_price and algo.price >= long_cross_price
and long_cross_price > 0 and long_cross_price > 0
) )
short_cross = ( short_cross = (
order.direction == Direction.SHORT algo.direction == Direction.SHORT
and order.price <= short_cross_price and algo.price <= short_cross_price
and short_cross_price > 0 and short_cross_price > 0
) )
@ -483,49 +477,49 @@ class BacktestingEngine:
continue continue
# Push order udpate with status "all traded" (filled). # Push order udpate with status "all traded" (filled).
order.traded = order.volume algo.traded = algo.volume
order.status = Status.ALLTRADED algo.status = Status.ALLTRADED
self.strategy.on_order(order) self.strategy.update_spread_algo(algo)
self.active_limit_orders.pop(order.vt_orderid) self.active_algos.pop(algo.algoid)
# Push trade update # Push trade update
self.trade_count += 1 self.trade_count += 1
if long_cross: if long_cross:
trade_price = min(order.price, long_best_price) trade_price = long_cross_price
pos_change = order.volume pos_change = algo.volume
else: else:
trade_price = max(order.price, short_best_price) trade_price = short_cross_price
pos_change = -order.volume pos_change = -algo.volume
trade = TradeData( trade = TradeData(
symbol=order.symbol, symbol=self.spread.name,
exchange=order.exchange, exchange=Exchange.LOCAL,
orderid=order.orderid, orderid=algo.algoid,
tradeid=str(self.trade_count), tradeid=str(self.trade_count),
direction=order.direction, direction=algo.direction,
offset=order.offset, offset=algo.offset,
price=trade_price, price=trade_price,
volume=order.volume, volume=algo.volume,
time=self.datetime.strftime("%H:%M:%S"), time=self.datetime.strftime("%H:%M:%S"),
gateway_name=self.gateway_name, gateway_name=self.gateway_name,
) )
trade.datetime = self.datetime trade.datetime = self.datetime
self.strategy.pos += pos_change self.spread.net_pos += pos_change
self.strategy.on_trade(trade) self.strategy.on_spread_pos()
self.trades[trade.vt_tradeid] = trade self.trades[trade.vt_tradeid] = trade
def load_bar( def load_bar(
self, spread: str, days: int, interval: Interval, callback: Callable self, spread: SpreadData, days: int, interval: Interval, callback: Callable
): ):
"""""" """"""
self.days = days self.days = days
self.callback = callback self.callback = callback
def load_tick(self, spread: str, days: int, callback: Callable): def load_tick(self, spread: SpreadData, days: int, callback: Callable):
"""""" """"""
self.days = days self.days = days
self.callback = callback self.callback = callback
@ -543,14 +537,39 @@ class BacktestingEngine:
lock: bool lock: bool
) -> str: ) -> str:
"""""" """"""
pass self.algo_count += 1
algoid = str(self.algo_count)
algo = SpreadAlgoTemplate(
self,
algoid,
self.spread,
direction,
offset,
price,
volume,
payup,
interval,
lock
)
self.algos[algoid] = algo
self.active_algos[algoid] = algo
return algoid
def stop_algo( def stop_algo(
self, self,
strategy: SpreadStrategyTemplate,
algoid: str algoid: str
): ):
"""""" """"""
pass if algoid not in self.active_algos:
return
algo = self.active_algos.pop(algoid)
algo.status = Status.CANCELLED
self.strategy.update_spread_algo(algo)
def send_order( def send_order(
self, self,
@ -563,23 +582,15 @@ class BacktestingEngine:
lock: bool lock: bool
): ):
"""""" """"""
price = round_to(price, self.pricetick) pass
if stop:
vt_orderid = self.send_stop_order(direction, offset, price, volume)
else:
vt_orderid = self.send_limit_order(direction, offset, price, volume)
return [vt_orderid]
def cancel_order(self, strategy: SpreadStrategyTemplate, vt_orderid: str): def cancel_order(self, strategy: SpreadStrategyTemplate, vt_orderid: str):
""" """
Cancel order by vt_orderid. Cancel order by vt_orderid.
""" """
if vt_orderid.startswith(STOPORDER_PREFIX): pass
self.cancel_stop_order(strategy, vt_orderid)
else:
self.cancel_limit_order(strategy, vt_orderid)
def write_log(self, msg: str, strategy: SpreadStrategyTemplate = None): def write_strategy_log(self, strategy: SpreadStrategyTemplate, msg: str):
""" """
Write log message. Write log message.
""" """
@ -598,6 +609,10 @@ class BacktestingEngine:
""" """
pass pass
def write_algo_log(self, algo: SpreadAlgoTemplate, msg: str):
""""""
pass
class DailyResult: class DailyResult:
"""""" """"""
@ -633,8 +648,7 @@ class DailyResult:
start_pos: float, start_pos: float,
size: int, size: int,
rate: float, rate: float,
slippage: float, slippage: float
inverse: bool
): ):
"""""" """"""
# If no pre_close provided on the first day, # If no pre_close provided on the first day,
@ -648,12 +662,7 @@ class DailyResult:
self.start_pos = start_pos self.start_pos = start_pos
self.end_pos = start_pos self.end_pos = start_pos
if not inverse: # For normal contract self.holding_pnl = self.start_pos * (self.close_price - self.pre_close) * size
self.holding_pnl = self.start_pos * \
(self.close_price - self.pre_close) * size
else: # For crypto currency inverse contract
self.holding_pnl = self.start_pos * \
(1 / self.pre_close - 1 / self.close_price) * size
# Trading pnl is the pnl from new trade during the day # Trading pnl is the pnl from new trade during the day
self.trade_count = len(self.trades) self.trade_count = len(self.trades)
@ -666,18 +675,10 @@ class DailyResult:
self.end_pos += pos_change self.end_pos += pos_change
# For normal contract turnover = trade.volume * size * trade.price
if not inverse: self.trading_pnl += pos_change * \
turnover = trade.volume * size * trade.price (self.close_price - trade.price) * size
self.trading_pnl += pos_change * \ self.slippage += trade.volume * size * slippage
(self.close_price - trade.price) * size
self.slippage += trade.volume * size * slippage
# For crypto currency inverse contract
else:
turnover = trade.volume * size / trade.price
self.trading_pnl += pos_change * \
(1 / trade.price - 1 / self.close_price) * size
self.slippage += trade.volume * size * slippage / (trade.price ** 2)
self.turnover += turnover self.turnover += turnover
self.commission += turnover * rate self.commission += turnover * rate
@ -687,30 +688,72 @@ class DailyResult:
self.net_pnl = self.total_pnl - self.commission - self.slippage self.net_pnl = self.total_pnl - self.commission - self.slippage
@lru_cache(maxsize=999) @lru_cache(maxsize=999)
def load_bar_data( def load_bar_data(
symbol: str, spread: SpreadData,
exchange: Exchange,
interval: Interval, interval: Interval,
start: datetime, start: datetime,
end: datetime end: datetime,
pricetick: float
): ):
"""""" """"""
return database_manager.load_bar_data( # Load bar data of each spread leg
symbol, exchange, interval, start, end leg_bars: Dict[str, Dict] = {}
)
for vt_symbol in spread.legs.keys():
symbol, exchange_str = vt_symbol.split(".")
exchange = Exchange(exchange_str)
bar_data: List[BarData] = database_manager.load_bar_data(
symbol, exchange, interval, start, end
)
bars: Dict[datetime, BarData] = {bar.datetime: bar for bar in bar_data}
leg_bars[vt_symbol] = bars
# Calculate spread bar data
spread_bars: List[BarData] = []
for dt in bars.keys():
spread_price = 0
spread_available = True
for leg in spread.legs.values():
leg_bar = leg_bars[leg.vt_symbol].get(dt, None)
if leg_bar:
price_multiplier = spread.price_multipliers[leg.vt_symbol]
spread_price += price_multiplier * leg_bar.close_price
else:
spread_available = False
if spread_available:
spread_price = round_to(spread_price, pricetick)
spread_bar = BarData(
symbol=spread.name,
exchange=exchange.LOCAL,
datetime=dt,
interval=interval,
open_price=spread_price,
high_price=spread_price,
low_price=spread_price,
close_price=spread_price,
gateway_name=BacktestingEngine.gateway_name,
)
spread_bars.append(spread_bar)
return spread_bars
@lru_cache(maxsize=999) @lru_cache(maxsize=999)
def load_tick_data( def load_tick_data(
symbol: str, spread: SpreadData,
exchange: Exchange,
start: datetime, start: datetime,
end: datetime end: datetime
): ):
"""""" """"""
return database_manager.load_tick_data( return database_manager.load_tick_data(
symbol, exchange, start, end spread.name, Exchange.LOCAL, start, end
) )

View File

@ -1,5 +1,6 @@
from typing import Dict, List from typing import Dict, List
from datetime import datetime from datetime import datetime
from enum import Enum
from vnpy.trader.object import TickData, PositionData, TradeData, ContractData from vnpy.trader.object import TickData, PositionData, TradeData, ContractData
from vnpy.trader.constant import Direction, Offset, Exchange from vnpy.trader.constant import Direction, Offset, Exchange

View File

@ -1,10 +1,12 @@
from collections import defaultdict from collections import defaultdict
from typing import Dict, List, Set from typing import Dict, List, Set, Callable
from copy import copy from copy import copy
from vnpy.trader.object import TickData, TradeData, OrderData, ContractData from vnpy.trader.object import (
from vnpy.trader.constant import Direction, Status, Offset TickData, TradeData, OrderData, ContractData, BarData
)
from vnpy.trader.constant import Direction, Status, Offset, Interval
from vnpy.trader.utility import virtual, floor_to, ceil_to, round_to from vnpy.trader.utility import virtual, floor_to, ceil_to, round_to
from .base import SpreadData, calculate_inverse_volume from .base import SpreadData, calculate_inverse_volume
@ -434,6 +436,20 @@ class SpreadStrategyTemplate:
""" """
pass pass
@virtual
def on_spread_tick(self, tick: TickData):
"""
Callback when new spread tick data is generated.
"""
pass
@virtual
def on_spread_bar(self, bar: BarData):
"""
Callback when new spread bar data is generated.
"""
pass
@virtual @virtual
def on_spread_pos(self): def on_spread_pos(self):
""" """
@ -635,3 +651,23 @@ class SpreadStrategyTemplate:
""" """
if self.inited: if self.inited:
self.strategy_engine.send_email(msg, self) self.strategy_engine.send_email(msg, self)
def load_bar(
self,
days: int,
interval: Interval = Interval.MINUTE,
callback: Callable = None,
):
"""
Load historical bar data for initializing strategy.
"""
if not callback:
callback = self.on_spread_bar
self.strategy_engine.load_bar(self.spread, days, interval, callback)
def load_tick(self, days: int):
"""
Load historical tick data for initializing strategy.
"""
self.strategy_engine.load_tick(self.spread, days, self.on_spread_tick)